Кастомные правила сортировки

Сортировка строк в JavaScript по умолчанию опирается на Unicode-коды символов, что приводит к результатам, не соответствующим языковым ожиданиям. Например, строки с заглавными буквами могут идти отдельно от строчных, а символы с диакритикой обрабатываются как «более поздние» или «менее ранние» в зависимости от их кодовой позиции, а не лингвистического смысла.

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

Базовый принцип работы Collator

Intl.Collator предоставляет метод сравнения, который можно передавать в Array.prototype.sort.

const collator = new Intl.Collator('ru');

const words = ['яблоко', 'Яндекс', 'арбуз', 'ёж', 'елка'];
words.sort(collator.compare);

console.log(words);

Метод compare возвращает:

  • отрицательное значение, если первая строка меньше второй
  • положительное значение, если больше
  • 0, если строки эквивалентны с точки зрения сортировки

Это поведение соответствует контракту функции сравнения для sort.

Локали и их влияние на порядок сортировки

Первый аргумент Intl.Collator — локаль, определяющая правила сортировки:

const collator = new Intl.Collator('de');

Локаль влияет на:

  • порядок букв с диакритикой
  • правила сравнения ß, ä, ö, ü
  • обработку регистра
  • специфические алфавитные особенности

При отсутствии локали используется локаль окружения выполнения.

Игнорирование регистра и диакритики

Одной из ключевых задач кастомной сортировки является управление «строгостью» сравнения.

Игнорирование регистра

const collator = new Intl.Collator('en', {
  sensitivity: 'base'
});

Значение sensitivity:

  • base — игнорирует регистр и диакритику
  • accent — учитывает диакритику, но не регистр
  • case — учитывает регистр, но не диакритику
  • variant — учитывает всё

Пример:

const collator = new Intl.Collator('en', { sensitivity: 'base' });

['a', 'A', 'á', 'b'].sort(collator.compare);
// ['a', 'A', 'á', 'b']

Управление регистром через caseFirst

Параметр caseFirst задаёт приоритет заглавных или строчных букв:

const collator = new Intl.Collator('en', {
  sensitivity: 'variant',
  caseFirst: 'upper'
});

Возможные значения:

  • upper — заглавные первыми
  • lower — строчные первыми
  • false — без приоритета
['a', 'A', 'b', 'B'].sort(collator.compare);

Результат зависит от выбранной стратегии.

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

Обычная сортировка строк приводит к неожиданным результатам:

['1', '2', '10', '20'].sort();
// ['1', '10', '2', '20']

Включение числового режима решает проблему:

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

['1', '2', '10', '20'].sort(collator.compare);
// ['1', '2', '10', '20']

Этот режим анализирует числовые последовательности внутри строки и сравнивает их как числа.

Управление пунктуацией

Параметр ignorePunctuation позволяет игнорировать знаки препинания при сравнении:

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

Пример:

['a-b', 'ab', 'a b'].sort(collator.compare);

Без игнорирования пунктуации эти строки могут иметь неожиданный порядок из-за символов - и пробелов.

Стабильная и предсказуемая сортировка

Хотя Intl.Collator определяет правила сравнения, стабильность сортировки зависит от реализации движка. В современных движках сортировка обычно стабильна, но это не гарантируется стандартом.

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

const collator = new Intl.Collator('en');

const data = ['b', 'a', 'a', 'c'];

const sorted = data
  .map((value, index) => ({ value, index }))
  .sort((a, b) =>
    collator.compare(a.value, b.value) || a.index - b.index
  )
  .map(item => item.value);

Комбинирование правил для кастомной сортировки

Кастомная сортировка часто требует комбинации нескольких факторов:

  • локализованное сравнение
  • числовая логика
  • вторичные ключи сортировки
  • бизнес-правила
const collator = new Intl.Collator('ru', {
  sensitivity: 'base',
  numeric: true
});

function compareItems(a, b) {
  return (
    collator.compare(a.name, b.name) ||
    a.priority - b.priority ||
    a.id - b.id
  );
}

Такой подход позволяет строить многоуровневую систему сортировки.

Использование compare в функциональных цепочках

Intl.Collator.compare может использоваться не только в sort, но и в других структурах сравнения:

const collator = new Intl.Collator('en');

const set = new Set(['z', 'a', 'm']);

[...set].sort(collator.compare);

Также он применяется при поиске позиции вставки:

function findInsertIndex(arr, value, collator) {
  let low = 0;
  let high = arr.length;

  while (low < high) {
    const mid = (low + high) >> 1;
    if (collator.compare(arr[mid], value) < 0) {
      low = mid + 1;
    } else {
      high = mid;
    }
  }

  return low;
}

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

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

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

function sortData(data) {
  return data.sort(collator.compare);
}

Избегание повторного создания особенно важно при сортировке больших массивов или частых обновлениях интерфейса.

Особенности поведения в разных локалях

Разные языки задают различные правила сравнения:

  • немецкий учитывает специфические лигатуры
  • французский по-разному обрабатывает диакритику
  • турецкий имеет отдельные правила для i/I и ı/İ
const tr = new Intl.Collator('tr');
['i', 'ı', 'I', 'İ'].sort(tr.compare);

Результат может существенно отличаться от английской или русской локали, что важно учитывать при интернациональных системах.

Расширенные параметры collation

Некоторые движки поддерживают дополнительные параметры через usage, ignorePunctuation, numeric, но базовая модель всегда строится вокруг:

  • локали
  • чувствительности (sensitivity)
  • порядка регистра (caseFirst)
  • числового режима (numeric)

Эти параметры формируют поведение сравнения, которое ближе к естественному языковому восприятию, чем к машинному порядку Unicode-кодов.

Интеграция с сортировкой структурированных данных

При работе с массивами объектов Intl.Collator используется как часть функции сравнения:

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

const users = [
  { name: 'Иван10' },
  { name: 'Иван2' },
  { name: 'Иван1' }
];

users.sort((a, b) => collator.compare(a.name, b.name));

Такой подход позволяет реализовать сортировку, устойчивую к смешанным числовым и строковым значениям внутри идентификаторов.

Поведение при частичном совпадении строк

Collator сравнивает строки не только посимвольно, но и с учётом лингвистической значимости символов. При равенстве на базовом уровне применяется более глубокий анализ:

  • акценты
  • регистр
  • вариации символов
  • расширенные Unicode-свойства

Это делает поведение более предсказуемым в многоязычных системах по сравнению с прямым лексикографическим сравнением.

Сочетание с localeCompare

String.prototype.localeCompare является обёрткой над аналогичной логикой:

'a'.localeCompare('b', 'en');

Но Intl.Collator предпочтительнее при множественных сравнениях, поскольку позволяет избежать повторной интерпретации опций:

const collator = new Intl.Collator('en');

array.sort(collator.compare);

В высокопроизводительных сценариях это снижает накладные расходы и упрощает повторное использование логики сравнения.