DEV Community

Cover image for Forever Functional: Sorting recipes for JavaScript
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Edited on • Originally published at blog.openreplay.com

Forever Functional: Sorting recipes for JavaScript

by Federico Kereki

The .sort(...) method provides an easy way to sort an array in a declarative way by just specifying how any two elements should be ordered with regard to each other. While sorting an array of strings is really simple, you may find some problems trying to sort other kinds of data. In this article, we'll see several "recipes" that will enable you to achieve all types of sorting procedures with minimal coding!

Let's review basic concepts. To sort some array, you write someArray.sort(comparisonFn); comparisonFn is a function that receives two elements, a and b, and returns a negative value if a should precede b, a positive value if a should follow b, and zero if a and b are equal. To accomplish more difficult sorting tasks, the key is to write the proper comparison function, and this is what we'll see below.

Sorting strings

Using .sort(...) to work with an array of strings is simple because you don't have to do anything at all! If no function is supplied, sorting is done by comparing the string representations of the array values.

const zodiac = [
  "Aries",
  "Taurus",
  "Gemini",
  "Cancer",
  "Leo",
  "Virgo",
  "Libra",
  "Scorpio",
  "Sagittarius",
  "Capricorn",
  "Aquarius",
  "Pisces"
];

zodiac.sort();
/*
[
 "Aquarius", "Aries", "Cancer", "Capricorn", 
 "Gemini", "Leo", "Libra", "Pisces", 
 "Sagittarius", "Scorpio", "Taurus", "Virgo"
]
*/
Enter fullscreen mode Exit fullscreen mode

Just for completeness, we could have achieved the same result by specifying a function that properly compares strings. Note that we don't have to specifically return -1; any negative value will do. The same applies when returning 1 because any positive value will work.

const compareStrings = (a, b) => {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
};

zodiac.sort(compareStrings); // same result as above
Enter fullscreen mode Exit fullscreen mode

We should remark that we are working in "pointfree style", as we described in an earlier article.

This sorting was easy. What about descending order?

Sorting strings in reverse order

We'll have to reverse the standard comparison to sort an array into descending order. Let's start with three simple solutions:

const reverseCompareStrings1 = (a, b) => {
  if (a < b) return 1;
  if (a > b) return -1;
  return 0;
};

const reverseCompareStrings2 = (a, b) => compareStrings(b, a); // Tricky!

const reverseCompareStrings3 = (a, b) => -compareStrings(a, b);
Enter fullscreen mode Exit fullscreen mode

Now, we get the array sorted in descending order by using any of our reverse comparison functions.

zodiac.sort(reverseCompareStrings1); // or any of the other two instead
/*
[
 "Virgo", "Taurus", "Scorpio", "Sagittarius", 
 "Pisces", "Libra", "Leo", "Gemini", 
 "Capricorn", "Cancer", "Aries", "Aquarius"
]
*/
Enter fullscreen mode Exit fullscreen mode

The first function is straightforward, the second is a bit tricky, but the third one is the most interesting: it points us to another functional-programming-stylish solution. For any sort we may want to implement, the key to changing from ascending to descending is to reverse the comparison sign. We can handle this with a higher order function, as seen in a previous article of this series. So, we can do the following:

const reverse = (fn) => (...args) => -fn(...args);

zodiac.sort(reverse(compareStrings)); // same result as above
Enter fullscreen mode Exit fullscreen mode

From now on, by using our reverse(...) function, we can turn any ascending sort into a descending sort very simply!

Sorting strings disregarding case

Let's consider a different situation -- what about sorting strings without considering differences in case? The solution is to compare strings but putting them in uppercase first.

const letItBe = ["When", "I", "find", "MYSELF", "in", "tiMes", "OF", "TRoubLE"];

letItBe.sort(compareStrings); // uppercase first, lowercase last
/*
["I", "MYSELF", "OF", "TRoubLE", "When", "find", "in", "tiMes"]
*/

const compareWithoutCase = (a, b) =>
  compareStrings(a.toUpperCase(), b.toUpperCase());

letItBe.sort(compareWithoutCase); // case ignored
/*
["find", "I", "in", "MYSELF", "OF", "tiMes", "TRoubLE", "When"]
*/
Enter fullscreen mode Exit fullscreen mode

This works -- but we will see an even more general solution in the next section.

Sorting strings with foreign characters

What happens if you attempt to sort foreign language words? I'll take an example from my Mastering JavaScript Functional Programming book. Suppose you wanted to sort some Spanish words ("palabras") - what would happen?

const palabras = ["ñandú", "oasis", "mano", "natural", "mítico", "musical"];

palabras.sort(); // wrong result!
/*
"mano", "musical", "mítico", "natural", "oasis", "ñandú"
*/
Enter fullscreen mode Exit fullscreen mode

In Spanish, "Ñ" comes between "N" and "O", so "ñandú" is misplaced. Similarly, "mítico" should precede "musical" (because "i" precedes "u") but here it doesn't. The solution to this is the .localeCompare() method, that recognizes different languages. We won't go into all details here, but the Spanish language sort problem can be easily fixed:

const spanishCompare = (a, b) => a.localeCompare(b, "es");

palabras.sort(spanishCompare); // right result
/*
"mano", "mítico", "musical", "natural", "ñandú", "oasis"
*/
Enter fullscreen mode Exit fullscreen mode

I added that it should use the Spanish ("es") option, so comparisons are now made according to rules for that language. The .localeCompare() method also accepts more options, so you could specify whether upper- and lowercase are ordered separately or together, if accents and other diacritic marks should be considered or ignored, etc.

Sorting numbers and dates

Sorting numbers and dates is not complex, but there are several things you must take into account.

Sorting numbers

First, I want to point out that the "reasonable" way of sorting numbers just won't work.

const numbers = [88, 1111, 444, 2];

numbers.sort(); // wrong result!
/*
[1111, 2, 444, 88]
 */
Enter fullscreen mode Exit fullscreen mode

Why is this result? The .sort(...) method, as we said earlier, by default sorts by comparing strings. So, when it compares 88 with 1111, it's actually comparing "88" and "1111" -- and the second string precedes the first!

Specifying a comparison function is mandatory for sorting anything other than strings in ascending order. For numbers this functions is easy to write because if we have numbers a and b, then a-b is negative if a<b, positive if a>b, and zero if a===b, precisely what we need!

const compareNumbers = (a, b) => a - b;

numbers.sort(compareNumbers); // right sort!
/*
[2, 88, 444, 1111]
 */
Enter fullscreen mode Exit fullscreen mode

For a descending sort, we just use our reverse(...) function.

numbers.sort(reverse(compareNumbers));
/*
[1111, 444, 88, 2]
 */
Enter fullscreen mode Exit fullscreen mode

Sorting dates

What about sorting dates? The standard way (meaning, not providing a sort function) won't work because of the conversion to strings fiasco. For an example, I picked several dates related to World War II, but the standard sort fails. (If the dates seem weird to you, remember that the month numbers start at 0 for January, so the first date is June 6th, 1944, the Normandy landings in Operation Overlord.) When sorting, comparisons start with the day's name, so the sorted array begins with a date from 1943 and ends with a date from 1942; bad!

const dates = [
  new Date(1944, 5, 6),  // Op. Overlord
  new Date(1940, 7, 10), // Battle of Britain
  new Date(1941, 11, 7), // Pearl Harbor
  new Date(1942, 5, 4),  // Battle of Midway
  new Date(1942, 7, 26), // Battle of Stalingrad
  new Date(1942, 10, 8), // Op. Torch
  new Date(1943, 6, 5),  // Battle of Kursk
  new Date(1941, 5, 22), // Op. Barbarossa
  new Date(1944, 11, 16) // Battle of the Bulge
];

dates.sort();
/*
[
Mon Jul 05 1943,
Sat Aug 10 1940,
Sat Dec 16 1944,
Sun Dec 07 1941,
Sun Jun 22 1941,
Sun Nov 08 1942,
Thu Jun 04 1942,
Tue Jun 06 1944,
Wed Aug 26 1942
]
*/
Enter fullscreen mode Exit fullscreen mode

The simplest way to fix this is to convert dates to numbers with the .getTime() function. This function transforms a date into a number (the number of milliseconds since January 1st, 1970, if you want to know) but what matters is that you can directly compare those numbers. We can do that by using our previous compareNumbers(...) function, or directly by subtracting.

const compareDates1 = (a, b) => compareNumbers(a.getTime(), b.getTime());

const compareDates2 = (a, b) => a.getTime() - b.getTime();

dates.sort(compareDates1); // or use compareDates2 instead
/*
[
Sat Aug 10 1940,
Sun Jun 22 1941,
Sun Dec 07 1941,
Thu Jun 04 1942,
Wed Aug 26 1942,
Sun Nov 08 1942,
Mon Jul 05 1943,
Tue Jun 06 1944,
Sat Dec 16 1944
]
*/
Enter fullscreen mode Exit fullscreen mode

The dates were ordered correctly; everything is OK.

Sorting booleans

Can you sort booleans? Yes, of course, though it's not very interesting given that you only have two values to work with! In fact, sorting booleans works "out of the box" because when JavaScript transforms booleans into strings, you get "false" and "true", and "false" < "true", so everything turns out well!

const flags = [true, false, true, false, true, false, false];
flags.sort();
/*
[false, false, false, false, true, true, true]
 */
Enter fullscreen mode Exit fullscreen mode

We can also take advantage of type conversions because false is converted to zero, and true is converted to one. Writing +a converts a boolean value a into a number, so we can do as follows.

const compareBooleans = (a, b) => compareNumbers(+a, +b);

flags.sort(compareBooleans); // same result as above
Enter fullscreen mode Exit fullscreen mode

We are now done sorting simple arrays; let's turn to sorting arrays made of objects with many attributes, and more complex sort requirements.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Sorting objects

Sorting an array with single values is simple, and we've seen examples for all cases. Having to sort an array of objects, each with several attributes, is more common. Fortunately, we can make do with the functions we already wrote, plus some extra work.

We can sort a list of objects by one or more attributes; let's just do the second, the more general case that includes the first as well. Let's imagine we have objects with a group (numeric), subgroup (string), total (numeric), and extra other data.

const data = [
  { group: 2, subgroup: "B", total: 111, other: "alpha" },
  { group: 2, subgroup: "A", total: 444, other: "bravo" },
  { group: 1, subgroup: "A", total: 333, other: "charlie" },
  { group: 1, subgroup: "B", total: 222, other: "delta" },
  { group: 1, subgroup: "A", total: 555, other: "echo" },
  { group: 1, subgroup: "C", total: 777, other: "foxtrot" },
  { group: 1, subgroup: "A", total: 999, other: "golf" },
  { group: 2, subgroup: "B", total: 333, other: "hotel" },
  { group: 1, subgroup: "B", total: 666, other: "india" },
  { group: 2, subgroup: "A", total: 555, other: "joliet" }
];
Enter fullscreen mode Exit fullscreen mode

We want to sort it in ascending order by group, within a group in ascending order by subgroup, and within a subgroup in descending order by total; how can we do it? The solution is simple. Given two objects, if the group fields do not match, we already know which goes first; just compare the group fields. If the group fields match, but the subgroup fields don't, we also know which goes first; compare the subgroup fields. And if the subgroup fields also match, then the comparison depends on (reverse) comparing the total fields. We can code that directly.

const compareData = (a, b) => {
  if (a.group !== b.group) {
    return compareNumbers(a.group, b.group);
  } else if (a.subgroup !== b.subgroup) {
    return compareStrings(a.subgroup, b.subgroup);
  } else {
    return reverse(compareNumbers)(a.total, b.total);
  }
};

data.sort(compareData);
Enter fullscreen mode Exit fullscreen mode

This works perfectly well -- sorting by several fields is just a matter of using the comparison functions one by one.

[{group:1, subgroup:"A", total:999, other:"golf"},
 {group:1, subgroup:"A", total:555, other:"echo"},
 {group:1, subgroup:"A", total:333, other:"charlie"},
 {group:1, subgroup:"B", total:666, other:"india"},
 {group:1, subgroup:"B", total:222, other:"delta"},
 {group:1, subgroup:"C", total:777, other:"foxtrot"},
 {group:2, subgroup:"A", total:555, other:"joliet"},
 {group:2, subgroup:"A", total:444, other:"bravo"},
 {group:2, subgroup:"B", total:333, other:"hotel"},
 {group:2, subgroup:"B", total:111, other:"alpha"}] 
Enter fullscreen mode Exit fullscreen mode

Sorting -- functionally?

We have now gone over several examples of sorting -- but a minor detail remains: this way of working isn't very functional! Why? Because the .sort() method modifies its input array, which is clearly a side effect. In true functional style, sorting should return a new array. (We discussed side effects and mutator methods in our Immutable objects for safer state article.) We saw a solution there, which we can apply for a better sort. If we want to produce a new array, we use the spread operator to generate a copy, and that's what we sort:

const newData = [...data].sort(compareData);
Enter fullscreen mode Exit fullscreen mode

This small change would produce a new array, newData, leaving the original one unchanged; an easy win!

If you wish, you could also modify the Array.prototype to add a new method, but that's usually not recommended... In any case, if you want to do it, this would work:

Array.prototype.cleanSort = function (fn) {
  return [...this].sort(fn);
};

const newData = data.cleanSort(compareData);
Enter fullscreen mode Exit fullscreen mode

This would work and generate a new array leaving the original one unchanged. But, as I said, modifying JavaScript's objects' prototypes isn't considered a very good practice and may cause problems; be warned!

Conclusion

In this article, we have gone over many examples of usage of .sort() for different kinds of data, showing how we can use this method to our advantage. With these recipes, you should be able to sort anything you want; try them out!

A TIP FROM THE EDITOR: If instead of getting an array into order, you want to randomize it into disorder, take a look at our Shuffling an Array, Not as Trivial as It Sounds article instead!

newsletter

Top comments (0)