JS compare function
The problem
The JS Array sort method is notorious for taking verbose comparison functions.
By adding a short compare
function and using the ||
operator with unary
negation (-
), we can clearly express ordering without resorting to external
libraries like Lodash.
The compare function
I went against my usual coding style (always use braces) in order to make this
short function more memorable. I also spared y’all the horror of nested ?:
conditional operators :)
function compare(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
Note: This function won’t behave well with objects and arrays. A stricter function would throw an error or use a more complex algorithm to compare data structures.
What now?
Two key facts allow this function to shine:
-
JS defines
a || b
as evaluating to eithera
orb
, not justtrue
orfalse
-
Since
-compare(a, b)
is equivalent tocompare(b, a)
, you can prefix each descending comparison with a special character to make it more obvious than flipping the arguments
Seeing as 0 || x
evaluates to x
, and compare(a, b) === 0
means a === b
,
the ||
operator lets us chain these “failing” comparisons elegantly to define
complex ordering:
const users = [
{ name: "flynn", age: 48 },
{ name: "bridget", age: 36 },
{ name: "flynn", age: 40 },
{ name: "bridget", age: 34 },
];
// In SQL this would be `ORDER BY name DESC, age ASC`
[...users].sort((a, b) => -compare(a.name, b.name) || compare(a.age, b.age));
// =>
[
{ name: "flynn", age: 40 },
{ name: "flynn", age: 48 },
{ name: "bridget", age: 34 },
{ name: "bridget", age: 36 },
];
I think it’s pretty clear that compare
is a massive improvement over writing a
comparison function from scratch:
[...users].sort((a, b) => {
// DESCENDING by name
if (b.name < a.name) return -1;
if (b.name > a.name) return 1;
// ASCENDING by age
if (a.age < b.age) return -1;
if (a.age > b.age) return 1;
return 0;
});
// =>
[
{ name: "flynn", age: 40 },
{ name: "flynn", age: 48 },
{ name: "bridget", age: 34 },
{ name: "bridget", age: 36 },
];
A real world example
And for a realistic example from my website Pokémon Type Calculator:
array.sort((a, b) => {
return (
// Put the largest cash bounty at the top
-compare(languageBounty[a], languageBounty[b]) ||
// Then sort by percentage completion
compare(languageCompletions[a] || 0, languageCompletions[b] || 0) ||
// Then sort by language name
compare(a, b)
);
});
It’s not quite as readable as Lodash’s orderBy, but I like how the ascending/descending information is colocated with the sort properties.
_.orderBy(
array,
[
// Put the largest cash bounty at the top
(item) => languageBounty[item],
// Then sort by percentage completion
(item) => languageCompletions[item] || 0,
// Then sort by language name
(item) => item,
],
["desc", "asc", "asc"]
);
Other thoughts
This pattern can be also applied to functions that return undefined
or null
in the failure case by chaining the ??
operator:
function parseInteger(value) {
// Convert numbers to strings before parsing
if (typeof value === "number") {
return parseInteger(String(value));
}
// Ignore non-string values
if (typeof value !== "string") {
return undefined;
}
value = value.trim();
// `Number("") === 0`, but we want `undefined`
if (value === "") {
return undefined;
}
const n = Number(value);
// Check for NaN, Infinity, and -Infinity
if (!Number.isFinite(n)) {
return undefined;
}
// Remove the part after the decimal point
return Math.trunc(n);
}
parseInteger(NaN) ??
parseInteger(Infinity) ??
parseInteger(" ") ??
parseInteger("x2") ??
parseInteger(" 42.1");
// => 42
You can even return entire objects, taking the result from the first call that didn’t fail. Debugging can be tougher compared with using exceptions or failure objects, though.