1

I think I"m a little confused about how JS sort works and how the compare function uses the positive and negative numbers that are returned from it. I have this code:

const stripCommas = value =>
  typeof value === "string" ? value.replace(/,/g, "") : value;

const compareStringNumbersWithCommasAndStrings = (option1, option2) =>
  isNaN(stripCommas(option2)) - isNaN(stripCommas(option1)) ||
  stripCommas(option1) - stripCommas(option2);

and this array:

var array = ["full", "no", '1,000', '3,000', 2, "may", 0]

and when this is run, this is the result:

array.sort(compareStringNumbersWithCommasAndStrings)
    => [ 'full', 'no', 'may', 0, 2, '1,000', '3,000' ]

So when "full" and "no" are passed into the compareStringNumnbersWithCommasAndStrings function, both are NaN so we get true - true which is === 0 so we get 0 || NaN which evaluates to NaN which is equivalent to 0 apparently in a sort function so the elements do not change. That I understand.

But when we get to no and 1,000, the compare function will do true - false in the first part before the || which evaluates to 1 and then no - 1000 which evaluates to NaN so then 1 || NaN evaluates to 1 so shouldn't the two switch? Doesn't sort switch when the result is positive? The positive means the first option passed into the sort will be at a higher index than the second right?

Under the hood, what kind of sort is this? How many compares are being made?

isherwood
  • 58,414
  • 16
  • 114
  • 157
Jwan622
  • 11,015
  • 21
  • 88
  • 181
  • The sort algorithm is unspecified, other than it need not be stable. Your comparator should also be prepared for the two elements to be passed in reverse order from their original positions. – Pointy Jan 25 '19 at 16:50
  • 2
    Also between `no` and `1,000` it'll be `false - true` because you perform that subtraction backwards (`option2` is on the *left* side of the `-` operator). – Pointy Jan 25 '19 at 16:56
  • It'd help you see what's going on to add a `console.log()` call inside the comparator so you can see the values and comparison results. – Pointy Jan 25 '19 at 16:58
  • 1
    You should never return `NaN` – Bergi Jan 25 '19 at 17:04
  • "Under the hood" is defined in ECMAScript 2015 (as of this writing): https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.sort – Heretic Monkey Jan 25 '19 at 17:08

2 Answers2

4

But when we get to no and 1,000, the compare function will do true - false in the first part before the || which evaluates to 1 and then no - 1000 which evaluates to NaN so then 1 || NaN evaluates to 1 so shouldn't the two switch?

No, because you are doing

isNaN(stripCommas(option2)) - isNaN(stripCommas(option1))
//                      ^                             ^

instead of

isNaN(stripCommas(option1)) - isNaN(stripCommas(option2))

So in fact it evaluates to -1, which causes the strings to be placed before the numbers. Change the subtraction order and the strings will be sorted towards the end.

Under the hood, what kind of sort is this? How many compares are being made?

See Javascript Array.sort implementation? for that.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
1

Note that while the answer from Bergi fixes the problem you note, you might still have a problem. This will sort anything not a number or comma-filled number-in-string to the end, but will not sort among those values in any particular manner. I believe most current implementations use stable sorts, so they would probably end up in the same relative order as in the input; but this stability is not required by the specification, and a few years ago there were non-stable version.

If you want to further sort these values, say by using a natural sort, you need to do a bit more. This is one such version:

const stripCommas = value =>
  typeof value === "string" ? value.replace(/,/g, "") : value;

const compare = (option1, option2, a = stripCommas(option1), b = stripCommas(option2)) =>
  isNaN(a) && isNaN(b)
    ? (a < b ? -1 : a > b ? 1 : 0)   // sort the non-'number's
    : isNaN(a)
      ? 1                            // move non-'number' after 'number'
      : isNaN(b)
        ? -1                         // move 'number' before non-'number'
        : a - b                      // sort the 'number's

const array = ["full", "no", '1,000', '3,000', 2, "may", 0]

array.sort(compare)
  //=> [0, 2, "1,000", "3,000", "full", "may", "no"]
  
console.log(array)

Update

Note that while it's less explicit, this version does the same thing, since the natural sort on numbers works equivalently to subtracting:

const compare = (option1, option2, a = stripCommas(option1), b = stripCommas(option2)) =>
  isNaN(a) == isNaN(b)
    ? (a < b ? -1 : a > b ? 1 : 0)
    : isNaN(a) ? 1 : -1

(I think this is perhaps too obscure, simulating a not-xor operation by calling == on two booleans, but it might be worth considering.)

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • well quicksort would be a good choice for big arrays, and basic quicksort isn't stable. However there may well be some variants or tricks that could make for a stable algorithm mostly like quicksort. – Pointy Jan 25 '19 at 17:40
  • Right. At the moment, I believe the main engines are using stable algorithms like `mergesort` or `timsort`, or hybrid-but-still stable variants, but there's no requirement that they do so. – Scott Sauyet Jan 25 '19 at 17:45