JS Methods that Blew My Mind - Array.prototype.sort() & String.prototype.localeCompare()

·

4 min read

#Array.prototype.sort() The sort() method sorts the elements of an array in place and returns the reference to the same array, now sorted. compareFn is an optional parameter. When passed in, it specifies a function that defines the sort order. If omitted, the array elements are converted to strings, then sorted according to each character's Unicode code point value.

I always knew that I could pass in an arrow function with a and b to sort an array, but I had only used the most straightforward ones such as:

let arr1 = [3, 0, 8, -2]

let arr2 = arr1.sort((a, b) => a - b)
console.log(arr1) // [-2, 0, 3, 8]
console.log(arr2) // [-2, 0, 3, 8]

let arr3 = arr1.sort((a, b) => b - a)
console.log(arr3) // [8, 3, 0, -2]
console.log(arr1) // [8, 3, 0, -2]
console.log(arr2) // [8, 3, 0, -2]

Note that sort() will mutate the original array! To prevent it from happening, make a copy of the array and then sort the copy:

let arr4 = [2, 4, 5, 0]
let copyOfArr4 = [...arr4]
let arr5 = copyOfArr4.sort((a, b) => a - b)
console.log(arr4) // [2, 4, 5, 0]
console.log(arr5) // [0, 2, 4, 5]
console.log(copyOfArr4) // [0, 2, 4, 5]

However, I had never thought much about the how or why the callback function is written in such a way.

In fact, the compare function works as such:

function compare(a, b) {
  if (a < b) return -1 // a - b < 0, sort a before b
  if (a > b) return 1 // a - b > 0, sort a after b
  // if a === b
  return 0 // keep original order of a and b
}

So, to sort an array of numbers in ascending order, we could write:

arr.sort((a, b) => a - b)

And for the same reason, to sort an array of numbers in descending order:

arr.sort((a, b) => b - a)

What if we need to sort an array in a more complex way? Can we take advantage of the compareFn by customizing it?

I came across a coding challenge that "forced" me to practice writing my own compareFn to pass in the sort() method: I was asked to write a function that takes in a string of space-separated numbers (e.g. "106 70 74 100 99 68 86 180 90"), for each of these numbers, sum up the total of each digit, and return a string of the sums, sorted in ascending order.

Doesn't sound too hard, right? Here's the catch (of course): If two or more numbers from the original string have the same sum of digits, they need to be treated as strings to sort. For example, '106' and '70' have the same sum -- 7, but '107' should be sorted before '70'.

Step 1: Trim the string and split it into an array -- easy.

Step 2: Get the sum of digit for each number -- not hard.

Step 3: Sort the sums in ascending order -- sure.

Step 4: Combining step 2 and step 3 by passing in a customized compareFn in sort()? -- I think I can do that.

Step 5: Wait, but what if the sums are the same? How should I refer to the original numbers and compare them as string? ... It really got me.

After some research (Googling), I found this string method that works perfectly with a customized compareFn for sort():

#String.prototype.localeCompare() The localeCompare() method returns a number (a negative number if referenceStr occurs before compareString; positive if the referenceStr occurs after compareString; 0 if they are equivalent.) indicating whether a reference string comes before, or after, or is the same as the given string in sort order.

a.localeCompare(b) returns a number indicating whether a reference string a comes before, or after, or is the same as the given string b in sort order. A negative number if referenceStr (a) occurs before compareString (b); positive if the referenceStr occurs after compareString; 0 if they are equivalent.

Notice that the form of return is similar to the compareFn in sort(): localeCompare() returns:

  • < 0 if a occurs before b; (thus sort a before b)
  • > 0 if the a occurs after b; (thus sort a after b)
  • 0 if a and bare equivalent. (thus keep the original order of a and b)

So I came up with the solution below:

function orderNums(strng) {
    let originalNums = strng.trim().split(' ').filter(w => w !== '') // the challenge states that the strng may have leading and trailing spaces

    let sortedNums = originalNums((a, b) => {
        let sumOfA = a.split('').reduce((sum, num) => s + +n, 0) 
        let sumOfB = b.split('').reduce((sum, num) => s + +n, 0)
        return sumOfA === sumOfB ? a.localeCompare(b) : sumOfA - sumOfB
    })

    return sortedNums.join(' ')
}

This way, I'm sorting the original numbers as strings only if the sums of their digits are the same. The return value from my customized compareFn will "tell" sort() whether to place a before or after b, same as the sumOfA - sumOfB.