Как сортировать строки в алфавитном порядке по локали в JavaScript
Используйте Intl.Collator и localeCompare() для правильной сортировки строк на любом языке
Введение
Когда вы сортируете массив строк в JavaScript, поведение по умолчанию сравнивает строки по их значениям кодовых единиц UTF-16. Это работает для базового текста ASCII, но не подходит для сортировки имен, названий продуктов или любого текста, содержащего символы с акцентами, нелатинские алфавиты или буквы разного регистра.
Разные языки имеют разные правила алфавитного порядка. В шведском языке буквы å, ä и ö находятся в конце алфавита после z. В немецком языке ä в большинстве случаев считается эквивалентной a. Во французском языке акценты игнорируются в определенных режимах сравнения. Эти лингвистические правила определяют, как люди ожидают видеть отсортированные списки на своем языке.
JavaScript предоставляет два API для сортировки строк с учетом локали. Метод String.prototype.localeCompare() обрабатывает простые сравнения. API Intl.Collator обеспечивает лучшую производительность при сортировке больших массивов. В этом уроке объясняется, как работают оба метода, когда использовать каждый из них и как настроить поведение сортировки для разных языков.
Почему сортировка по умолчанию не подходит для международного текста
Метод Array.sort() по умолчанию сравнивает строки по их значениям кодовых единиц UTF-16. Это означает, что заглавные буквы всегда идут перед строчными, а символы с акцентами сортируются после z.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// Вывод: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
Этот вывод неверен для шведского языка. В шведском языке буквы å, ä и ö являются отдельными буквами, которые должны находиться в конце алфавита. Правильный порядок должен быть: Anna, затем Bengt, затем Åsa, Ärla и Örjan.
Проблема возникает из-за того, что сортировка по умолчанию сравнивает значения кодовых точек, а не лингвистическое значение. Буква Å имеет кодовую точку U+00C5, которая больше, чем кодовая точка для z (U+007A). JavaScript не знает, что шведские носители языка считают Å отдельной буквой с определенной позицией в алфавите.
Смешанный регистр создает еще одну проблему.
const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// Вывод: ['Apple', 'Zoo', 'banana', 'zebra']
Все заглавные буквы имеют меньшие значения кодовых точек, чем строчные буквы. Это приводит к тому, что Apple и Zoo появляются перед banana, что не является алфавитным порядком ни в одном языке.
Как localeCompare сортирует строки по языковым правилам
Метод localeCompare() сравнивает две строки в соответствии с правилами сортировки для определённой локали. Он возвращает отрицательное число, если первая строка предшествует второй, ноль, если они эквивалентны, и положительное число, если первая строка следует за второй.
const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// Вывод: -1 (отрицательное значение означает, что 'a' предшествует 'b')
Вы можете использовать localeCompare() напрямую с Array.sort(), передавая его в качестве функции сравнения.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// Вывод: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
Шведская локаль размещает Anna и Bengt первыми, так как они используют стандартные латинские буквы. Затем идут Åsa, Ärla и Örjan с их особыми шведскими буквами в конце.
Тот же список, отсортированный с использованием немецкой локали, даёт другой результат.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// Вывод: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']
В немецком языке ä считается эквивалентной a для целей сортировки. Это размещает Ärla сразу после Anna, а не в конце, как в шведском языке.
Когда использовать localeCompare
Используйте localeCompare(), когда вам нужно отсортировать небольшой массив или сравнить две строки. Этот метод предоставляет простой API без необходимости создавать и управлять объектом collator.
const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// Вывод: ['apple', 'Banana', 'Cherry']
Этот подход хорошо работает для массивов с несколькими десятками элементов. Влияние на производительность незначительно для небольших наборов данных.
Вы также можете использовать localeCompare() для проверки, предшествует ли одна строка другой, без сортировки всего массива.
const firstName = 'Åsa';
const secondName = 'Anna';
if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
console.log(`${firstName} предшествует ${secondName}`);
} else {
console.log(`${secondName} предшествует ${firstName}`);
}
// Вывод: "Anna предшествует Åsa"
Это сравнение учитывает шведский алфавитный порядок без необходимости сортировать весь массив.
Как Intl.Collator улучшает производительность
Intl.Collator API создаёт повторно используемую функцию сравнения, оптимизированную для многократного использования. При сортировке больших массивов или выполнении множества сравнений collator значительно быстрее, чем вызов localeCompare() для каждого сравнения.
const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// Вывод: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
Свойство collator.compare возвращает функцию сравнения, которая работает напрямую с Array.sort(). Вам не нужно оборачивать её в стрелочную функцию.
Создание collator один раз и его повторное использование для нескольких операций позволяет избежать накладных расходов на поиск данных локали при каждом сравнении.
const collator = new Intl.Collator('de-DE');
const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);
const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);
console.log(sortedCities);
// Вывод: ['Berlin', 'Hamburg', 'Köln', 'München']
console.log(sortedNames);
// Вывод: ['Fischer', 'Müller', 'Schmidt', 'Schröder']
Один и тот же collator обрабатывает оба массива без необходимости создавать новый экземпляр.
Когда использовать Intl.Collator
Используйте Intl.Collator при сортировке массивов, содержащих сотни или тысячи элементов. Преимущество в производительности увеличивается с размером массива, так как функция сравнения вызывается множество раз во время сортировки.
const collator = new Intl.Collator('en-US');
const products = [/* массив с 10,000 названий продуктов */];
const sorted = products.sort(collator.compare);
Для массивов, содержащих более нескольких сотен элементов, collator может быть в несколько раз быстрее, чем localeCompare().
Также используйте Intl.Collator, если вам нужно сортировать несколько массивов с одной и той же локалью и параметрами. Создание collator один раз и его повторное использование устраняет повторные запросы данных локали.
const collator = new Intl.Collator('fr-FR');
const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];
const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);
Этот подход хорошо работает при создании таблиц или других интерфейсов, отображающих несколько отсортированных списков.
Как указать локаль
И localeCompare(), и Intl.Collator принимают идентификатор локали в качестве первого аргумента. Этот идентификатор использует формат BCP 47, который обычно сочетает код языка и необязательный код региона.
const names = ['Åsa', 'Anna', 'Ärla'];
// Шведская локаль
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Вывод: ['Anna', 'Åsa', 'Ärla']
// Немецкая локаль
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Вывод: ['Anna', 'Ärla', 'Åsa']
Локаль определяет, какие правила сортировки применяются. В шведском и немецком языках разные правила для букв å и ä, что приводит к различным порядкам сортировки.
Вы можете опустить локаль, чтобы использовать локаль по умолчанию, установленную в браузере пользователя.
const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);
Этот подход учитывает языковые предпочтения пользователя без жесткого указания конкретной локали. Порядок сортировки будет соответствовать ожиданиям пользователя на основе настроек его браузера.
Вы также можете передать массив локалей, чтобы указать варианты для резервного использования.
const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);
API использует первую поддерживаемую локаль из массива. Если шведский для Швеции недоступен, он попробует общий шведский, а затем перейдет к американскому английскому.
Как управлять чувствительностью к регистру
Опция sensitivity определяет, как сравнение учитывает различия в регистре и акцентах. Она принимает четыре значения: base, accent, case и variant.
Чувствительность base игнорирует как регистр, так и акценты, сравнивая только базовые символы.
const collator = new Intl.Collator('en-US', { sensitivity: 'base' });
console.log(collator.compare('a', 'A'));
// Вывод: 0 (равны)
console.log(collator.compare('a', 'á'));
// Вывод: 0 (равны)
console.log(collator.compare('a', 'b'));
// Вывод: -1 (разные базовые символы)
В этом режиме символы a, A и á считаются идентичными, так как у них одинаковый базовый символ.
Чувствительность accent учитывает акценты, но игнорирует регистр.
const collator = new Intl.Collator('en-US', { sensitivity: 'accent' });
console.log(collator.compare('a', 'A'));
// Вывод: 0 (равны, регистр игнорируется)
console.log(collator.compare('a', 'á'));
// Вывод: -1 (разные, акцент имеет значение)
Чувствительность case учитывает регистр, но игнорирует акценты.
const collator = new Intl.Collator('en-US', { sensitivity: 'case' });
console.log(collator.compare('a', 'A'));
// Вывод: -1 (разные, регистр имеет значение)
console.log(collator.compare('a', 'á'));
// Вывод: 0 (равны, акцент игнорируется)
Чувствительность variant (по умолчанию) учитывает все различия.
const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });
console.log(collator.compare('a', 'A'));
// Вывод: -1 (разные)
console.log(collator.compare('a', 'á'));
// Вывод: -1 (разные)
Этот режим обеспечивает самое строгое сравнение, при котором любое различие считается значимым.
Как сортировать строки с встроенными числами
numeric (числовая) опция включает числовую сортировку для строк, содержащих числа. При включении сравнение рассматривает последовательности цифр как числовые значения, а не сравнивает их посимвольно.
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
// Сортировка по умолчанию (неправильный порядок)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Вывод: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
// Числовая сортировка (правильный порядок)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Вывод: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
Без числовой сортировки строки сортируются посимвольно. Строка 10 идет перед 2, потому что первый символ 1 имеет меньший код, чем 2.
При включенной числовой сортировке компаратор распознает 10 как число десять, а 2 как число два. Это приводит к ожидаемому порядку сортировки, где 2 идет перед 10.
Эта опция полезна для сортировки имен файлов, номеров версий или любых строк, которые содержат текст и числа.
const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// Вывод: ['v1.2', 'v1.3', 'v1.10', 'v1.20']
Как управлять порядком регистра
Опция caseFirst определяет, какие буквы — заглавные или строчные — будут идти первыми при сравнении строк, которые различаются только регистром. Она принимает три значения: upper, lower или false.
const words = ['apple', 'Apple', 'APPLE'];
// Сначала заглавные буквы
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Вывод: ['APPLE', 'Apple', 'apple']
// Сначала строчные буквы
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Вывод: ['apple', 'Apple', 'APPLE']
// По умолчанию (зависит от локали)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Вывод зависит от локали
Значение false использует порядок регистра по умолчанию для локали. В большинстве локалей строки, которые различаются только регистром, считаются равными при использовании настроек чувствительности по умолчанию.
Эта опция влияет только в том случае, если опция sensitivity позволяет учитывать различия в регистре.
Как игнорировать знаки препинания при сортировке
Опция ignorePunctuation позволяет коллатору пропускать знаки препинания при сравнении строк. Это может быть полезно при сортировке заголовков или фраз, которые могут содержать или не содержать знаки препинания.
const titles = [
'The Old Man',
'The Old-Man',
'The Oldman',
];
// По умолчанию (знаки препинания учитываются)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Вывод: ['The Old Man', 'The Old-Man', 'The Oldman']
// Игнорировать знаки препинания
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Вывод: ['The Old Man', 'The Old-Man', 'The Oldman']
Когда знаки препинания игнорируются, сравнение обрабатывает дефис в "Old-Man" так, как будто его нет, что делает строки эквивалентными "TheOldMan".
Сортировка имен пользователей из разных стран
При сортировке имен пользователей со всего мира используйте предпочтительный язык пользователя, чтобы учитывать их лингвистические ожидания.
const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);
const users = [
{ name: 'Müller', country: 'Germany' },
{ name: 'Martin', country: 'France' },
{ name: 'Andersson', country: 'Sweden' },
{ name: 'García', country: 'Spain' },
];
const sorted = users.sort((a, b) => collator.compare(a.name, b.name));
sorted.forEach(user => {
console.log(`${user.name} (${user.country})`);
});
Этот код определяет язык пользователя из браузера и сортирует имена соответственно. Немецкий пользователь увидит список, отсортированный по немецким правилам, а шведский пользователь — по шведским.
Сортировка с переключением языка
Если ваше приложение позволяет пользователям переключать языки, обновляйте коллатор при изменении языка.
let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);
function setLocale(newLocale) {
currentLocale = newLocale;
collator = new Intl.Collator(currentLocale);
}
function sortItems(items) {
return items.sort(collator.compare);
}
// Пользователь переключается на шведский
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Вывод: ['Anna', 'Åsa', 'Örjan']
// Пользователь переключается на немецкий
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Вывод: ['Udo', 'Uhr', 'Über']
Этот подход гарантирует, что отсортированные списки обновляются в соответствии с выбранным пользователем языком.
Выбор между localeCompare и Intl.Collator
Используйте localeCompare(), когда вам нужно быстро выполнить одноразовое сравнение или отсортировать небольшой массив с менее чем 100 элементами. Более простой синтаксис легче читать, а разница в производительности незначительна для небольших наборов данных.
const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
Используйте Intl.Collator, если вы сортируете большие массивы, выполняете множество сравнений или сортируете несколько массивов с одинаковыми локалями и параметрами. Создание объекта collator один раз и его повторное использование обеспечивает лучшую производительность.
const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });
const products = [/* большой массив */];
const sorted = products.sort(collator.compare);
Оба подхода дают одинаковые результаты. Выбор зависит от ваших требований к производительности и предпочтений в организации кода.