Sorting data in ascending and descending order

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 :slight_smile:

2 Likes

I like it. Dont know if i’d have bothered creating flip (letters.sort(comparator).reverse() accomplishes the same, a bit more directly/legibly?), but a good exploration of the subject.

Makes me glad PHP has all of those sorting functions predefined… now if only we could get a spaceship operator in Javascript…

1 Like

You’ve got a point there, nonetheless I think flip is a nice little function to know about.

1 Like

To reverse the sort order just multiply the return value of the comparator function by -1.
Or add another parameter into the comparator function 1 for normal sort and -1 for reverse sort

const comparator = (a, b, c) => {
  if (a < b) return -1 * c
  if (a > b) return 1 * c
  return 0 // stay where you are
}

I had thought about that, but Sort will only pass 2 parameters (a and b) to the comparator function. You could fix a global variable, but thats a last resort.

You can functionally Multiply by -1 by defining flip to be flip = (fn) => (a,b) => fn(a,b) * -1, but that doesnt really shorten anything.

If you want to take that approach you could go with currying.

const comparator = _curry3((c, a, b) => {
  if (a < b) return -1 * c
  if (a > b) return 1 * c
  return 0
})

or a more manual approach

const comparator = (c = 1) => 
  (a, b) => {
     if (a < b) return -1 * c
     if (a > b) return 1 * c
     return 0
  })
[101, 5, 3, 210].sort(comparator(-1)) // [210, 101, 5, 3]

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.