Array.sort takes a comparator. One field is straightforward:
data.sort(function(a, b) {
return a.SCORE - b.SCORE;
});
Multiple fields gets ugly fast. Sort by score descending, then time ascending, then age ascending – you end up chaining comparisons:
data.sort(function(a, b) {
return (b.SCORE - a.SCORE) || (a.TIME - b.TIME) || (a.AGE - b.AGE);
});
This works for numbers but breaks down with strings, custom transforms, or when you want to configure the sort dynamically. I wanted something reusable.
The pattern below is adapted from a Stack Overflow answer I kept coming back to – writing it up here for my own reference.
The predicate function
function predicate() {
var fields = [];
var n_fields = arguments.length;
var field, name, cmp;
var default_cmp = function(a, b) {
if (a === b) return 0;
return a < b ? -1 : 1;
};
var getCmpFunc = function(primer, reverse) {
var dfc = default_cmp;
var cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
} else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({ name: name, cmp: cmp });
}
return function(A, B) {
var result;
for (var i = 0; i < n_fields; i++) {
result = fields[i].cmp(A[fields[i].name], B[fields[i].name]);
if (result !== 0) break;
}
return result;
};
}
Pass it field names as strings for ascending sort, or objects with name, reverse, and primer for more control. It returns a comparator function that Array.sort can use directly.
Usage
data.sort(predicate(
{ name: 'SCORE', reverse: true },
'TIME',
'AGE'
));
Score descending, then time ascending, then age ascending. The output:
jane 4000 35 16
yuri 3000 34 19
anita 2500 32 17
joe 2500 33 18
sally 2000 30 16
mark 2000 30 18
bob 2000 32 16
amy 1500 29 19
mary 1500 31 19
tim 1000 30 17
Ties on score fall through to time. Ties on time fall through to age. The comparator short-circuits on the first field that differs.
The primer option handles type coercion – pass parseInt to sort string numbers correctly, or a custom function to normalize values before comparison.
data.sort(predicate(
{ name: 'PRICE', primer: parseInt, reverse: true },
'NAME'
));
The function preprocesses the field specifications once and returns a closure. The sort comparisons don’t rebuild anything per call – just iterate the fields and break on the first difference.