I would personally define some general purpose sort helpers that are useful in all sorts of sort()
scenarios.
Let's first look at the answer without knowing what the helpers do:
import { fallThrough, by, asc, desc, front, back } from './lib/sort-helpers';
const items = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];
items.sort(fallThrough(
back(item => typeof item === "string"),
asc,
));
console.log(items);
function solution({ fallThrough, by, asc, desc, front, back, itself }) {
const items = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];
items.sort(fallThrough(
back(item => typeof item === "string"),
asc,
));
console.log(items);
}
const SortHelpers = (function () {
const fallThrough = (...comparators) => (a, b) => (
comparators.reduce((diff, comparator) => diff || comparator(a, b), 0)
);
const by = (fn, comparator = asc) => (a, b) => comparator(fn(a), fn(b));
const asc = (a, b) => -(a < b) || +(a > b);
const desc = (a, b) => asc(b, a);
const bool = (fn) => (item) => !!fn(item);
const front = (fn) => by(bool(fn), desc);
const back = (fn) => by(bool(fn), asc);
return { fallThrough, by, asc, desc, front, back };
})();
solution(SortHelpers);
As you can see this solution looks relatively simple. Note that the second sorting criteria asc
will apply to both numbers and strings. So if the strings are non-empty, they will also be sorted in ascending order.
Helpers
Now, let's explain the general purpose helpers (see collapsed snippet).
I'll use the term comparator quite a bit. A comparator is a function that accepts 2 arguments, then returns a number to tell sort()
how the 2 arguments should be sorted.
A negative value indicates that the first argument should be placed before the second. A positive value indicates that the first argument should be placed after the second argument. Zero indicates that both arguments can be considered equal.
A basic example of an comparator can be seen in the explanation for asc
.
fallThrough
fallThrough
allows us to pass multiple comparator functions. If the first comparator says that the values are equal, then the next comparator is invoked. If the second comparator also says the values are equal, we'll move on the the third.
This goes on until one of the comparators says which value comes first. If there are no more comparators available, the values are considered equal.
Here is an example without the use of the other helpers:
const people = [
{ firstName: "John", lastName: "Doe" },
{ firstName: "Jane", lastName: "Doe" },
{ firstName: "Dave", lastName: "Dex" },
];
people.sort(fallThrough(
(personA, personB) => personA.lastName.localeCompare(personA.lastName),
(personA, personB) => personB.firstName.localeCompare(personB.firstName),
));
The above will first try to sort based on last name. If those are equal we sort by first name.
by
by
is a simple helper that accepts a mapping function and a comparator. It applies the mapping function to both comparator arguments. The purpose is mainly to reduce code repetition.
When looking in the example above we can see that we access personA.lastName
and personB.lastName
, which are the same mappings for both compared items. By using by
we can simplify this example:
const localeCompare = (a, b) => a.localeCompare(b);
people.sort(fallThrough(
by(person => person.LastName, localeCompare),
by(person => person.firstName, localeCompare),
));
Note that you don't have to use fallThrough
. If have just a single sorting criteria then the following will suffice:
people.sort(by(person => person.LastName, localeCompare));
asc
& desc
asc
and desc
are comparators based on <
(less than) and >
(greater then).
The definition of asc
is:
const asc = (a, b) => -(a < b) || +(a > b);
Which might be hard to read if you're still new to JavaScript. It could also be written as:
function asc(a, b) {
if (a < b) return -1; // a before b
if (a > b) return 1; // a after b
return 0; // equal
}
The more cryptic version uses -(a < b)
, which evaluates to -(true)
, which in turn evaluates to -1
. Since -1
is a truthy value ||
short circuits and -1
is returned. If this first expression is not true, then it evaluates to -(false)
, which will produce -0
. Since -0
is a falsy value we'll move on to the second operand of ||
. Here we'll do the same if a > b
is true then +(true)
will result in 1
for the return value. If a > b
is false (aka the values are equal) then +(false)
will return 0
.
Note that when comparing strings with <
and >
they are compared based on code point value of the characters.
desc
is the same as asc
, but produces the reverse order by swapping the comparator arguments.
front
& back
front
and back
are there to simplify boolean logic in sorting.
by(item => typeof item === "string", asc)
Is kind of hard to read. Are the strings placed in the front or in the back? Let's think for a second. typeof item === "string"
produces a true
or false
value. true
will coerce into 1
, false
into 0
. We are sorting in ascending order, so we are sorting from small to high. 1
is greater than 0
, meaning that strings are placed in the back.
You don't want to do this kind of thinking when reading code. Therefore I've provided the front
and back
helpers. These expect you to pass a test and make the above a lot more readable.
back(item => typeof item === "string")
In this version you can instantly see that strings are placed at the back of the array.