Thursday, November 26, 2009

Custom-sorting arrays in JavaScript

To sort an array in JavaScript, we use the array.sort() method.

Let's take a look at the following code:

var words   = ["andreas", "john", "grech", "janica", "buhagiar"];
var numbers = [5, 2, 8, 666, 42, 13];

console.log(words.sort());
console.log(numbers.sort());

The results of the above are:

["andreas", "buhagiar", "grech", "janica", "john"]

[13, 2, 42, 5, 666, 8]

As you can see, the strings are sorted correctly, but what happened to the numbers? 13 is obviously greater than 2, so why is it before?

Sorting lexicographically

The problem with the array.sort function is that it sorts lexicographically, meaning it sorts 'alphabetically'; in dictionary order; and thus the sort function just assumes that the elements that need to be sorted are strings. And if they aren't strings, it converts them to strings and then compares them.

Fortunately, sort takes also a single parameter; a function that will be used to sort the array with.

Let's take an example of how we can sort numbers:

var numbers = [5, 2, 8, 666, 42, 13];
var ascNumSort = function (a, b) {
    return a - b;
};
var ns = numbers.sort(ascNumSort);
console.log(ns);

Now we created a function called ascNumSort and passed it to the sort method, and as you can see the numbers are now correctly sorted in ascending order.

What kind of function ?

The function that you pass as a parameter to the sort method needs to abide with the following rules:
  1. It must accept two formal arguments
  2. Return a 0 if the two elements are equal
  3. Return a positive number if the second parameter should come before the first parameter
  4. Return a negative number if the first parameter should come before the second parameter
With these rules, we can now build our custom sorting functions.

Let's say that we have an array of objects representing people and we want to sort these people according to their height, in descending order:

var people = [
    {name: "Andreas", height: 5.4},
    {name: "Janica",  height: 4.3},
    {name: "Cameron", height: 5.8},
    {name: "Richard", height: 5.4},
    {name: "Ryan",    height: 4.5}
];

var sortedPeople = people.sort(function (p1, p2) {
    return p2.height - p1.height;
});

console.log(sortedPeople);
/*Result:
[
 Object name=Cameron height=5.8, 
 Object name=Andreas height=5.4, 
 Object name=Richard height=5.4, 
 Object name=Ryan    height=4.5, 
 Object name=Janica  height=4.3
]
*/

Sorting on multiple keys

By implementing what we did above, but in a more general way, we can come up with a function that will allow us to sort an array of objects, given a key (or multiple keys) [taken from Douglas Crockford's The Good Parts]:

//Function by takes a member name string and an
//optional minor comparison function and returns
//a comparison function that can be used to sort an
//array of objects that contain that member.  The
//minor comparison function is used to break ties
//when the o[name] and p[name] are equal 

var by = function (name, minor) {
    return function (o, p) {
        var a, b;
        if (typeof o === 'object' && typeof p === 'object' && o && p) {
            a = o[name];
            b = p[name];
            if (a === b) {
                return typeof minor === 'function' ? minor(o, p) : o;
            }
            if (typeof a === typeof b) {
                return a < b ? -1 : 1;
            }
            return typeof a < typeof b ? -1 : 1;
        } else {
            throw {
                name: 'Error',
                message: 'Expected an object when sorting by ' + name
            };
        }
    }
};      

Now let's take a look of an example of how we can use the above by function:

var people = [
    {name: "Andreas", height: 5.4},
    {name: "Janica",  height: 4.3},
    {name: "Tony",    height: 5.8},
    {name: "Richard", height: 5.4},
    {name: "Ryan",    height: 4.5},
    {name: "Cameron", height: 5.8}
];

console.log(people.sort(by('height')));
/*Result:
[
 Object name=Janica height=4.3,  Object name=Ryan height=4.5, 
 Object name=Andreas height=5.4, Object name=Richard height=5.4, 
 Object name=Tony height=5.8,    Object name=Cameron height=5.8
]
*/

console.log(people.sort(by('height', by('name'))));
/*Result:
[
 Object name=Janica height=4.3,  Object name=Ryan height=4.5, 
 Object name=Andreas height=5.4, Object name=Richard height=5.4, 
 Object name=Cameron height=5.8, Object name=Tony height=5.8
]
*/