Or again, extending it to Tom's data sample, and proceeding for the sake of variety by splits (my patience for regex-fiddling shortens with advancing years )
Sorted filenames by splits.kmmacros (23.2 KB)
JavaScript ES6 source (Paste into Babel JS REPL at https://babeljs.io/repl/ to get pre-Sierra ES5 JavaScript source)
(() => {
'use strict';
// GENERIC FUNCTIONS -----------------------------------------------------
// (++) :: [a] -> [a] -> [a]
const append = (xs, ys) => xs.concat(ys);
// concat :: [[a]] -> [a] | [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// elem :: Eq a => a -> [a] -> Bool
const elem = (x, xs) => xs.indexOf(x) !== -1;
// id :: a -> a
const id = x => x;
// init :: [a] -> [a]
const init = xs => xs.length > 0 ? xs.slice(0, -1) : [];
// intercalate :: String -> [a] -> String
const intercalate = (s, xs) => xs.join(s);
// isDigit :: Char -> Bool
const isDigit = c => {
const n = ord(c);
return n >= 48 && n <= 57;
};
// last :: [a] -> a
const last = xs => xs.length ? xs.slice(-1)[0] : undefined;
// length :: [a] -> Int
const length = xs => xs.length;
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
const mappendComparing = fs => (x, y) =>
fs.reduce((ord, f) => (ord !== 0) ? (
ord
) : (() => {
const
a = f(x),
b = f(y);
return a < b ? -1 : a > b ? 1 : 0
})(), 0);
// ord :: Char -> Int
const ord = c => c.codePointAt(0);
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = (f, xs) =>
xs.slice()
.sort(f);
// Splitting not on a delimiter, but whenever the relationship between
// two consecutive items matches a supplied predicate function
// splitBy :: (a -> a -> Bool) -> [a] -> [[a]]
const splitBy = (f, ys) => {
const
bool = typeof ys === 'string',
xs = bool ? ys.split('') : ys;
return (xs.length < 2) ? [xs] : (() => {
const
h = xs[0],
lstParts = xs.slice(1)
.reduce(([acc, active, prev], x) =>
f(prev, x) ? (
[acc.concat([active]), [x], x]
) : [acc, active.concat(x), x], [
[],
[h],
h
]);
return map(
(bool ? concat : id),
lstParts[0].concat([lstParts[1]])
);
})();
};
// splitOn :: a -> [a] -> [[a]]
// splitOn :: String -> String -> [String]
const splitOn = (needle, haystack) =>
typeof haystack === 'string' ? (
haystack.split(needle)
) : (function sp_(ndl, hay) {
const mbi = findIndex(x => ndl === x, hay);
return mbi.nothing ? (
[hay]
) : append(
[take(mbi.just, hay)],
sp_(ndl, drop(mbi.just + 1, hay))
);
})(needle, haystack);
// toLower :: Text -> Text
const toLower = s => s.toLowerCase();
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// COMPARABLE FILE NAME PARTS --------------------------------------------
// suffixDotSplit :: String -> (String, String)
const suffixSplit = s =>
elem('.', s) ? (() => {
const xs = splitOn('.', s);
return [intercalate('.', init(xs)), last(xs)];
})() : [s, ''];
// stemNumSplit :: String -> (String, String)
const stemNumSplit = s => {
const tpl = splitBy(
(a, b) => isDigit(b) && !isDigit(a),
suffixSplit(s)[0]
);
return length(tpl) > 1 ? tpl : append(tpl, ["0"]);
};
const stemPreNum = s => toLower(stemNumSplit(s)[0]);
const stemNum = s => parseInt(stemNumSplit(s)[1], 10) || 0;
const suffix = s => suffixSplit(s)[1];
// TEST ------------------------------------------------------------------
return unlines(
sortBy(
mappendComparing(
[stemPreNum, stemNum, suffix]
),
lines(
Application('Keyboard Maestro Engine')
.getvariable('fileNames')
)
)
);
})();