Сравнение строк без учета диакритических знаков
Узнайте, как сравнивать строки, игнорируя диакритические знаки, с помощью нормализации JavaScript и Intl.Collator
Введение
При создании приложений, работающих с несколькими языками, часто возникает необходимость сравнивать строки, содержащие диакритические знаки. Пользователь, ищущий "cafe", должен находить результаты для "café". Проверка имени пользователя "Jose" должна совпадать с "José". Стандартное сравнение строк рассматривает их как разные строки, но логика вашего приложения должна считать их равными.
JavaScript предоставляет два подхода для решения этой проблемы. Вы можете нормализовать строки и удалить диакритические знаки или использовать встроенный API колlation для сравнения строк с определёнными правилами чувствительности.
Что такое диакритические знаки
Диакритические знаки — это символы, размещённые над, под или через буквы, чтобы изменить их произношение или значение. Эти знаки называются диакритиками. Распространённые примеры включают острый акцент в "é", тильду в "ñ" и умлаут в "ü".
В Unicode эти символы могут быть представлены двумя способами. Один кодовый пункт может представлять полный символ, или несколько кодовых пунктов могут комбинировать базовую букву с отдельным диакритическим знаком. Буква "é" может быть сохранена как U+00E9 или как "e" (U+0065) плюс комбинированный острый акцент (U+0301).
Когда игнорировать диакритические знаки при сравнении
Функциональность поиска — это самый распространённый случай использования сравнения, нечувствительного к диакритическим знакам. Пользователи, вводящие запросы без диакритических знаков, ожидают найти контент, содержащий символы с диакритиками. Поиск "Muller" должен находить "Müller".
Проверка пользовательского ввода требует этой возможности при проверке, существуют ли уже имена пользователей, адреса электронной почты или другие идентификаторы. Вы хотите предотвратить создание дублирующих аккаунтов для "maria" и "maría".
Сравнения, нечувствительные к регистру, часто требуют игнорирования диакритических знаков одновременно. При проверке совпадения двух строк независимо от регистра обычно также нужно игнорировать различия в диакритических знаках.
Удаление акцентных знаков с использованием нормализации
Первый подход преобразует строки в нормализованную форму, где базовые буквы и акцентные знаки разделены, а затем удаляет акцентные знаки.
Нормализация Unicode преобразует строки в стандартную форму. Форма NFD (каноническая декомпозиция) разделяет комбинированные символы на их базовые буквы и комбинирующие знаки. Строка "café" становится "cafe" с последующим комбинирующим символом острого акцента.
После нормализации вы можете удалить комбинирующие знаки с помощью регулярного выражения. Диапазон Unicode U+0300 до U+036F содержит комбинирующие диакритические знаки.
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const text1 = 'café';
const text2 = 'cafe';
const normalized1 = removeAccents(text1);
const normalized2 = removeAccents(text2);
console.log(normalized1 === normalized2); // true
console.log(normalized1); // "cafe"
Этот метод позволяет получить строки без акцентных знаков, которые можно сравнивать с использованием стандартных операторов равенства.
Вы можете комбинировать это с преобразованием в нижний регистр для нечувствительных к регистру и акцентам сравнений.
function normalizeForComparison(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
const search = 'muller';
const name = 'Müller';
console.log(normalizeForComparison(search) === normalizeForComparison(name)); // true
Этот подход хорошо работает, если вам нужно хранить или индексировать нормализованную версию строк для эффективного поиска.
Сравнение строк с использованием Intl.Collator
Второй подход использует API Intl.Collator, который обеспечивает сравнение строк с учетом локали и настраиваемыми уровнями чувствительности.
Объект Intl.Collator сравнивает строки в соответствии с языковыми правилами. Опция чувствительности (sensitivity) определяет, какие различия учитываются при сравнении строк.
Уровень чувствительности "base" игнорирует как акцентные знаки, так и различия в регистре. Строки, которые отличаются только акцентами или заглавными буквами, считаются равными.
const collator = new Intl.Collator('en', { sensitivity: 'base' });
console.log(collator.compare('café', 'cafe')); // 0 (равны)
console.log(collator.compare('Café', 'cafe')); // 0 (равны)
console.log(collator.compare('café', 'caff')); // -1 (первая строка идет перед второй)
Метод compare возвращает 0, если строки равны, отрицательное число, если первая строка идет перед второй, и положительное число, если первая строка идет после второй.
Вы можете использовать это для проверки равенства или для сортировки массивов.
const collator = new Intl.Collator('en', { sensitivity: 'base' });
function areEqualIgnoringAccents(str1, str2) {
return collator.compare(str1, str2) === 0;
}
console.log(areEqualIgnoringAccents('José', 'Jose')); // true
console.log(areEqualIgnoringAccents('naïve', 'naive')); // true
Для сортировки вы можете передать метод compare напрямую в Array.sort.
const names = ['Müller', 'Martinez', 'Muller', 'Márquez'];
const collator = new Intl.Collator('en', { sensitivity: 'base' });
names.sort(collator.compare);
console.log(names); // Группирует варианты вместе
API Intl.Collator предоставляет другие уровни чувствительности для различных случаев использования.
Уровень "accent" игнорирует регистр, но учитывает различия в акцентах. "Café" равно "café", но не "cafe".
const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (равны)
console.log(accentCollator.compare('café', 'cafe')); // 1 (не равны)
Уровень "case" игнорирует акценты, но учитывает различия в регистре. "café" равно "cafe", но не "Café".
const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (равны)
console.log(caseCollator.compare('café', 'Café')); // -1 (не равны)
Уровень "variant" учитывает все различия. Это поведение по умолчанию.
const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (не равны)
Выбор между нормализацией и упорядочиванием
Оба метода дают корректные результаты для сравнения без учета акцентов, но имеют разные характеристики.
Метод нормализации создает новые строки без акцентных знаков. Используйте этот подход, если вам нужно сохранить или индексировать нормализованные версии. Поисковые системы и базы данных часто хранят нормализованный текст для эффективного поиска.
Метод Intl.Collator сравнивает строки, не изменяя их. Используйте этот подход, если вам нужно сравнивать строки напрямую, например, для проверки на дубликаты или сортировки списков. Collator учитывает языковые правила сортировки, которые не может обработать простое строковое сравнение.
Производительность зависит от конкретного случая использования. Создание объекта collator один раз и его повторное использование эффективно для множества сравнений. Нормализация строк эффективна, если вы нормализуете один раз и сравниваете много раз.
Метод нормализации удаляет информацию об акцентах навсегда. Метод упорядочивания сохраняет оригинальные строки, сравнивая их в соответствии с указанными вами правилами.
Фильтрация массивов с использованием поиска без учета акцентов
Распространенный случай использования — фильтрация массива элементов на основе пользовательского ввода, игнорируя различия в акцентах.
const products = [
{ name: 'Café Latte', price: 4.50 },
{ name: 'Crème Brûlée', price: 6.00 },
{ name: 'Croissant', price: 3.00 },
{ name: 'Café Mocha', price: 5.00 }
];
function searchProducts(query) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return products.filter(product => {
return collator.compare(product.name.slice(0, query.length), query) === 0;
});
}
console.log(searchProducts('cafe'));
// Возвращает Café Latte и Café Mocha
Для поиска подстрок метод нормализации работает лучше.
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function searchProducts(query) {
const normalizedQuery = removeAccents(query.toLowerCase());
return products.filter(product => {
const normalizedName = removeAccents(product.name.toLowerCase());
return normalizedName.includes(normalizedQuery);
});
}
console.log(searchProducts('creme'));
// Возвращает Crème Brûlée
Этот подход проверяет, содержит ли нормализованное имя продукта нормализованный поисковый запрос в качестве подстроки.
Обработка текстового ввода для совпадений
При проверке пользовательского ввода на соответствие существующим данным необходимо использовать сравнение, нечувствительное к акцентам, чтобы избежать путаницы и дублирования.
const existingUsernames = ['José', 'María', 'François'];
function isUsernameTaken(username) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return existingUsernames.some(existing =>
collator.compare(existing, username) === 0
);
}
console.log(isUsernameTaken('jose')); // true
console.log(isUsernameTaken('Maria')); // true
console.log(isUsernameTaken('francois')); // true
console.log(isUsernameTaken('pierre')); // false
Это предотвращает создание пользователями учетных записей с именами, которые отличаются от существующих только акцентами или регистром.
Поддержка браузеров и окружений
Метод String.prototype.normalize поддерживается во всех современных браузерах и средах Node.js. Internet Explorer не поддерживает этот метод.
API Intl.Collator поддерживается во всех современных браузерах и версиях Node.js. Internet Explorer 11 включает частичную поддержку.
Оба подхода работают надежно в текущих JavaScript-окружениях. Если вам нужно поддерживать старые браузеры, потребуется использовать полифилы или альтернативные реализации.
Ограничения удаления акцентов
В некоторых языках диакритические знаки используются для создания отдельных букв, а не просто вариаций акцентов. В турецком языке "i" и "ı" — это разные буквы. В немецком языке "ö" — это отдельная гласная, а не акцентированная "o".
Удаление акцентов изменяет значение в таких случаях. Учитывайте, подходит ли сравнение, нечувствительное к акцентам, для вашего случая использования и целевых языков.
Подход с использованием колляции лучше справляется с такими случаями, так как он следует правилам, специфичным для локали. Указание правильной локали в конструкторе Intl.Collator обеспечивает культурно корректные сравнения.
const turkishCollator = new Intl.Collator('tr', { sensitivity: 'base' });
const germanCollator = new Intl.Collator('de', { sensitivity: 'base' });
Всегда учитывайте языки, которые поддерживает ваше приложение, при выборе стратегии сравнения.