I've recently needed to sort JavaScript arrays in a couple of different
projects, and in both cases the sorting had to be done on more than one
criteria. Sorting in JavaScript is as simple as calling the
.sort
method
on an Array object, and optionally passing in a function (usually an
anonymous function) to do the comparison.
Comparison functions
Let's say I want to sort an array of training courses according to their titles. I need to supply a comparison function which returns −1 when the two arguments are given in the right order, +1 if they should be swapped around, or 0 if they're equal. Something like this:
var courses = [ { title: "..." }, ... ];
courses.sort(function (a, b) {
if (a.title < b.title)
return -1;
else if (a.title > b.title)
return +1;
else
return 0;
});
That gets a bit clunky when there are multiple such sorting functions
comparing different keys. In Perl that
comparison could be done with the built-in cmp
operator,
which compares two values as strings and return −1, +1, or 0, as appropriate.
For numbers there's a separate operator <=>
(the spaceship operator).
Since JavaScript doesn't allow me to define my own operators, I did
the next best thing by defining a reusable cmp
function:
function cmp (a, b) {
if (a < b)
return -1;
else if (a > b)
return +1;
else
return 0;
}
That will work for comparing numbers or strings, but unfortunately not
for things like Date
objects, because the comparison operators
don't work for them. If you're only sorting numbers, then there's
a trick that makes it much simpler to write that function:
function cmp (a, b) {
return a - b;
}
That won't return exactly the same numbers, but it will be correct
in whether the return value is positive, negative, or zero, and that's
all the sort
method cares about.
So now we could sort a list of courses by one thing, say the title,
just by using an anonymous function to pass the right information to
cmp
:
courses.sort(function (a, b) { return cmp(a.title, b.title); });
Sorting with multiple keys
But what about sorting on more than one key? For example, let's say I want to sort a list of training courses according to their subject area (JavaScript, CSS, etc.), and then further sort them so that the within each subject the courses are ordered by their title. In some languages that could be done with multiple sorts like this:
var courses = [ { subject: "CSS", title: "..." }, ... ];
// This might not work in all browsers:
courses.sort(function (a, b) { return cmp(a.title, b.title); });
courses.sort(function (a, b) { return cmp(a.subject, b.subject); });
That will actually work at least in Firefox (at time of writing), but not necessarily in other browsers. The problem is that it relies on a stable sort, which means that the second sorting operation leaves the courses in their existing order—in this case already sorted by title—when it doesn't need to reorder them relative to each other. So the second sort will rearrange the array to make sure all the subjects are in the right order, but it won't disturb the title ordering for courses which have the same subject.
Unfortunately, JavaScript doesn't guarantee that the .sort
method will be stable, so we can't rely on this. Instead, we need to do
all the sorting in one operation. There's another trick from Perl which
makes this easy: using the ||
operator to select which
comparison to use. For example:
courses.sort(function (a, b) {
return cmp(a.subject, b.subject) ||
cmp(a.title, b.title);
}
The first call to cmp
will compare the subjects. If the
result is that they're different—if cmp
returns −1 or +1—then
JavaScript will consider that to be ‘truthy’, which means the ||
will short-circuit and return that value. On the other hand, if the
subjects are the same then the first cmp
will return zero,
which is ‘falsy’ to JavaScript, so the ||
operator will move
on and use the result of the second comparison.
Reversing sort order
To reverse a sort using cmp
just pass the values to
cmp
in the opposite order. And because the result of a
comparison is numeric, you can also reverse the order by negating the
result of cmp
, if you think that would be clearer. So for
example both of these will sort by price, with the most expensive
products first:
products.sort(function (a, b) { return cmp(b.price, a.price); });
products.sort(function (a, b) { return -cmp(a.price, b.price); });