API Intl.Collator

Корректная сортировка и сравнение строк на разных языках

Введение

Сортировка строк в JavaScript кажется простой задачей, пока не сталкиваешься с интернациональным текстом. По умолчанию строки сравниваются по значениям кодовых точек Unicode, что приводит к ошибочным результатам для многих языков. API Intl.Collator обеспечивает сравнение строк с учётом локали, соблюдая культурные правила сортировки и корректно обрабатывая специальные символы.

Почему стандартная сортировка не работает

Рассмотрим сортировку списка немецких имён:

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort());
// ["Ava", "Zoe", "Änder", "Ärzte"]

Такой результат неверен для носителей немецкого языка. В немецком буквы с умлаутами, такие как ä, должны сортироваться рядом с их базовой буквой a, а не в конце. Проблема в том, что JavaScript сравнивает значения кодовых точек Unicode, где Ä (U+00C4) идёт после Z (U+005A).

В разных языках свои правила сортировки. В шведском ä ставится в конец алфавита, в немецком — рядом с a, а во французском акцентированные буквы обрабатываются иначе. Бинарное сравнение игнорирует эти культурные особенности.

Как работает колlation строк

Колляция — это процесс сравнения и упорядочивания строк по языковым правилам. Алгоритм унифицированной колляции Unicode определяет, как сравнивать строки, анализируя символы, диакритические знаки, регистр и пунктуацию по отдельности.

При сравнении двух строк функция колляции возвращает число:

  • Отрицательное значение: первая строка идёт перед второй
  • Ноль: строки считаются эквивалентными на текущем уровне чувствительности
  • Положительное значение: первая строка идёт после второй

Такое трёхстороннее сравнение работает с Array.sort и позволяет точно контролировать, какие различия важны.

Использование localeCompare для базовой сортировки с учётом локали

Метод localeCompare обеспечивает сравнение строк с учётом локали:

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort((a, b) => a.localeCompare(b, "de")));
// ["Ava", "Änder", "Ärzte", "Zoe"]

Это обеспечивает правильную сортировку по-немецки. Первый параметр указывает локаль, а localeCompare автоматически учитывает культурные правила.

В качестве третьего параметра можно передать опции:

const items = ["File10", "File2", "File1"];
console.log(items.sort((a, b) =>
  a.localeCompare(b, "en", { numeric: true })
));
// ["File1", "File2", "File10"]

Опция numeric включает естественную сортировку, при которой "2" идёт перед "10". Без неё "10" будет раньше "2", потому что "1" стоит перед "2".

Проблема с производительностью при многократном использовании localeCompare

Каждый вызов localeCompare заново обрабатывает настройки локали. При сортировке больших массивов это создаёт значительные издержки:

// Inefficient: processes locale for every comparison
const sorted = items.sort((a, b) => a.localeCompare(b, "de"));

Сортировка 1000 элементов требует примерно 10000 сравнений. Каждое сравнение пересоздаёт конфигурацию локали, что многократно увеличивает затраты на производительность. Эта нагрузка становится заметной в интерфейсах с большими наборами данных.

Эффективное сравнение строк с помощью Intl.Collator

Intl.Collator создаёт объект сравнения, который обрабатывает настройки локали один раз и может использоваться повторно:

const collator = new Intl.Collator("de");
const sorted = items.sort((a, b) => collator.compare(a, b));

Экземпляр collator хранит конфигурацию локали и правила сравнения. Метод compare использует эти заранее вычисленные правила для каждого сравнения, устраняя повторную инициализацию.

Производительность увеличивается на 60–80% при сортировке больших массивов по сравнению с многократными вызовами localeCompare.

Прямой доступ к методу compare

Метод compare можно передать напрямую в sort:

const collator = new Intl.Collator("de");
const sorted = items.sort(collator.compare);

Это работает, потому что compare привязан к экземпляру collator. Метод принимает две строки и возвращает результат сравнения, что соответствует сигнатуре, ожидаемой Array.sort.

Как работает опция sensitivity

Опция sensitivity определяет, какие различия между символами учитываются при сравнении. Есть четыре уровня:

Базовая чувствительность

Базовая чувствительность игнорирует акценты и регистр:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("a", "b")); // -1

Отличаются только базовые буквы. Этот уровень хорошо подходит для нестрогого поиска, если пользователь может не ввести акценты.

Учет акцентов

Учет акцентов различает символы с акцентами, но игнорирует регистр:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("á", "A")); // 1

Символы с акцентами и без считаются разными. Прописные и строчные буквы считаются одинаковыми.

Учет регистра

Учет регистра различает прописные и строчные буквы, но игнорирует акценты:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

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

Учет всех различий

Учет всех различий принимает во внимание любые отличия:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

Это стандартный режим сортировки. Любое отличие символов приводит к разному результату сравнения.

Выбор чувствительности в зависимости от задачи

Для разных сценариев нужны разные уровни чувствительности:

  • Сортировка списков: используйте учет всех различий для строгого порядка
  • Поиск по содержимому: используйте базовую чувствительность, чтобы находить совпадения независимо от акцентов и регистра
  • Фильтрация вариантов: используйте учет акцентов, если регистр не важен
  • Поиск с учетом регистра: используйте учет регистра, если акценты не важны

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

Использование опции usage для режимов сортировки и поиска

Опция usage оптимизирует работу коллатора для сортировки или поиска:

// Optimized for sorting
const sortCollator = new Intl.Collator("en", { usage: "sort" });

// Optimized for searching
const searchCollator = new Intl.Collator("en", { usage: "search" });

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

Для поиска без учета регистра и акцентов:

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

const items = ["Apple", "Äpfel", "Banana"];
const matches = items.filter(item =>
  collator.compare(item, "apple") === 0
);
console.log(matches); // ["Apple"]

Такой подход позволяет искать "на глаз", не вводя точные символы.

Включение числовой сортировки для естественного порядка

Опция numeric воспринимает встроенные числа как числовые значения:

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

const files = ["File1", "File10", "File2"];
console.log(files.sort(collator.compare));
// ["File1", "File2", "File10"]

Без числовой сортировки "File10" будет идти перед "File2", потому что строка "10" начинается с "1". Числовая сортировка разбирает числовые последовательности и сравнивает их математически.

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

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

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

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

const values = ["1.5", "1.10", "1.2"];
console.log(values.sort(collator.compare));
// ["1.2", "1.5", "1.10"]

Десятичная точка считается знаком препинания, а не частью числа. Каждый сегмент между знаками препинания сортируется отдельно. Для сортировки десятичных чисел преобразуйте значения в числа и используйте числовое сравнение.

Управление порядком регистра с помощью caseFirst

Опция caseFirst определяет, какие буквы — заглавные или строчные — будут идти первыми при сортировке:

// Uppercase first
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]

// Lowercase first
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]

По умолчанию установлено значение false, что использует стандартный порядок для локали. Эта опция не влияет на сортировку, если sensitivity установлено в base или accent, потому что эти уровни игнорируют регистр.

Игнорирование знаков препинания при сравнении

Опция ignorePunctuation пропускает знаки препинания при сравнении строк:

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

console.log(collator.compare("hello", "he-llo")); // 0
console.log(collator.compare("hello", "hello!")); // 0

Для тайского языка по умолчанию эта опция включена, для других языков — выключена. Используйте её, если знаки препинания не должны влиять на порядок или совпадение строк.

Указание типов колляции для языковых правил

Некоторые локали поддерживают несколько типов колляции для специализированной сортировки:

// Chinese pinyin ordering
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");

// German phonebook ordering
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

// Emoji grouping
const emoji = new Intl.Collator("en-u-co-emoji");

Тип колляции указывается в строке локали с помощью синтаксиса расширения Unicode. Часто используемые типы:

  • pinyin: китайская сортировка по романизированному произношению
  • stroke: китайская сортировка по количеству черт
  • phonebk: немецкий телефонный порядок
  • trad: традиционные правила сортировки для некоторых языков
  • emoji: группировка эмодзи по категориям

Проверьте Intl.supportedValuesOf, чтобы узнать, какие типы сортировки доступны в вашей среде.

Повторное использование экземпляров collator в приложении

Создавайте экземпляры collator один раз и используйте их повторно по всему приложению:

// utils/collation.js
export const germanCollator = new Intl.Collator("de");
export const searchCollator = new Intl.Collator("en", {
  sensitivity: "base"
});
export const numericCollator = new Intl.Collator("en", {
  numeric: true
});

// In your components
import { germanCollator } from "./utils/collation";

const sorted = names.sort(germanCollator.compare);

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

Сортировка массивов объектов по свойству

Используйте collator в функции сравнения, которая обращается к свойствам объекта:

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

const users = [
  { name: "Zoe" },
  { name: "Änder" },
  { name: "Ava" }
];

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

Этот способ работает с любой структурой объекта. Извлеките строки для сравнения и передайте их в collator.

Сравнение производительности Intl.Collator и localeCompare

Intl.Collator обеспечивает лучшую производительность при сортировке больших наборов данных:

// Slower: recreates locale settings for each comparison
const slow = items.sort((a, b) => a.localeCompare(b, "de"));

// Faster: reuses precomputed locale settings
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);

Для небольших массивов (менее 100 элементов) разница незначительна. Для больших массивов (тысячи элементов) Intl.Collator может быть быстрее на 60–80%.

Есть исключение для браузеров на базе V8, например Chrome. Для строк только из ASCII localeCompare использует оптимизацию с таблицами поиска. При сортировке только ASCII-строк localeCompare может работать так же быстро, как Intl.Collator.

Когда использовать Intl.Collator или localeCompare

Используйте Intl.Collator, если:

  • Сортируете большие массивы (сотни или тысячи элементов)
  • Часто сортируете (пользователь меняет порядок, виртуальные списки)
  • Создаёте переиспользуемые функции сравнения
  • Для вас важна производительность

Используйте localeCompare, если:

  • Нужно сделать одно сравнение
  • Сортируете небольшие массивы (менее 100 элементов)
  • Простота важнее производительности
  • Нужно сравнить "на лету" без подготовки

Оба API поддерживают одинаковые опции и дают одинаковый результат. Различие только в производительности и организации кода.

Проверка применённых опций

Метод resolvedOptions возвращает реальные опции, которые использует collator:

const collator = new Intl.Collator("de", { sensitivity: "base" });
console.log(collator.resolvedOptions());
// {
//   locale: "de",
//   usage: "sort",
//   sensitivity: "base",
//   ignorePunctuation: false,
//   collation: "default",
//   numeric: false,
//   caseFirst: "false"
// }

Это помогает отлаживать поведение сортировки и понимать значения по умолчанию. Итоговая локаль может отличаться от запрошенной, если система не поддерживает именно эту локаль.

Проверка поддержки локалей

Проверьте, какие локали поддерживаются в текущем окружении:

const supported = Intl.Collator.supportedLocalesOf(["de", "fr", "xx"]);
console.log(supported); // ["de", "fr"]

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

Поддержка браузеров и окружений

Intl.Collator широко поддерживается с сентября 2017 года. Все современные браузеры и версии Node.js поддерживают этот API. Он работает одинаково во всех средах.

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

Частые ошибки, которых стоит избегать

Не создавайте новый collator для каждого сравнения:

// Wrong: creates collator repeatedly
items.sort((a, b) => new Intl.Collator("de").compare(a, b));

// Right: create once, reuse
const collator = new Intl.Collator("de");
items.sort(collator.compare);

Не думайте, что сортировка по умолчанию подходит для интернационального текста:

// Wrong: breaks for non-ASCII characters
names.sort();

// Right: use locale-aware sorting
names.sort(new Intl.Collator("de").compare);

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

// Wrong: variant sensitivity requires exact match
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);

// Right: base sensitivity for fuzzy matching
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);

Практические примеры использования

Используйте Intl.Collator для:

  • Сортировки пользовательского контента (имена, заголовки, адреса)
  • Реализации поиска и автодополнения
  • Построения таблиц данных с сортируемыми столбцами
  • Создания фильтрованных списков и выпадающих опций
  • Сортировки имён файлов и номеров версий
  • Алфавитной навигации в списках контактов
  • Мультиязычных интерфейсов приложений

Любой интерфейс, где пользователю показывается отсортированный текст, выигрывает от сортировки с учётом локали. Это делает приложение более привычным и корректным для любого языка пользователя.