To understand how .sort()
works, let’s have a look at a few examples.
['George', 'Janet', 'Emma', 'Eve', 'Charles', 'Tracey'].sort();
// = ['Charles', 'Emma', 'Eve', 'George', 'Janet', 'Tracey']
Sorting simple strings works fine and the resulting array is as expected.
['Cäcilia', 'Janet', 'Èmma', 'Eve', 'Charles', 'Jörg', 'Julian'].sort();
// = ['Charles', 'Cäcilia', 'Eve', 'Janet', 'Julian', 'Jörg', 'Èmma']
Sorting real-world strings with special characters like umlauts or characters with accents mixes up the default .sort()
implementation. Usually, »Èmma« would be coming before »Eve« and »Jörg« would be between »Janet« and »Julian«.
[1, 25, 100, 3000].sort();
// = [1, 100, 25, 3000]
The same »misbehaviour« is true for sorting numbers. Usually, »25« would be coming between »1« and »100« but the sorting order seems to be completely mixed up.
Why?
The reason for this bumpy sorting is that .sort()
by default uses the UTF codepoints of values to compare values with each other.
Let’s convert the strings to such codepoints to see, what’s going on.
const encoder = new TextEncoder();
encoder.encode('Cäcilia'); // [67, 195, 164, 99, 105, 108, 105, 97]
encoder.encode('Janet'); // [74, 97, 110, 101, 116
encoder.encode('Èmma'); // [195, 136, 109, 109, 97]
encoder.encode('Eve'); // [69, 118, 101]
encoder.encode('Charles'); // [67, 104, 97, 114, 108, 101, 115]
encoder.encode('Jörg'); // [74, 195, 182, 114, 103]
encoder.encode('Julian'); // [74, 117, 108, 105, 97, 110]
For »Èmma« and »Eve«, 195
(= first character of »Èmma«) is greater than 69
(= first character of »Eve«), so according to this logic, »Èmma« comes after »Eve«. As 195
is the highest value for the first character of all names in that array in general, »Èmma« is sorted to the very end of the list.
For »Jörg«, »Janet«, and »Julian«, 195
(= second character of »Jörg«) is greater than 97
(= second character of »Janet«) and 117
(= second character of »Julian«), so »Jörg« is sorted after »Julian«.
Let’s do the same analysis for the number example as well.
const encoder = new TextEncoder();
encoder.encode(1); // [49]
encoder.encode(25); // [50, 53]
encoder.encode(100); // [49, 48, 48]
encoder.encode(3000); // [51, 48, 48, 48]
50
(= first character of »25«) is greater than 49
(= first character of both »1« and »100«), so »25« is sorted after »100« erroneously.
How to fix
To sort numbers, a so-called »comparator function« can be specified. Comparator functions are actually kind of easy and have 3 expected kinds of return values. This makes sense as the comparator function takes 2 values and compares them with each other – naturally, there’s 3 potential outcomes of such a comparison.
- A is smaller than B → negative number (e.g.
-1
or-123
) - A is greater than B → positive number (e.g.
1
or123
) - A is equal to B → zero (i.e.
0
)
How the comparator function comes to one of these 3 possible return values is the job of the custom implementation. Let’s implement a comparator function to sort numbers properly.
[1, 20, 100, 3000].sort((a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
// = [1, 20, 100, 3000]
Now the sorting order is looking good. For numbers, this can even be shortened to a simple mathematical subtraction.
[1, 20, 100, 3000].sort((a, b) => a - b);
// = [1, 20, 100, 3000]
This works because the W3C specification only mandates negative and positive values and subtracting a greater number from a smaller number leads to a negative remainder and thus negative return value, subtracting a smaller number from a greater number leads to a positive remainder and thus positive return value, and subtracting identical numbers results in 0.
To sort strings with characters from languages other than English, the localeCompare
method can be used to compare these strings so that they are displayed in the correct/expected order.
['Cäcilia', 'Janet', 'Èmma', 'Eve', 'Charles', 'Jörg', 'Julian'].sort((a, b) => {
return a.localeCompare(b);
});
// = ['Cäcilia', 'Charles', 'Èmma', 'Eve', 'Janet', 'Jörg', 'Julian']
It’s important to keep in mind that localeCompare
actually is nothing but a pre-defined comparator function that also simply returns a number indicating whether a string comes before, or after, or is the same as the given string in sort order.
If you want to be able to sort both strings and numbers, the localeCompare
can be configured to do so as well.
[1, 20, '10', 100, 'Eve', 3000, 'Jörg', 'Èmma'].sort((a, b) => {
return String(a).localeCompare(String(b), undefined, {
numeric: true,
sensitivity: 'base',
});
});
// = [1, '10', 20, 100, 3000, 'Èmma', 'Eve', 'Jörg']
(As localeCompare
is a String
method, we need to convert the numbers to strings first – that’s what String(a)
and String(b)
in the example above is about.)