DEV Community

Kai Mercer
Kai Mercer

Posted on

Why sort() puts 100 before 2, and Eclair after zebra

[10, 9, 1, 100, 25, 2].sort() does not give you 1, 2, 9, 10, 25, 100. I ran the usual ways to sort numbers and names in Node v22.23.4 (V8) and wrote down what each one does, so you can pick on purpose.

Numbers

Call Result
.sort() [1, 10, 100, 2, 25, 9]
.sort((a, b) => a - b) [1, 2, 9, 10, 25, 100]
.sort((a, b) => a - b) on [3, NaN, 1, 2] [3, NaN, 1, 2] (not sorted)

MDN says the default sort converts elements to strings and compares UTF-16 code units, so "100" sorts before "2". The a - b comparator fixes numbers, but MDN notes it only works if the array has no NaN. My run shows what that looks like: nothing throws, the array just comes back in an order that is not sorted.

Names and file names

Input: ["Zoë", "zebra", "Éclair", "apple", "Apple", "eclair", "Zoe", "10 files", "2 files", "file 10", "file 2"]

Method Result
default .sort() 10 files, 2 files, Apple, Zoe, Zoë, apple, eclair, file 10, file 2, zebra, Éclair
localeCompare(b, "en") 10 files, 2 files, apple, Apple, eclair, Éclair, file 10, file 2, zebra, Zoe, Zoë
Intl.Collator("en", {numeric: true}) 2 files, 10 files, apple, Apple, eclair, Éclair, file 2, file 10, zebra, Zoe, Zoë

Three things to see:

  • The default puts every capital letter before every lowercase one, and "Éclair" lands after "zebra", because the comparison is by code unit.
  • localeCompare gives the order people expect for letters, but "file 10" still sorts before "file 2" and "10 files" before "2 files".
  • Adding numeric: true to a collator is what makes "2 files" come before "10 files". MDN lists numeric as off by default.

Equal or not: sensitivity

With sensitivity: "base", my run gave compare("Zoe", "Zoë") === 0, compare("a", "A") === 0 and compare("e", "é") === 0. That is what you want for search-as-you-type, and the wrong choice for a list where Zoe and Zoë are different people. MDN describes base as differing only in base letters, and says the default for sorting is variant.

The locale is part of the answer

["z", "ä", "a"] sorted with Intl.Collator("de") gave a, ä, z. With Intl.Collator("sv") it gave a, z, ä. Same data, same code, different order, because the locale decides where ä goes.

Holes and undefined

[3, undefined, 1, , 2].sort() gave [1, 2, 3, undefined, <empty>]: the length stayed 5, undefined went to the end, and the empty slot after it (MDN documents both rules). The comparator is never called for undefined.

What I would do

  • For numbers, always pass a comparator, and filter out NaN first if it can occur.
  • For text a person will read, use one Intl.Collator with the user's locale and numeric: true for names with numbers in them. Create it once and pass collator.compare.
  • Use toSorted() when you do not want the original array changed (my run: [3, 1, 2] stayed as it was).
  • Write one test with "Éclair", "file 2" and "file 10".

Not tested

Browsers (Node only), Safari and Firefox collation differences, locales other than en, de and sv, very large arrays, and sort stability. Collation depends on the engine's locale data, so check your target runtime.

Related: One number, three roundings.

Top comments (0)