Числовая сортировка

Обычное лексикографическое сравнение строк в JavaScript основано на кодовых точках Unicode и не учитывает семантику чисел. Это приводит к неожиданным результатам при сортировке значений, содержащих числа:

["file1", "file2", "file10", "file20"].sort()
// ["file1", "file10", "file2", "file20"]

Причина в том, что сравнение выполняется посимвольно: символ "1" в "file10" оказывается меньше "2" в "file2", хотя с точки зрения человека порядок должен быть другим.

Для решения этой проблемы используется механизм локализованного сравнения строк с числовой чувствительностью, реализованный через Intl.Collator.


Intl.Collator и числовой режим сравнения

Класс Intl.Collator предоставляет средство для языково-зависимого сравнения строк. Он учитывает правила конкретной локали и дополнительные параметры сортировки.

Ключевая опция для числовой сортировки:

  • numeric: true — включает «естественное» сравнение чисел внутри строк

Пример:

const collator = new Intl.Collator("en", { numeric: true });

["file1", "file2", "file10", "file20"].sort(collator.compare);
// ["file1", "file2", "file10", "file20"]

В этом режиме последовательности цифр внутри строки интерпретируются как целые числа, а не как набор символов.


Принцип работы numeric-сравнения

При включённой опции numeric алгоритм сравнения разбивает строку на сегменты:

  • текстовые фрагменты
  • числовые последовательности

Далее применяется гибридная логика:

  • текст сравнивается по правилам локали
  • числа сравниваются как числовые значения, а не строки

Таким образом:

"file2" < "file10"

потому что 2 < 10, а не потому что символ '2' меньше '1'.


Сравнение с localeCompare

Аналогичный результат можно получить через String.prototype.localeCompare, который принимает те же параметры:

["file1", "file2", "file10"].sort((a, b) =>
  a.localeCompare(b, "en", { numeric: true })
);

Разница:

  • Intl.Collator создаётся один раз и переиспользуется
  • localeCompare создаёт временный механизм при каждом вызове

Для больших массивов предпочтителен Intl.Collator:

const collator = new Intl.Collator("en", { numeric: true });

array.sort(collator.compare);

Локализация и числовая сортировка

Числовая интерпретация работает независимо от языка, но порядок строковой части зависит от локали:

const ruCollator = new Intl.Collator("ru", { numeric: true });

["a2", "a10", "a1"].sort(ruCollator.compare);
// ["a1", "a2", "a10"]

При этом правила алфавита определяются языком, а числа обрабатываются универсально.


Сортировка версий и семантика точек

Частый сценарий — сортировка версий:

["v1.2.0", "v1.10.0", "v1.3.0"].sort(collator.compare);

Результат:

v1.2.0
v1.3.0
v1.10.0

Каждая числовая группа сравнивается независимо.

Важно учитывать, что:

  • разделители (. - _) не объединяют числа
  • каждая числовая последовательность обрабатывается отдельно

Сложные числовые случаи

Ведущие нули

["file01", "file1", "file002"].sort(collator.compare);

Результат зависит от реализации, но обычно:

file1
file01
file002

Числа сравниваются как значения, но строковая структура может влиять на вторичную сортировку.


Десятичные числа

["1.2", "1.10", "1.3"].sort(collator.compare);

Результат:

1.2
1.3
1.10

Точка разделяет числовые сегменты, поэтому сравнение идёт по частям (1, 2 vs 1, 10).


Смешанные строки

["img20version3", "img3version12", "img20version1"].sort(collator.compare);

Числовая логика применяется к каждому числовому сегменту независимо, что даёт естественный порядок для человеческого восприятия.


Поведение с отрицательными числами и символами

Intl.Collator не трактует знак минус как часть числа. Строки:

["-1", "-10", "2"]

рассматриваются как обычный текст с символами, а не как числовые значения.

Для полноценной числовой сортировки отрицательных чисел требуется предварительное преобразование:

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

или комбинированная логика при смешанных данных.


Параметры, влияющие на результат

sensitivity

Определяет строгость сравнения:

  • "base" — игнорирует диакритику и регистр
  • "accent" — учитывает диакритику
  • "case" — учитывает регистр
  • "variant" — максимально строгий режим

При числовой сортировке обычно используется:

new Intl.Collator("en", { numeric: true, sensitivity: "base" });

caseFirst

Определяет приоритет регистра:

  • "upper"
  • "lower"
  • "false"

В числовой сортировке влияет только на текстовые сегменты, не затрагивая числа.


ignorePunctuation

Игнорирует знаки пунктуации при сравнении текста:

new Intl.Collator("en", {
  numeric: true,
  ignorePunctuation: true
});

Полезно для строк вроде:

"file-2", "file2"

Производительность и повторное использование

Создание Intl.Collator — относительно дорогая операция. При сортировке больших массивов важно избегать повторного создания объекта:

// плохо
array.sort((a, b) =>
  new Intl.Collator("en", { numeric: true }).compare(a, b)
);

// хорошо
const collator = new Intl.Collator("en", { numeric: true });
array.sort(collator.compare);

Дополнительно:

  • один Collator можно использовать многократно
  • особенно критично в циклах и частых рендерах

Стабильность сортировки

Сортировка через Intl.Collator в современных движках обычно стабильна, но спецификация JavaScript не гарантирует стабильность Array.prototype.sort во всех реализациях.

При необходимости строгой стабильности применяется декорирование:

array
  .map((value, index) => ({ value, index }))
  .sort((a, b) => {
    const res = collator.compare(a.value, b.value);
    return res !== 0 ? res : a.index - b.index;
  })
  .map(item => item.value);

Типичные области применения числовой сортировки

Файловые системы и имена

["doc1", "doc20", "doc3"]

Версионирование

["2.0.1", "2.0.10", "2.0.2"]

Каталоги и списки товаров

["item9", "item10", "item11"]

Логические идентификаторы

["user2session3", "user10session1"]

Ограничения подхода

Числовой режим Intl.Collator не является полноценным числовым парсером. Он не решает задачи:

  • математического сравнения отрицательных и вещественных чисел в общем виде
  • обработки научной нотации (1e10)
  • строгой числовой нормализации

Он работает исключительно в рамках строковой интерпретации числовых сегментов.


Поведение в разных локалях

Хотя числовая логика универсальна, локаль влияет на текстовые части:

const de = new Intl.Collator("de", { numeric: true });
const en = new Intl.Collator("en", { numeric: true });

Различия проявляются при сортировке букв, но не чисел внутри строк.