If you have written some JavaScript and manipulate slightly complex data, you have had to write some code like this to sort an array of objects:
const data = [
{ name: "Alice", age: 22 },
{ name: "Bob", age: 32 },
{ name: "Carl", age: 63 },
{ name: "Clara", age: 28 },
...
];
data.sort(function(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
})
// Or, as a one-liner:
data.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
While this is perfectly fine for one-off sorting of shallow objects, it can get a bit more complex and repetitive when having to sort based on nested fields.
Something else you might have tripped on while using the native .sort()
on arrays, is the following behaviour:
const array = [1, 2, 3, 10, 23]
console.log(array.sort())
// [1, 10, 2, 23, 3]
Indeed, by default, the comparison function used by .sort()
treats each element as a string! To make the above example work, you need to pass a custom comparison function such as the following one-liner:
const array = [1, 23, 3, 10, 2]
console.log(array.sort((a, b) => a - b))
// [1, 2, 3, 10, 23]
As sorting is a common operation on arrays, a more scalable and less error-prone strategy would be to define common compare functions. Let's build said compare functions!
First, let's look at the API we would like to end up with:
const array = [1, 23, 3, 10, 2]
array.sort(numerically)
// Should be equivalent to:
array.sort((a, b) => a - b)
array.sort(numerically.desc)
// Should be equivalent to:
array.sort((a, b) => b - a)
// For completeness, we can also expose `numerically.asc`.
To achieve the above API, we can define numerically
as follows:
function numerically (a, b) {
return a - b;
}
As in JavaScript, (almost) everything is an object, we can then add a desc
and an asc
field to the numerically
function as follows:
numerically.desc = function(a, b) {
return b - a;
}
numerically.asc = function(a, b) {
return numerically(a, b); // This works because we sort from lower to higher by default!
}
Now that we have defined compare functions to work on arrays holding primitives values, let's generalise it to arrays of objects:
const data = [
{ name: "Alice", age: 22 },
{ name: "Bob", age: 32 },
{ name: "Carl", age: 63 },
{ name: "Clara", age: 28 },
...
];
data.sort(alphabetically.by("name"))
// Should be equivalent to:
data.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
To achieve that, let's create a small utility function that will help us retrieve the value of an object based on a key path:
function getValueByKey(obj, key) {
return String(key)
.split(".")
.reduce((acc, cur) => acc?.[cur] ?? null, obj);
}
With the code above, we can do deep object look-ups!
With that in hand, let's add the following to our example alphabetically
sort function:
function alphabetically (a, b) { ... }
alphabetically.desc = function(a, b) { ... }
alphabetically.asc = function(a, b) { ...}
alphabetically.by = function(key) {
return function(a, b) {
const aVal = getValueByKey(a, key);
const bVal = getValueByKey(b, key);
return a < b ? -1 : a > b ? 1 : 0;
}
}
Alright, this works great for ascending order sorting, but how could we implement descending order? There are different ways of solving this:
- Pass another argument that can have either
"desc"
or"asc"
values (defaults to"asc"
) - Append a
-
sign in the key (for example:sort(alphabetically.by("-name")
) - Add
.desc()
and.asc()
functions to our new function.by()
Either designs are fine, but to stay consistent with our previous utility function, we will be adding the ordering feature as follow:
data.sort(alphabetically.by("name").desc)
All implemented, it looks like:
function alphabetically (a, b, direction = 1) {
if (a < b) {
return -1 * direction;
}
if (a > b) {
return 1 * direction;
}
return 0;
}
alphabetically.asc = (a, b) => alphabetically(a, b, 1);
alphabetically.desc = (a, b) => alphabetically(a, b, -1);
alphabetically.by = function(key) {
function compareBy(a, b, direction = 1) {
const aVal = getValueByKey(a, key);
const bVal = getValueByKey(b, key);
return aVal < bVal ? -1 * direction : aVal > bVal ? 1 * direction : 0;
}
compareBy.asc = (a, b) => compareBy(a, b, 1);
compareBy.desc = (a, b) => compareBy(a, b, -1);
return compareBy;
}
I found this exercise particularly interesting, and decided to build a library with some of the ideas discussed in this post. You can have a look at it here:
AntonioVdlC / sort
π - Custom compare functions for sorting arrays
sort
Custom compare functions for sorting arrays.
Installation
This package is distributed via npm:
npm install @antoniovdlc/sort
Motivation
Sorting arrays is a common operation in JavaScript, so this library provides some common custom compare functions to have a more declarative way of sorting arrays.
Usage
You can use this library either as an ES module or a CommonJS package:
import { alphabetically, chronologically, numerically } from "@antoniovdlc/sort";
- or -
const { alphabetically, chronologically, numerically } = require("@antoniovdlc/sort");
Examples
All compare functions can be used out of the box for sorting as follows:
import { numerically } from "@antoniovdlc/sort";
const arr = [1, 2, 2, 23, 30, 4];
arr.sort(numerically); // [1, 2, 2, 4, 23, 30]
By default, sorting is doing in an ascending fashion. Allβ¦
Top comments (0)