Sorting Data
I was looking at a React tutorial the other day on how to sort tables by column. Midway through the tutorial I couldn’t resist doing a bit of experimentation to see I how I might approach the task and just thought I would share what I learnt along the way.
The following is a look at one part of that, which is Array.sort
Array.sort()
To start with I want to look at the typical array sort routine — usually something like this. Note I am aware we could just use the default Array.sort()
method, but thought best to be explicit.
const comparator = (a, b) => {
if (a < b) return -1 // shift to the left
if (a > b) return 1 // shift to the right
return 0 // stay where you are
}
const letters = ['c', 'f', 'a', 'd']
const numbers = [5, 3, 1, 7]
letters.sort(comparator)
// ['a', 'c', 'd', 'f']
numbers.sort(comparator)
// [1, 3, 5, 7]
The above works fine for same case letters, but doesn’t work so well if we mix upper and lowercases. It also gives unexpected results for a sequence of numbers in string format.
const letters = ['Z', 'a', 'd', 'F']
const numbers = ['101', '5', '3', '210']
letters.sort(comparator)
// ['F', 'Z', 'a', 'd']
numbers.sort(comparator)
// ['101', '210', '3', '5']
The reason for the above result is due to the compare operators (< >) evaluating each character to its unicode value. For instance uppercase letters have lower values than lowercase letters e.g.
'a'.charCodeAt(0) // 97
'A'.charCodeAt(0) // 65
So one solution to that might be to amend the comparator accordingly
const comparator = (a, b) => {
const toLowerA = a.toLowerCase() // convert to lowercase
const toLowerB = b.toLowerCase() // same here
if (toLowerA < toLowerB) return -1
if (toLowerA > toLowerB) return 1
return 0
}
const letters = ['Z', 'a', 'd', 'F']
letters.sort(comparator)
// ['a', 'd', 'F', 'Z']
This has solved the problem with letters, but what about numbers? Fortunately there is a one stop solution in the form of localeCompare
'b'.localeCompare('a') // 1
'a'.localeCompare('b') // -1
LocaleCompare provides us with the option to handle numbers in string format as well.
'101'.localeCompare('2', undefined, { numeric: true }) // 1
'b'.localeCompare('a', undefined, { numeric: true }) // 1
We can now re-write the comparator accordingly.
const comparator = (a, b) => {
const stringA = a.toString()
const stringB = b.toString()
return stringA.localeCompare(stringB, undefined, { numeric: true })
}
const letters = ['c', 'f', 'a', 'd']
const numbers = [101, 5, 3, 210]
letters.sort(comparator)
// ['a', 'c', 'd', 'f']
numbers.sort(comparator)
// [3, 5, 101, 210]
Reverse order
To finish this off and for a bit of fun I want to throw in a functional method that can help us to reverse the order called flip.
const flip = (fn) => (a, b) => fn(b, a)
It takes a binary callback function, a function with two parameters, and returns a function that calls that given callback with the arguments in reverse order. We can use it like this.
const comparator = (a, b) => {
... same as last example
}
const reverseComparator = flip(comparator)
const letters = ['c', 'f', 'a', 'd']
const numbers = [101, 5, 3, 210]
letters.sort(reverseComparator)
// ['f', 'd', 'c', 'a']
numbers.sort(reverseComparator)
// [210, 101, 5, 3]
So there you go, hopefully that was short and to the point.
Note: There is another option that follows the localeCompare approach/pattern that is also worth considering, called Intl.collator. I will leave that to you