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 Collation определяет, как сравнивать строки, анализируя символы, диакритические знаки, регистр и пунктуацию отдельно.

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

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

Этот трехсторонний шаблон сравнения работает с 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 обрабатывает настройки локали с нуля. При сортировке больших массивов это создает значительные накладные расходы:

// Неэффективно: обрабатывает локаль для каждого сравнения
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 контролирует, какие различия между символами учитываются при сравнении. Существует четыре уровня:

Чувствительность на уровне основы

Чувствительность на уровне основы игнорирует акценты и регистр:

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 оптимизирует поведение сравнения для сортировки или поиска:

// Оптимизировано для сортировки
const sortCollator = new Intl.Collator("en", { usage: "sort" });

// Оптимизировано для поиска
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 определяет, какие буквы — заглавные или строчные — будут располагаться первыми:

// Сначала заглавные
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]

// Сначала строчные
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]

По умолчанию используется false, что соответствует порядку по умолчанию для локали. Эта опция не влияет на сортировку, если уровень чувствительности установлен на 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

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

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

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

// Упорядочивание по пиньинь для китайского языка
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");

// Упорядочивание по телефонной книге для немецкого языка
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

// Группировка эмодзи
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
});

// В ваших компонентах
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 обеспечивает лучшую производительность при сортировке больших наборов данных:

// Медленнее: настройки локали создаются заново для каждого сравнения
const slow = items.sort((a, b) => a.localeCompare(b, "de"));

// Быстрее: повторное использование предварительно настроенных локалей
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);

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

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

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

Используйте Intl.Collator, когда:

  • Нужно сортировать большие массивы (сотни или тысячи элементов)
  • Требуется повторная сортировка (пользователь переключает порядок сортировки, виртуальные списки)
  • Вы создаёте универсальные утилиты для сравнения
  • Производительность важна для вашего случая использования

Используйте localeCompare, когда:

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

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

Проверка разрешённых параметров

Метод resolvedOptions возвращает фактические параметры, используемые компаратором:

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 для каждого сравнения:

// Неправильно: объект Collator создаётся многократно
items.sort((a, b) => new Intl.Collator("de").compare(a, b));

// Правильно: создайте один раз и используйте повторно
const collator = new Intl.Collator("de");
items.sort(collator.compare);

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

// Неправильно: не работает с не-ASCII символами
names.sort();

// Правильно: используйте сортировку с учётом локали
names.sort(new Intl.Collator("de").compare);

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

// Неправильно: чувствительность к вариантам требует точного совпадения
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);

// Правильно: базовая чувствительность для нестрогого совпадения
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);

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

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

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

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