RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

Ramda.js Array Sorting (With Tiebreakers) Using R.comparator, Variadic R.either, and R.reduce

A recent exercise in data processing with Ramda.js: I wanted to sort an array of objects by their key/value pairs. More specifically, I wanted to sort an array that looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
    {
        "code": "AUT",
        "gold": 9,
        "silver": 5,
        "bronze": 7
    },
    {
        "code": "USA",
        "gold": 9,
        "silver": 7,
        "bronze": 10
    },
    {
        "code": "EGY",
        "gold": 2,
        "silver": 3,
        "bronze": 12
    }
]

A basic implementation is easy enough:

R.sortBy defaults to ascending order
1
R.sortBy(R.prop('silver'), array);  // [{"code": "EGY", "silver": 3}, {"code": "AUT", "silver": 5}, {"code": "USA", "silver": 7}]

R.sortBy sorts according to a given function, in this case R.prop (where 'silver' could be substituted for any other property).

To ensure the order (ascending vs. descending), we can introduce R.comparator:

R.comparator enforces descending order, but “AUT” and “USA” tie
1
2
const goldComparator = R.comparator((a, b) => R.gt(R.prop('gold', a), R.prop('gold', b)));
R.sort(goldComparator, array);    //  [{"code": "AUT", "gold": 9}, {"code": "USA", "gold": 9}, {"code": "EGY", "silver": 3}]

How can we handle tiebreakers? That is, as in the example abolve, what if two elements in the array have identical gold values and we attempt to sort by gold — which should be sorted first? We can ensure a deterministic result with predictable tiebreaks using comparators and R.either.

R.comparator enforces descending order and second R.comparator passed to R.either breaks ties
1
2
3
4
const goldComparator = R.comparator((a, b) => R.gt(R.prop('gold', a), R.prop('gold', b)));
const silverComparator = R.comparator((a, b) => R.gt(R.prop('silver', a), R.prop('silver', b)));

R.sort(R.either(goldComparator, silverComparator), array);    // [{"code": "USA", "gold": 9, "silver": 7}, {"code": "AUT", "gold": 9, "silver": 5}, {"code": "EGY", "gold": 2, "silver": 3}]

Finally, what if we need more than one tiebreaker? How do we handle objects that have identical gold AND silver values? R.either expects two arguments, so the solution is to create a variadic implementation of R.either, one that will accept an unknown number of arguments, so we can pass tiebreaker comparators for all possible situations:

Addresses all edge cases: sort by gold; if gold ties sort by silver; if silver ties sort by bronze; if bronze ties sort by country code
1
2
3
4
5
6
7
8
const variadicEither = (head, ...tail) => R.reduce(R.either, head, tail);

const goldComparator = R.comparator((a, b) => R.gt(R.prop('gold', a), R.prop('gold', b)));
const silverComparator = R.comparator((a, b) => R.gt(R.prop('silver', a), R.prop('silver', b)));
const bronzeComparator = R.comparator((a, b) => R.gt(R.prop('bronze', a), R.prop('bronze', b)));
const codeComparator = R.comparator((a, b) => R.lt(R.prop('code', a), R.prop('code', b)));    // sorts alphabetically by country code

R.sort(variadicEither([goldComparator, silverComparator, bronzeComparator, codeComparator]), array);

The crux of this solution is variadicEither, a variadic re-implementation of R.either that can accept a variable number of arguments. It uses head (first argument) and ...tail (all remaining arguments) to reduce over all arguments and return a function that addresses all tiebreak possibilities. R.sort expects a comparator function, which R.either and variadicEither both return.

Of course this solution still has a bit of boilerplate (repetition of R.comparator(...)). For a reusable sortByProps implementation that takes an array of props and a list, see this Ramda.js recipe that I recently added.