After useful discussions in other threads about sorting filenames by 3 substrings, here are a few notes, with:
- line data and record data examples,
- some source code, and
- a couple of working macros with various custom sorts,
about the basics of sorting anything (any lines or records of data) by using JavaScript actions (JXA or browser) in Keyboard Maestro:
- Using primary, secondary and N-ary sort keys
- Using any mix of Ascending and Descending orders for each key
- Specifying custom sorts which don't depend on the order of 'fields' or 'columns' in the data.
Basic functions for sorting in JS
The built-in Array.sort() method lets us provide a custom function on two arguments (a, b)
which returns an 'Ordering' value: -1 or 0 or 1, depending on whether a
is less than, equal to, or more than b
.
For a simple one-key sort, all we need to write or paste is a compare(a, b)
function, which takes two arguments of the same type (e.g. two numbers, strings, or booleans which we want to compare), and returns an Ordering value.
// compare :: a -> a -> Ordering
const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);
If we want secondary, tertiary ... n-ary sort keys, we can append a list of these Ordering values together in a special way: as soon as one of them has a non-zero value (i.e. not equal: either -1 for 'less than' or 1 for 'more than), we just ignore the rest – primary keys override secondary keys, secondaries override tertiaries and so on.
The pattern when we are combining two Ordering values (a, b)
(e.g. for the primary and secondary sort keys) is just: if a is not zero then we use a
. Otherwise, we use b
.
// Ordering :: ( LT | EQ | GT ) | ( -1 | 0 | 1 )
// appendOrdering :: Ordering -> Ordering -> Ordering
const appendOrdering = (a, b) => a !== 0 ? a : b;
With these basics, we can now define our custom sorts.
A custom sort is an array of simple property-getting functions, in the order of our primary, secondary and n-Ary sort keys. Each function is applied to a line or record of data, and returns a value (for example a field of the data, the key of a record, or a particular part of a string).
For example, if we want to sort a KM variable containing some record lines like these in various ways:
{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}
We could sort first by name of Country, and then by descending populations size, with a sequence of the following two functions:
x => x.country
x => x.pop // false
Note that this is using ES6 (Sierra onwards JS) anonymous
functions or lambda
functions.
If you were trying to do something like this in the older ES5 JavaScript you would need to use the following, slightly wordier format:
function (x) { return x.country }
function (x) { return x.pop } // false
Ascending sort is the default. In the attached macros, if you add false
after a pair of comment slashes, this is read to mean that you want a descending sort.
Another example – if we want to separate out the large cities that are national capitals from those that are not, we can use the capital
key here as the primary sort key, and perhaps the Country name as the secondary key:
x => x.capital // false // DESC :: true (1) then false (0)
x => x.country
There are two comment markers here. Only the first is used, the second is ignored, so you can use it for adding notes.
There are other examples, both for record sorting and for sorting tab-delimited lines, in the attached macros. The code is the same for both.
What are these macros doing ? They are combining the Ordering values returned by an array of functions (of any length) into a comparison function (like our first compare function) which can be passed to Array.sort(); They are also taking account of any false or true in the first comment string after a function, to choose between Ascending and Descending sorts.
The function which combines the Orderings and the direction Booleans is (in an ES6, Sierra onwards JS idiom):
// mappendFoldComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
const mappendFoldComparing = fbs =>
(x, y) => fbs.reduce(
(ord, [f, bool]) => ord !== 0 ? ord : (
(bool ? id : flip)(compare)(f(x), f(y))
), 0
);
Note that:
- It uses our simple
compare
function - When one of the sort keys is being used in a descending order, it creates a 'flipped' version of
compare
, which reverses the sort order by reversing the order of the (a, b) arguments of compare to (b, a).
Flip can be written in JS as:
// flip :: (a -> b -> c) -> b -> a -> c
const flip = f => (a, b) => f.apply(null, [b, a]);
The main mappendFoldComparing function uses a 'fold' (in JS terms the Array.reduce() method) rather than a loop.
Using a fold/reduce simplifies code, and makes it less accident-prone, but it may not yet be as familiar to some scripters as an iterating loop with a mutating counter variable. To unpack what an mppendComparing function is doing, here is a looping (ES5) rewrite with some comments. More verbose, and more detail that needs to be checked and can go wrong, but perhaps a bit clearer at first acquaintance ?
// ILLUSTRATIVE ONLY - NOT USED IN THE MACROS BELOW
// AN ITERATIVE AND COMMENTED ES5 VERSION, FOR COMPARISON WITH FOLD
// VERSION: mappendFoldComparing() (below)
// mappendIterComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
function mappendIterComparing(fbs) {
// Given an ordered list of functions (tupled with direction Bools),
// each of which returns some field or value
// derived from a line or record of the data,
// as well as a Bool :: True for Ascending | False for Descending,
return function (x, y) {
// this returns a single comparator function
// which can be passed to Array.sort()
var lng = fbs.length,
ord = 0; // An Ordering value: ( -1 | 0 | 1 )
for (i = 0; i < lng; i++) {
// The nth function to sort by,
// tupled with a Bool (Ascending::True, Descending::False)
var fb = fbs[i],
f = fb[0],
bool = fb[1];
// An ASCending or DESC (flipped) comparison of the values
// that are returned by f when applied to x and y respectively.
var ord = (bool ? id : flip)(compare)(f(x), f(y));
if (ord !== 0) return ord; // Only first non-zero value needed
}
return ord;
};
};
For more examples, and for use of the same code with delimited or other text lines, see the two attached macros. Each defines 3/4 different custom sorts in Set KM Variable to Text
actions. Make sure that you use the gear wheel on these actions to enable only one custom sort at a time.
Sorting Macros.kmmacros (60.9 KB)