DEV Community

Cover image for JS Sort is "weird".
Zenobio
Zenobio

Posted on • Updated on

JS Sort is "weird".

It does not take a highly experienced JS programmer to notice that Array.prototype.sort works in his own "weird" way. But maybe you're not familiar with JS at all, in that case, let me show you what i mean by "weird":

[32, 2, 43, 101, 1025, 5].sort() 
// Result: (5) [101, 1025, 2, 32, 43, 5]
Enter fullscreen mode Exit fullscreen mode

TL:DR

The sort method turns each and every value into string and then compare their sequences of UTF-16 code units values, leading to the "weird" behavior.

Not so brief explanation:

Going into MDN docs we are given the following information:

The sort() method sorts the elements of an array in place and returns the sorted array. The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.

Now that we know that the value is really compared as a string in UTF-16, lets check the values for our first test in this format:

[32, 2, 43, 101, 1025, 5].map(
  value => ({
    value,
    charCodeAt0: value.toString().charCodeAt(0)
  })
)

/* Result: [{…}, {…}, {…}, {…}, {…}, {…}]
0: {value: 32, unityCharCode: 51}
1: {value: 2, unityCharCode: 50}
2: {value: 43, unityCharCode: 52}
3: {value: 101, unityCharCode: 49}
4: {value: 1025, unityCharCode: 49}
5: {value: 5, unityCharCode: 53}
*/
Enter fullscreen mode Exit fullscreen mode

That's nice, if you check some stackoverflow questions about how sort is implemented inside the JS motor, isn't hard to see that it's a simple std::qsort from C++ which will sort the given values alphabetically ascending if no comparison function is provided.

So if we provide a function which compare the charCodeAt0 property for our generated object, we should end with a list sorted the same way, right? Let's test it:

[32, 2, 43, 101, 1025, 5].map(value => ({
    value,
    unityCharCode: value.toString().charCodeAt(0)
  })
).sort(
  (a, z) => a.unityCharCode - z.unityCharCode
)

/* Result: [{…}, {…}, {…}, {…}, {…}, {…}]
0: {value: 101, unityCharCode: 49}
1: {value: 1025, unityCharCode: 49}
2: {value: 2, unityCharCode: 50}
3: {value: 32, unityCharCode: 51}
4: {value: 43, unityCharCode: 52}
5: {value: 5, unityCharCode: 53}
*/
Enter fullscreen mode Exit fullscreen mode

Yep, is seems just like the first test.

But, which function should i use?

Having a little more understanding of how the Array.prototype.sort runs, we can pass a comparison function to handle the sort in the way we want it to:

Alphabetically ascending:

// Only Numbers:
[32, 2, 43, 101, 1025, 5].sort((a, z) => a - z)
// Result: [2, 5, 32, 43, 101, 1025]

// Only Letters:
["j", "A", "c", "D", "a", "d", "e", "k"].sort(
  (a,z) => a > z ? 1 : -1
)
// Result: ["A", "D", "a", "c", "d", "e", "j", "k"]

// Letters and Numbers:
[32, 43, 'j', 'A', 1025, 5, 'a', 'c', 'b']
.sort()
.sort((a,z) => a > z ? 1 : -1)
// Result: ["A", "a", "b", "c", "j", 5, 32, 43, 1025]
Enter fullscreen mode Exit fullscreen mode

Alphabetically descending:

// Only Numbers:
[32, 2, 43, 101, 1025, 5].sort((a, z) => z - a)
// Result: [1025, 101, 43, 32, 5, 2]

// Only Letters:
["j", "A", "c", "D", "a", "d", "e", "k"].sort(
  (a,z) => a < z ? 1 : -1
)
// Result: ["k", "j", "e", "d", "c", "a", "D", "A"]

// Letters and Numbers:
[32, 43, 'j', 'A', 1025, 5, 'a', 'c', 'b']
.sort()
.sort((a,z) => a < z ? 1 : -1)
// Result: ["j", "c", "b", "a", "A", 1025, 43, 32, 5]
Enter fullscreen mode Exit fullscreen mode

In case you want to take it a step further and use a custom function that could validate any of the above cases for you, there you go:

const isNumber = (v) => !isNaN(v)
const compareNumbers = (a, z, order = 'asc') => ({
  asc: a - z,
  desc: z - a
}[order]);

const compareWords = (a, z, order = 'asc') => ({
  asc: a > z ? 1 : -1,
  desc: a < z ? 1 : -1
}[order]);


const compareFunction = (a, z, order = 'asc') => {
  if(isNumber(a) && !isNumber(z)) return 1;
  if(!isNumber(a) && isNumber(z)) return -1;
  if(isNumber(a) && isNumber(z)) { 
    return compareNumbers(a, z, order)
  }

  return compareWords(a, z, order)
}

[32, 43, 'j', 'A', 1025, 5, 'a', 'c', 'b'].sort(
  (a, z) => compareFunction(a, z)
)

//Result: ["A", "a", "b", "c", "j", 5, 32, 43, 1025]
Enter fullscreen mode Exit fullscreen mode

What about undefined?

Well, undefined is a edge case for sort, it will always have a bigger comparison value than any other, so it will always shown in the end of a sorted list.

It doesn't get more complex than this (no pun intended).
You can also check the String.prototype.localeCompare which is pretty good for sorting some words or letters considering that it can differentiate between upper or lower case and also accents.

Top comments (0)