Как правильно сортировать строки с встроенными числами
Используйте числовую сортировку для упорядочивания имен файлов, номеров версий и других строк, содержащих числа, в естественном порядке
Введение
Когда вы сортируете строки, содержащие числа, вы ожидаете, что file1.txt, file2.txt и file10.txt будут расположены именно в таком порядке. Однако стандартное сравнение строк в JavaScript приводит к порядку file1.txt, file10.txt, file2.txt. Это происходит потому, что строки сравниваются посимвольно, и символ 1 в 10 идет раньше символа 2.
Эта проблема возникает всякий раз, когда вы сортируете имена файлов, номера версий, адреса улиц, коды продуктов или любые другие строки с встроенными числами. Неправильный порядок сбивает пользователей с толку и затрудняет навигацию по данным.
JavaScript предоставляет API Intl.Collator с опцией numeric, которая решает эту проблему. В этом уроке объясняется, как работает числовая сортировка, почему стандартное сравнение строк не справляется и как сортировать строки с числами в естественном числовом порядке.
Что такое числовая сортировка
Числовая сортировка — это метод сравнения, который рассматривает последовательности цифр как числа, а не как отдельные символы. При сравнении строк коллатор определяет последовательности цифр и сравнивает их по их числовому значению.
Когда числовая сортировка отключена, строка file10.txt идет перед file2.txt, потому что посимвольное сравнение показывает, что 1 идет перед 2 в первой различающейся позиции. Коллатор не учитывает, что 10 представляет число, большее, чем 2.
Когда числовая сортировка включена, коллатор распознает 10 и 2 как целые числа и сравнивает их численно. Поскольку 10 больше 2, file2.txt правильно идет перед file10.txt.
Такое поведение обеспечивает так называемую естественную сортировку или естественный порядок, при котором строки с числами сортируются так, как это ожидают люди, а не строго в алфавитном порядке.
Почему стандартное сравнение строк не работает для чисел
Стандартное сравнение строк в JavaScript использует лексикографический порядок, который сравнивает строки посимвольно слева направо, используя значения кодовых точек Unicode. Это работает корректно для алфавитного текста, но приводит к неожиданным результатам при работе с числами.
Рассмотрим, как лексикографическое сравнение обрабатывает эти строки:
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort();
console.log(files);
// Вывод: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
Сравнение анализирует каждую позицию символа независимо. На первой различающейся позиции после file сравниваются 1 и 2. Поскольку 1 имеет более низкое значение Unicode, чем 2, любая строка, начинающаяся с file1, будет идти перед любой строкой, начинающейся с file2, независимо от того, что следует дальше.
Это приводит к последовательности file1.txt, file10.txt, file2.txt, file20.txt, что нарушает человеческие ожидания относительно порядка чисел.
Использование Intl.Collator с опцией numeric
Конструктор Intl.Collator принимает объект с опциями, содержащий свойство numeric. Установка numeric: true включает числовую сортировку, заставляя коллатор сравнивать последовательности цифр по их числовому значению.
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort(collator.compare);
console.log(files);
// Вывод: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
Метод compare коллатора возвращает отрицательное число, если первый аргумент должен идти перед вторым, ноль, если они равны, и положительное число, если первый должен идти после второго. Это соответствует сигнатуре, ожидаемой методом Array.sort() в JavaScript.
Отсортированный результат располагает файлы в естественном числовом порядке. Коллатор распознает, что 1 < 2 < 10 < 20, создавая последовательность, ожидаемую человеком.
Сортировка смешанных строк с буквами и цифрами
Числовая сортировка обрабатывает строки, где числа могут находиться в любом месте, а не только в конце. Сравнение выполняется по алфавитным частям как обычно, а числовые части сравниваются численно.
const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];
addresses.sort(collator.compare);
console.log(addresses);
// Вывод: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']
Сравниватель определяет последовательности цифр в начале каждой строки и сравнивает их численно. Он понимает, что 5 < 45 < 123 < 1234, даже если лексикографическое сравнение дало бы другой порядок.
Сортировка номеров версий
Номера версий — это распространённый случай использования числовой сортировки. Например, версии программного обеспечения, такие как 1.2.10, должны следовать за 1.2.2, но лексикографическое сравнение даёт неправильный порядок.
const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];
versions.sort(collator.compare);
console.log(versions);
// Вывод: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']
Сравниватель корректно сравнивает каждую числовую часть. В последовательности 1.2.2, 1.2.5, 1.2.10 он понимает, что третья часть увеличивается численно. В 1.10.5 он понимает, что вторая часть равна 10, что больше, чем 2.
Работа с кодами продуктов и идентификаторами
Коды продуктов, номера счетов и другие идентификаторы часто содержат смесь букв и цифр. Числовая сортировка обеспечивает их логический порядок.
const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];
products.sort(collator.compare);
console.log(products);
// Вывод: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']
Алфавитный префикс PROD- совпадает во всех строках, поэтому сравниватель сравнивает числовой суффикс. Результат отражает возрастающий числовой порядок, а не лексикографический.
Сортировка с использованием разных локалей
numeric (числовая) опция работает с любой локалью. Хотя разные локали могут иметь разные правила сортировки для алфавитных символов, поведение числового сравнения остается неизменным.
const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });
const items = ['item1', 'item10', 'item2'];
console.log(items.sort(enCollator.compare));
// Вывод: ['item1', 'item2', 'item10']
console.log(items.sort(deCollator.compare));
// Вывод: ['item1', 'item2', 'item10']
Обе локали дают одинаковый результат, так как строки содержат только ASCII-символы и числа. Когда строки включают символы, специфичные для локали, алфавитное сравнение следует правилам локали, в то время как числовое сравнение остается неизменным.
Сравнение строк без сортировки
Вы можете использовать метод compare коллатора напрямую, чтобы определить отношение между двумя строками без сортировки всего массива.
const collator = new Intl.Collator('en-US', { numeric: true });
console.log(collator.compare('file2.txt', 'file10.txt'));
// Вывод: -1 (отрицательное число означает, что первый аргумент идет перед вторым)
console.log(collator.compare('file10.txt', 'file2.txt'));
// Вывод: 1 (положительное число означает, что первый аргумент идет после второго)
console.log(collator.compare('file2.txt', 'file2.txt'));
// Вывод: 0 (ноль означает, что аргументы равны)
Это полезно, когда вам нужно проверить порядок без изменения массива, например, при вставке элемента в отсортированный список или проверке, попадает ли значение в диапазон.
Понимание ограничения с десятичными числами
Числовая колляция сравнивает последовательности цифр, но не распознает десятичные точки как часть чисел. Символ точки рассматривается как разделитель, а не как десятичный разделитель.
const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];
measurements.sort(collator.compare);
console.log(measurements);
// Вывод: ['0.005', '0.05', '0.5']
Коллятор рассматривает каждое измерение как три отдельных числовых компонента: часть перед точкой, сама точка и часть после точки. Он сравнивает 0 с 0 (равны), затем сравнивает части после точки как отдельные числа: 5, 5 и 5 (равны). Затем он сравнивает вторую десятичную позицию: ничего, 5 и ничего. Это приводит к неправильному порядку для десятичных чисел.
Для сортировки десятичных чисел преобразуйте их в настоящие числа и сортируйте численно или используйте дополнение строк, чтобы обеспечить правильный лексикографический порядок.
Комбинирование числовой сортировки с другими опциями
numeric опция работает вместе с другими параметрами сортировки, такими как sensitivity и caseFirst. Вы можете управлять тем, как компаратор обрабатывает регистр и акценты, сохраняя при этом поведение числового сравнения.
const collator = new Intl.Collator('en-US', {
numeric: true,
sensitivity: 'base'
});
const items = ['Item1', 'item10', 'ITEM2'];
items.sort(collator.compare);
console.log(items);
// Вывод: ['Item1', 'ITEM2', 'item10']
Опция sensitivity: 'base' делает сравнение нечувствительным к регистру. Компаратор рассматривает Item1, item1 и ITEM1 как эквивалентные, при этом корректно сравнивая числовые части.
Повторное использование компараторов для повышения производительности
Создание нового экземпляра Intl.Collator включает загрузку данных локали и обработку параметров. Если вам нужно сортировать несколько массивов или выполнять множество сравнений, создайте компаратор один раз и используйте его повторно.
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];
files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);
Этот подход более эффективен, чем создание нового компаратора для каждой операции сортировки. Разница в производительности становится значительной при сортировке большого количества массивов или частых сравнениях.