Sorting arrays in JavaScript with some Perl tricks

Published

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); });