Как сравнивать строки, игнорируя различия в регистре
Используйте сравнение с учетом локали, чтобы правильно обрабатывать регистронезависимое сопоставление для разных языков
Введение
Сравнение строк без учета регистра часто встречается в веб-приложениях. Пользователи вводят поисковые запросы в смешанном регистре, вводят имена пользователей с непоследовательным использованием заглавных букв или заполняют формы, не обращая внимания на регистр букв. Ваше приложение должно правильно сопоставлять эти вводы, независимо от того, используют ли пользователи заглавные, строчные или смешанные буквы.
Простой подход заключается в преобразовании обеих строк в строчные буквы и их сравнении. Это работает для английского текста, но не подходит для международных приложений. В разных языках существуют разные правила преобразования между заглавными и строчными буквами. Метод сравнения, который работает для английского, может давать неверные результаты для турецкого, немецкого, греческого или других языков.
JavaScript предоставляет API Intl.Collator для правильного выполнения сравнения без учета регистра во всех языках. В этом уроке объясняется, почему простое преобразование в строчные буквы не работает, как работает сравнение с учетом локали и когда использовать каждый из подходов.
Простой подход с использованием toLowerCase
Преобразование обеих строк в строчные буквы перед сравнением — это самый распространенный подход для сопоставления без учета регистра:
const str1 = "Hello";
const str2 = "HELLO";
console.log(str1.toLowerCase() === str2.toLowerCase());
// true
Этот метод работает для текста в кодировке ASCII и английских слов. Сравнение рассматривает заглавные и строчные версии одной и той же буквы как идентичные.
Вы можете использовать этот подход для нечеткого поиска:
const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];
const matches = items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
console.log(matches);
// ["Apple", "APPLE PIE"]
Фильтр находит все элементы, содержащие поисковый запрос, независимо от регистра. Это обеспечивает ожидаемое поведение для пользователей, которые вводят запросы, не задумываясь о регистре.
Почему простой подход не работает для международного текста
Метод toLowerCase() преобразует текст в соответствии с правилами Unicode, но эти правила работают не одинаково во всех языках. Самый известный пример — проблема с турецкой буквой i.
В английском языке строчная буква i преобразуется в заглавную I. В турецком языке существуют две разные буквы:
- Строчная буква с точкой
iпреобразуется в заглавную с точкойİ - Строчная буква без точки
ıпреобразуется в заглавную без точкиI
Эта разница нарушает регистронезависимое сравнение:
const word1 = "file";
const word2 = "FILE";
// В английской локали (корректно)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true
// В турецкой локали (некорректно)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" становится "fıle"
При преобразовании FILE в строчные буквы с использованием турецких правил, I становится ı (без точки), что приводит к fıle. Это не совпадает с file (с точкой над i), поэтому сравнение возвращает false, даже если строки представляют одно и то же слово.
В других языках также есть подобные проблемы. В немецком языке символ ß преобразуется в заглавные SS. В греческом языке есть несколько строчных форм сигмы (σ и ς), которые обе преобразуются в заглавную Σ. Простое преобразование регистра не может корректно обработать эти языковые особенности.
Использование Intl.Collator с базовой чувствительностью для регистронезависимого сравнения
API Intl.Collator предоставляет возможность сравнения строк с учетом локали и настраиваемой чувствительностью. Опция sensitivity определяет, какие различия имеют значение при сравнении.
Для регистронезависимого сравнения используйте sensitivity: "base":
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("Hello", "hello"));
// 0 (строки равны)
console.log(collator.compare("Hello", "HELLO"));
// 0 (строки равны)
console.log(collator.compare("Hello", "Héllo"));
// 0 (строки равны, акценты также игнорируются)
Базовая чувствительность игнорирует различия в регистре и акцентах. Учитываются только базовые буквы. Сравнение возвращает 0, если строки эквивалентны на этом уровне чувствительности.
Этот подход корректно обрабатывает проблему с турецкой буквой i:
const collator = new Intl.Collator("tr", { sensitivity: "base" });
console.log(collator.compare("file", "FILE"));
// 0 (корректное совпадение)
console.log(collator.compare("file", "FİLE"));
// 0 (корректное совпадение, даже с точкой над İ)
Collator автоматически применяет турецкие правила преобразования регистра. Оба сравнения распознают строки как эквивалентные, независимо от того, какая заглавная буква I используется во входных данных.
Использование localeCompare с опцией sensitivity
Метод localeCompare() предоставляет альтернативный способ выполнения регистронезависимого сравнения. Он принимает те же параметры, что и Intl.Collator:
const str1 = "Hello";
const str2 = "HELLO";
console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (строки равны)
Это дает тот же результат, что и использование Intl.Collator с базовой чувствительностью. Сравнение игнорирует различия в регистре и возвращает 0 для эквивалентных строк.
Вы можете использовать это для фильтрации массивов:
const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];
const matches = items.filter(item =>
item.localeCompare(query, "en", { sensitivity: "base" }) === 0 ||
item.toLowerCase().includes(query.toLowerCase())
);
console.log(matches);
// ["Apple"]
Однако localeCompare() возвращает 0 только для точных совпадений на указанном уровне чувствительности. Он не поддерживает частичное совпадение, как includes(). Для поиска подстрок вам все равно нужно использовать преобразование в нижний регистр или реализовать более сложный алгоритм поиска.
Выбор между базовой и акцентной чувствительностью
Опция sensitivity принимает четыре значения, которые контролируют различные аспекты сравнения строк:
Базовая чувствительность
Базовая чувствительность игнорирует как регистр, так и акценты:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("cafe", "café"));
// 0 (акценты игнорируются)
console.log(collator.compare("cafe", "Café"));
// 0 (регистр и акценты игнорируются)
console.log(collator.compare("cafe", "CAFÉ"));
// 0 (регистр и акценты игнорируются)
Это обеспечивает наиболее мягкое соответствие. Пользователи, которые не могут вводить символы с акцентами или пропускают их для удобства, все равно получают правильные совпадения.
Акцентная чувствительность
Акцентная чувствительность игнорирует регистр, но учитывает акценты:
const collator = new Intl.Collator("en", { sensitivity: "accent" });
console.log(collator.compare("cafe", "café"));
// -1 (акценты имеют значение)
console.log(collator.compare("cafe", "Café"));
// -1 (акценты имеют значение, регистр игнорируется)
console.log(collator.compare("Café", "CAFÉ"));
// 0 (регистр игнорируется, акценты совпадают)
Это рассматривает акцентированные и неакцентированные буквы как разные, игнорируя регистр. Используйте это, когда различия в акцентах важны, но различия в регистре не имеют значения.
Выбор подходящей чувствительности для вашего случая использования
Для большинства задач, не требующих учета регистра, базовая чувствительность обеспечивает наилучший пользовательский опыт:
- Функционал поиска, где пользователи вводят запросы без учета акцентов
- Сопоставление имен пользователей, где регистр не должен иметь значения
- Нечеткий поиск, где требуется максимальная гибкость
- Проверка форм, где
Smithиsmithдолжны совпадать
Используйте чувствительность к акцентам, когда:
- Язык требует различения символов с акцентами
- Ваши данные содержат как акцентированные, так и неакцентированные версии с разным значением
- Вам нужно сравнение, не зависящее от регистра, но учитывающее акценты
Выполнение поиска без учета регистра с использованием includes
API Intl.Collator сравнивает полные строки, но не предоставляет возможности поиска подстрок. Для поиска без учета регистра вам все равно нужно комбинировать сравнение с учетом локали с другими подходами.
Один из вариантов — использовать toLowerCase() для поиска подстрок, принимая его ограничения для международного текста:
function caseInsensitiveIncludes(text, query, locale = "en") {
return text.toLowerCase().includes(query.toLowerCase());
}
const text = "The Quick Brown Fox";
console.log(caseInsensitiveIncludes(text, "quick"));
// true
Для более сложного поиска, который корректно обрабатывает международный текст, вам нужно перебирать возможные позиции подстрок и использовать collator для каждого сравнения:
function localeAwareIncludes(text, query, locale = "en") {
const collator = new Intl.Collator(locale, { sensitivity: "base" });
for (let i = 0; i <= text.length - query.length; i++) {
const substring = text.slice(i, i + query.length);
if (collator.compare(substring, query) === 0) {
return true;
}
}
return false;
}
const text = "The Quick Brown Fox";
console.log(localeAwareIncludes(text, "quick"));
// true
Этот подход проверяет каждую возможную подстроку нужной длины и использует сравнение с учетом локали для каждой. Он корректно обрабатывает международный текст, но имеет худшую производительность по сравнению с простым includes().
Особенности производительности при использовании Intl.Collator
Создание экземпляра Intl.Collator включает загрузку данных локали и обработку параметров. Если вам нужно выполнить несколько сравнений, создайте экземпляр collator один раз и используйте его повторно:
// Неэффективно: создается collator для каждого сравнения
function badCompare(items, target) {
return items.filter(item =>
new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
);
}
// Эффективно: создается collator один раз и используется повторно
function goodCompare(items, target) {
const collator = new Intl.Collator("en", { sensitivity: "base" });
return items.filter(item =>
collator.compare(item, target) === 0
);
}
Эффективная версия создает collator один раз перед фильтрацией. Каждое сравнение использует один и тот же экземпляр, избегая повторной инициализации.
Для приложений, которые часто выполняют сравнения, создавайте экземпляры collator при запуске приложения и экспортируйте их для использования в коде:
// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "base"
});
export const accentInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "accent"
});
// В вашем коде приложения
import { caseInsensitiveCollator } from "./utils/collation";
const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;
Этот подход максимизирует производительность и обеспечивает единообразное поведение сравнения во всем приложении.
Когда использовать toLowerCase вместо Intl.Collator
Для приложений, работающих только с английским языком, где вы контролируете текстовый контент и знаете, что он содержит только символы ASCII, toLowerCase() обеспечивает приемлемые результаты:
// Приемлемо для текста только на английском языке и только с символами ASCII
const isMatch = str1.toLowerCase() === str2.toLowerCase();
Этот подход прост, быстр и знаком большинству разработчиков. Если ваше приложение действительно никогда не обрабатывает международный текст, дополнительная сложность локализованного сравнения может быть излишней.
Для международных приложений или приложений, где пользователи вводят текст на любом языке, используйте Intl.Collator с соответствующей чувствительностью:
// Необходимо для международного текста
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;
Это гарантирует корректное поведение независимо от того, на каком языке говорят или печатают пользователи. Небольшие затраты на производительность при использовании Intl.Collator оправданы для предотвращения некорректных сравнений.
Даже если ваше приложение в настоящее время поддерживает только английский язык, использование локализованного сравнения с самого начала упрощает будущую интернационализацию. Добавление поддержки новых языков не потребует изменений в логике сравнения.
Практические примеры использования регистронезависимого сравнения
Регистронезависимое сравнение встречается во многих распространенных сценариях:
Сопоставление имен пользователей и адресов электронной почты
Пользователи вводят имена и адреса электронной почты с разным использованием заглавных и строчных букв:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function findUserByEmail(users, email) {
return users.find(user =>
collator.compare(user.email, email) === 0
);
}
const users = [
{ email: "[email protected]", name: "John" },
{ email: "[email protected]", name: "Jane" }
];
console.log(findUserByEmail(users, "[email protected]"));
// { email: "[email protected]", name: "John" }
Это находит пользователя независимо от того, как они ввели адрес электронной почты — с заглавными или строчными буквами.
Автозаполнение поиска
Предложения автозаполнения должны соответствовать частичному вводу без учета регистра:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function getSuggestions(items, query) {
const queryLower = query.toLowerCase();
return items.filter(item =>
item.toLowerCase().startsWith(queryLower)
);
}
const items = ["Apple", "Apricot", "Banana", "Cherry"];
console.log(getSuggestions(items, "ap"));
// ["Apple", "Apricot"]
Это предоставляет предложения независимо от регистра, в котором пользователи вводят текст.
Сопоставление тегов и категорий
Пользователи назначают теги или категории контенту без соблюдения единого регистра:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function hasTag(item, tag) {
return item.tags.some(itemTag =>
collator.compare(itemTag, tag) === 0
);
}
const article = {
title: "My Article",
tags: ["JavaScript", "Tutorial", "Web Development"]
};
console.log(hasTag(article, "javascript"));
// true
Это сопоставляет теги независимо от различий в регистре.