Sorting is a super handy JavaScript method that can display the values of an array in a certain order. Whether that’s real estate listings by price, burger joints by distance, or best nearby happy hours by rating, sorting arrays of information is a common need.
If you’re already doing this with JavaScript on a project, you are will likely using the built-in array .sort
method, which is in the same family of array methods that includes .filter
, .map
and .reduce
.
Let’s take a look at how to do that!
A quick note about side effects
Before going into the details of how to use .sort
, there is a very important detail that needs to be addressed. While many of the ES5 array methods such as .filter
, .map
, and .reduce
will return a new array and leave the original untouched, .sort
will sort the array in place. If this is unwanted, an ES6 technique to avoid this is using the spread operator to concisely create a new array.
const foo = ['c','b','a'];
const bar = ['x','z','y'];
const fooSorted = foo.sort();
const barSorted = [...bar].sort();
console.log({foo, fooSorted, bar, barSorted});
/*
{
"foo": [ "a", "b", "c" ],
"fooSorted": [ "a", "b", "c" ],
"bar": [ "x", "z", "y" ],
"barSorted": [ "x", "y", "z" ]
}
*/
foo
and fooSorted
both reference the same array, but bar
and barSorted
are now individual arrays.
General overview
The only parameter of the .sort
method is a function. The spec refers to this as the compareFn
— I will refer to it as the “comparison function” for the rest of the post. This comparison function accepts two parameters, which I will refer to as a
and b
. a
and b
are the two elements that we will be comparing. If you do not provide a comparison function, the array will coerce each element into a string and sort according to Unicode points.
If you would like the a
to be ordered first in the array, the comparison function should return a negative integer; for b
, a positive integer. If you would like the two to maintain their current order, return a 0
.
If you don’t understand, don’t worry! Hopefully it will become much more clear with a few examples.
Comparing numbers
One of the simplest callbacks to write is a number comparison.
const numbers = [13,8,2,21,5,1,3,1];
const byValue = (a,b) => a - b;
const sorted = [...numbers].sort(byValue);
console.log(sorted); // [1,1,2,3,5,8,13,21]
If a
is greater than b
, a - b
will return a positive number, so b
will be sorted first.
Comparing strings
When comparing strings, the >
and <
operators will compare values based on each string’s Unicode value. This means that all uppercase letters will be “less” than all lowercase letters, which can lead to unexpected behavior.
JavaScript does have a method to help with comparing strings: the String.prototype.localeCompare
method. This method accepts a comparison string, a locale, and an options object. The options object accepts a few properties (all of which you can view here), but I find that the most useful is “sensitivity.” This will affect how comparisons work between letter variations such as case and accent.
const strings = ['Über', 'alpha', 'Zeal', 'über', 'uber', 'Uber', 'Alpha', 'zeal'];
const sortBySensitivity = sensitivity => (a, b) => a.localeCompare(
b,
undefined, // locale string -- undefined means to use browser default
{ sensitivity }
);
const byAccent = sortBySensitivity('accent');
const byBase = sortBySensitivity('base');
const byCase = sortBySensitivity('case');
const byVariant = sortBySensitivity('variant'); // default
const accentSorted = [...strings].sort(byAccent);
const baseSorted = [...strings].sort(byBase);
const caseSorted = [...strings].sort(byCase);
const variantSorted = [...strings].sort(byVariant);
console.log({accentSorted, baseSorted, caseSorted, variantSorted});
/*
{
"accentSorted": [ "alpha", "Alpha", "uber", "Uber", "Über", "über", "Zeal", "zeal" ],
"baseSorted": [ "alpha", "Alpha", "Über", "über", "uber", "Uber", "Zeal", "zeal" ],
"caseSorted": [ "alpha", "Alpha", "über", "uber", "Über", "Uber", "zeal", "Zeal" ],
"variantSorted": [ "alpha", "Alpha", "uber", "Uber", "über", "Über", "zeal", "Zeal" ]
}
*/
Running functions before comparing values
You may want to run a comparison function on a value that is derived from each array’s element. First, let’s write a comparison function factory that will “map” over the element before calling the comparison function.
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
One use case for this is sorting based on the attribute of an object.
const purchases = [
{ name: 'Popcorn', price: 5.75 },
{ name: 'Movie Ticket', price: 12 },
{ name: 'Soda', price: 3.75 },
{ name: 'Candy', price: 5 },
];
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const byValue = (a,b) => a - b;
const toPrice = e => e.price;
const byPrice = sortByMapped(toPrice,byValue);
console.log([...purchases].sort(byPrice));
/*
[
{ name: "Soda", price: 3.75 },
{ name: "Candy", price: 5 },
{ name: "Popcorn", price: 5.75 },
{ name: "Movie Ticket", price: 12 }
]
*/
Another case might be to compare an array of dates.
const dates = ['2018-12-10', '1991-02-10', '2015-10-07', '1990-01-11'];
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const toDate = e => new Date(e).getTime();
const byValue = (a,b) => a - b;
const byDate = sortByMapped(toDate,byValue);
console.log([...dates].sort(byDate));
// ["1990-01-11", "1991-02-10", "2015-10-07", "2018-12-10"]
Reversing a sort
There are some cases where you may want to reverse the outcome of a comparison function. This is subtly different than doing a sort and then reversing the result in the way ties are handled: if you reverse the outcome, ties will also be reversed in order.
To write a higher order function that accepts a comparison function and returns a new one, you will need to flip the sign of the comparison’s return value.
const flipComparison = fn => (a,b) => -fn(a,b);
const byAlpha = (a,b) => a.localeCompare(b, null, { sensitivity: 'base' });
const byReverseAlpha = flipComparison(byAlpha);
console.log(['A', 'B', 'C'].sort(byReverseAlpha)); // ['C','B','A']
Running a tiebreaker sort
There are times when you may want to have a “tie-breaker” sort — that is, another comparison function that is used in the case of a tie.
By using [].reduce
, you can flatten an array of comparison functions into a single one.
const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const flipComparison = fn => (a,b) => -fn(a,b);
const byValue = (a,b) => a - b;
const byPrice = sortByMapped(e => e.price)(byValue);
const byRating = sortByMapped(e => e.rating)(flipComparison(byValue));
const sortByFlattened = fns => (a,b) =>
fns.reduce((acc, fn) => acc || fn(a,b), 0);
const byPriceRating = sortByFlattened([byPrice,byRating]);
const restaurants = [
{ name: "Foo's Burger Stand", price: 1, rating: 3 },
{ name: "The Tapas Bar", price: 3, rating: 4 },
{ name: "Baz Pizza", price: 3, rating: 2 },
{ name: "Amazing Deal", price: 1, rating: 5 },
{ name: "Overpriced", price: 5, rating: 1 },
];
console.log(restaurants.sort(byPriceRating));
/*
{name: "Amazing Deal", price: 1, rating: 5}
{name: "Foo's Burger Stand", price: 1, rating: 3}
{name: "The Tapas Bar", price: 3, rating: 4}
{name: "Baz Pizza", price: 3, rating: 2}
{name: "Overpriced", price: 5, rating: 1}
*/
Writing a random sort
You might want to sort an array “randomly.” One technique that I have seen is to use the following function as the comparison function.
const byRandom = () => Math.random() - .5;
Since Math.random()
returns a “random” number between 0
and 1
, the byRandom
function should return a positive number half of the time and a negative number the other half. This seems like it would be a good solution, but unfortunately, since the comparison function is not “consistent” — meaning it may not return the same value when called multiple times with the same values — it may result in some unexpected results.
For example, let’s take an array of numbers between 0 and 4. If this byRandom
function was truly random, it would be expected that the new index of each number would be spread out equally over enough iterations. The original 0 value would be just as likely to be in index 4 as index 0 in the new array. However, in practice, this function will bias each number to its original position.
See the Pen
Array.sort() Random 👎 by Adam Giese (@AdamGiese)
on CodePen.
The “diagonal” from the top-left will statistically have the greatest value. In an ideal and truly random sort, each table cell would hover around 20%.
The fix for this is to find a way to ensure that the comparison function remains consistent. One way to do this is to map the random value to each array element before the comparison, then map it away after.
const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const values = [0,1,2,3,4,5,6,7,8,9];
const withRandom = (e) => ({ random: Math.random(), original: e });
const toOriginal = ({original}) => original;
const toRandom = ({random}) => random;
const byValue = (a,b) => a - b;
const byRandom = sortByMapped(toRandom)(byValue);
const shuffleArray = array => array
.map(withRandom)
.sort(byRandom)
.map(toOriginal);
This ensures that each element has a single random value that is only calculated once per element rather than once per comparison. This removes the sorting bias towards the original position.
See the Pen
Array.sort() Random 👍 by Adam Giese (@AdamGiese)
on CodePen.
Wait, why is base sorted the most logical? That one breaks upper/lower case continuity. IMO both case and variant sorted seem more logical.
To be fair, the author did say “To me”, with which I agree, but it all depends on context, doesn’t it?
If I’m looking through car model names, for example, I’d rather scan a base-ordered list to find the information I’m looking for. But in another situation, case-ordered could make a lot more sense.
Exactly my thoughts, nevertheless it is just an opinion.
This is an informative article. I wrote the compose-sort npm library to help with the challenge of complexity in “tie-breaker” behavior. I just wanted to point it out in case it is helpful to anybody who would like to avoid writing the function you have listed above.
Nice article. Regarding “Comparing strings” as far as I know when you have a large array of strings that you want to sort it is better (performance wise) to use
Intl.Collator
and itscompare
method instead.As for the “random sort” it seems too overwhelming and a very much sounds like a shuffle using a random index rather than a random sort. Am I missing something?
I must say I’m a big fan of your arrow game. It’s not very intuitive at the beginning to read – but once I got used to it it seems very clear.
” ‘ü’, ‘u’, ‘Ü’, and ‘U’ are equivalent”
As someone whose native language is one where the letter “ü” exists, I find this statement kinda ignorant, and grammatically incorrect.
Both u and ü has it’s own place in the alphabet as separate letters, and their place defines the order too.
In Hungarian (and German) ü comes after u, and the fictional word “üa” would definitely be after “uz”.
I’m sorry — I meant that they are equivalent in the base sort, not equivalent in language, but I realize this was not very clear in how I wrote it.
I removed the sentence altogether, as it was not necessary for the article.