Updated code – a JavaScript version (with examples) of the sortOn(f, xs) function posted for Applescript at:
(() => {
'use strict';
// Sort a list by comparing the results of a key function applied to each
// element. sortOn f is equivalent to sortBy (comparing f)
// sortOn :: Ord b => (a -> b) -> [a] -> [a]
const sortOn = (f, xs) => {
// Functions and matching bools derived from argument f
// which may be a single key function, or a list of key functions
// each of which may or may not be followed by a direction bool
const [fs, bs] = unzip(
flatten([f])
.reduceRight((a, x) =>
typeof x === 'boolean' ? {
asc: x,
fbs: a.fbs
} : {
asc: true,
fbs: [
[x, a.asc]
].concat(a.fbs)
}, {
asc: true,
fbs: []
})
.fbs
),
iLast = fs.length;
// decorate-sort-undecorate
return sortBy(mappendComparing(
// functions that access pre-calculated values by position
// in the decorated ('Schwartzian') version of xs
zip(
fs.map((_, i) => x => x[i]),
bs
)
), xs.map( // xs decorated with precalculated key function values
x => fs.reduceRight(
(a, g) => [g(x)].concat(a), [
x
])))
.map(x => x[iLast]); // undecorated version of data, post sort.
};
// -----------------------------------------------------------------------
// compare :: a -> a -> Ordering
const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);
// flatten :: Tree a -> [a]
const flatten = t =>
Array.isArray(t) ? (
[].concat.apply([], t.map(flatten))
) : t;
// mappendComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
const mappendComparing = fboolPairs =>
(x, y) => fboolPairs.reduce(
(ord, [f, b]) => ord !== 0 ? (
ord
) : (
b ? compare(f(x), f(y)) : compare(f(y), f(x))
), 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);
// unzip :: [(a,b)] -> ([a],[b])
const unzip = xys =>
xys.reduceRight(([xs, ys], [x, y]) => [
[x].concat(xs), [y].concat(ys)
], [
[],
[]
]);
// zip :: [a] -> [b] -> [(a, b)]
const zip = (xs, ys) =>
xs.slice(0, Math.min(xs.length, ys.length))
.map((x, i) => [x, ys[i]]);
// TEST -------------------------------------------------------------------
// Data to sort
// greekLetterNames :: () -> [String]
const greekLetterNames = () => [
"alpha", "beta", "gamma", "delta", "epsilon", "zeta",
"eta", "theta", "iota", "kappa", "lambda", "mu"
];
// cities :: () -> [Dict]
const cities = () => [{
city: "Shanghai",
pop: 24.3,
country: "China",
capital: false
},
{
city: "Beijing",
pop: 21.5,
country: "China",
capital: true
},
{
city: "Delhi",
pop: 11.0,
country: "India",
capital: true
},
{
city: "Lagos",
pop: 16.0,
country: "Nigeria",
capital: true
},
{
city: "Karachi",
pop: 14.9,
country: "Pakistan",
capital: false
},
{
city: "Dhaka",
pop: 14.5,
country: "Bangladesh",
capital: true
},
{
city: "Guangzhou",
pop: 14.0,
country: "China",
capital: false
},
{
city: "Istanbul",
pop: 14.0,
country: "Turkey",
capital: false
},
{
city: "Tokyo",
pop: 13.5,
country: "Japan",
capital: true
}
];
// Key Functions for sort descriptors ------------------------------------
// country :: Dict -> String
const country = x => x.country;
// isCapital :: Dict -> Bool
const isCapital = x => x.capital;
// population :: Dict -> Num
const population = x => x.pop;
// reverseString :: String -> String
const reverseString = s =>
s.split('')
.reverse()
.join('');
// stringLength :: String -> Int
const stringLength = s =>
s.length;
return [
// Sorting from suffix toward prefix, ASCENDING
sortOn(reverseString, greekLetterNames()),
// From -da up to -mu
// -> ["lambda","alpha","gamma","kappa","eta","beta","theta",
// "zeta","delta","iota","epsilon","mu"]
// Sorting from suffix toward prefix, DESCENDING
sortOn([reverseString, false], greekLetterNames()),
// From -mu down to -da
// -> ["mu","epsilon","iota","delta","zeta","theta","beta","eta",
// "kappa","gamma","alpha","lambda"]
// Primary sort : Country name Ascending ('Bangladesh' first)
// Secondary sort : isCapital Boolean Descending (True then False)
// Beijing, as capital, comes before Shanghai and Guangzhou.
// Tertiary sort : Population Descending - Shanghai before Guangzhou
sortOn([country, isCapital, false, population, false], cities()),
// Key functions can optionally be bracketed with any direction bools
// that follow them, for legibility.
sortOn([country, [isCapital, false], [population, false]], cities())
];
})();