Sorting javascript arrays on multiple properties
tl;dr
/*
Find out what each compare function on a single property would look like, e.g: sort by name only. Then chain all the compare functions together with ||
*/
persons.sort((a, b) => {
const compareHasPayed = (a,b) => (!a.hasPayed?1:0) - (!b.hasPayed?1:0);
const compareName = (a,b) => a.name<b.name?-1: b.name<a.name?1:0;
const compareAge = (a,b) => Math.sign(a.age - b.age);
return compareHasPayed(a,b) || compareName(a,b) || compareAge(a,b);
});
/*
Find out what a compare function on a single property would look like, e.g: how would you sort by name.
Then chain all the compare functions together by ||
*/
persons.sort(function(a, b) {
return compareHasPayed(a,b) || compareName(a,b) || compareAge(a,b);
});
function compareHasPayed(a,b) {
function bool2Int(val) { return val?1:0; }
return bool2Int(a) - bool2Int(b);
}
function compareName(a,b) {
if(a.name<b.name) return -1;
else if(b.name<a.name) return 1;
else return 0;
}
function compareAge(a,b) {
return Math.sign(a.age - b.age);
}
Situation
You have an array of persons:
let persons = [
{age: 47, name: "Kimmy", hasPayed: false},
{age: 23, name: "Kimmy", hasPayed: false},
{age: 16, name: "Brownie", hasPayed: true},
{age: 11, name: "Johnny", hasPayed: true}
];
The job is to sort the array so that persons who have not payed comes first. Ties are sorted with name (alphabetically), and people with the same name are sorted with age (ascending).
Array.sort compare function
In order to sort an array of objects, we need to supply a compare function. The job of this function is to tell, for two given objects in the array, in which order they should appear. The compare function will return -1, 0 or 1 to indicate this.
We want | We return |
---|---|
object1 should be placed before object2 | -1 |
object1 should be placed after object2 | 1 |
With regards to the sorting, they are equal. | 0 |
A simple implementation which will sort persons with lower age first:
function compareAge(obj1, obj2) {
if(obj1.age < obj2.age)
return -1;
else if(obj1.age > obj2.age)
return 1;
return 0;
}
persons.sort(compareAge);
Sorting numbers and math.sign
There is a nicer way to create a comparison function for numbers:
function compareAge(obj1, obj2) {
return obj1.age - obj2.age;
}
The difference in age will return either a negative number, zero, or a positive number. This is actually OK! The negative number will be parsed as -1, the positive number will be parsed as 1. However, certain environments (hello Google App Scripts) absolutly require -1, 0 or 1. So just to be safe we can apply the sign function:
function compareAge(obj1, obj2) {
return Math.sign(obj1.age - obj2.age);
}
The || operator
In most programming languages, the || operator is reserved for boolean operations. In JavaScript, it is much more powerful.
|| starts from the left and returns the first non-falsy value it encounters.
If no values are non-falsy, the rightmost value is returned. Some examples will hopefully make this clear:
let a = false || 0 || 47 || 3
// a = 47
let b = 0 || -1 || 1
// b = -1
let c = getErrorMessage() || ""
// If there is an error message, c is the error message. Otherwise c is "".
More info on falsy values.
Composing compare-functions
Since a compare function will return 0 (a falsy value) only when the order doesn't matter, we can chain compare functions with ||. This allows us to simply think about sorting one property at a time.
persons.sort(function(obj1, obj2) {
// First sort by age.
// If comparison by age returns 0 (they are the same age):
// then compare by name.
return compareAge(obj1,obj2) || compareName(obj1,obj2);
});
function compareAge(obj1, obj2) { return Math.sign(obj1.age - obj2.age); }
function compareName(obj1,obj2) {
if(obj1.name<obj2.name) return -1;
else if(obj1.name<obj2.name) return 1;
else return 0;
}
See the full solution at the top of the page.
This turned out to be a quite long article. I hope you discovered something worthwile. Take care! :)