大文字小文字の違いを無視して文字列を比較する方法
ロケールを考慮した比較を使用して、言語間で大文字小文字を区別しないマッチングを正しく処理する
はじめに
大文字小文字を区別しない文字列比較は、Webアプリケーションで頻繁に使用されます。ユーザーは検索クエリを大文字小文字混在で入力したり、ユーザー名を一貫性のない大文字小文字で入力したり、文字の大文字小文字を気にせずフォームに入力したりします。アプリケーションは、ユーザーが大文字、小文字、または混在で入力したかに関係なく、これらの入力を正しくマッチングする必要があります。
単純なアプローチは、両方の文字列を小文字に変換して比較することです。これは英語のテキストでは機能しますが、国際的なアプリケーションでは失敗します。言語によって、大文字と小文字の変換ルールが異なります。英語で機能する比較方法は、トルコ語、ドイツ語、ギリシャ語、その他の言語では誤った結果を生成する可能性があります。
JavaScriptは、すべての言語で大文字小文字を区別しない比較を正しく処理するためにIntl.Collator APIを提供しています。このレッスンでは、単純な小文字変換が失敗する理由、ロケールを考慮した比較がどのように機能するか、各アプローチをいつ使用するかについて説明します。
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 に変換されます。トルコ語では、2つの異なる文字が存在します。
- ドット付き小文字
iはドット付き大文字İに変換されます - ドットなし小文字
ıはドットなし大文字Iに変換されます
この違いにより、大文字小文字を区別しない比較が機能しなくなります。
const word1 = "file";
const word2 = "FILE";
// In English locale (correct)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true
// In Turkish locale (incorrect)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" becomes "fıle"
FILE をトルコ語のルールで小文字に変換すると、I は ı(ドットなし)になり、fıle が生成されます。これは file(ドット付きi)と一致しないため、文字列が同じ単語を表していても比較結果はfalseを返します。
他の言語にも同様の問題があります。ドイツ語には ß という文字があり、大文字では SS になります。ギリシャ語にはシグマの小文字形が複数あり(σ と ς)、どちらも大文字では Σ になります。単純な大文字小文字変換では、これらの言語固有のルールを正しく処理できません。
base sensitivityを使用したIntl.Collatorによる大文字小文字を区別しない比較
Intl.Collator APIは、設定可能な感度を持つロケール対応の文字列比較を提供します。sensitivity オプションは、比較時にどの違いを考慮するかを制御します。
大文字小文字を区別しない比較には、sensitivity: "base" を使用します。
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("Hello", "hello"));
// 0 (strings are equal)
console.log(collator.compare("Hello", "HELLO"));
// 0 (strings are equal)
console.log(collator.compare("Hello", "Héllo"));
// 0 (strings are equal, accents ignored too)
base sensitivityは、大文字小文字とアクセント記号の両方の違いを無視します。基本文字のみが重要になります。この感度レベルで文字列が等価である場合、比較結果は0を返します。
このアプローチは、トルコ語のiの問題を正しく処理します。
const collator = new Intl.Collator("tr", { sensitivity: "base" });
console.log(collator.compare("file", "FILE"));
// 0 (correctly matches)
console.log(collator.compare("file", "FİLE"));
// 0 (correctly matches, even with dotted İ)
collatorはトルコ語の大文字小文字変換ルールを自動的に適用します。入力にどちらの大文字Iが含まれていても、両方の比較で文字列が等価であると認識されます。
sensitivityオプションを使用したlocaleCompare
localeCompare()メソッドは、大文字小文字を区別しない比較を実行する代替方法を提供します。Intl.Collatorと同じオプションを受け入れます:
const str1 = "Hello";
const str2 = "HELLO";
console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (strings are equal)
これは、base sensitivityで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()は、指定されたsensitivityレベルでの完全一致に対してのみ0を返します。includes()のような部分一致はサポートしていません。部分文字列検索には、小文字変換を使用するか、より高度な検索アルゴリズムを実装する必要があります。
baseとaccent sensitivityの選択
sensitivityオプションは、文字列比較のさまざまな側面を制御する4つの値を受け入れます:
Base sensitivity
Base sensitivityは、大文字小文字とアクセント記号の両方を無視します:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("cafe", "café"));
// 0 (accents ignored)
console.log(collator.compare("cafe", "Café"));
// 0 (case and accents ignored)
console.log(collator.compare("cafe", "CAFÉ"));
// 0 (case and accents ignored)
これは最も寛容なマッチングを提供します。アクセント記号付き文字を入力できないユーザーや、便宜上それらを省略するユーザーでも、正しいマッチングが得られます。
Accent sensitivity
Accent sensitivityは、大文字小文字を無視しますが、アクセント記号は考慮します:
const collator = new Intl.Collator("en", { sensitivity: "accent" });
console.log(collator.compare("cafe", "café"));
// -1 (accents matter)
console.log(collator.compare("cafe", "Café"));
// -1 (accents matter, case ignored)
console.log(collator.compare("Café", "CAFÉ"));
// 0 (case ignored, accents match)
これは、大文字小文字の違いを無視しながら、アクセント記号付き文字とアクセント記号なし文字を異なるものとして扱います。アクセント記号の違いが重要だが、大文字小文字の違いは重要でない場合に使用します。
ユースケースに適したsensitivityの選択
ほとんどの大文字小文字を区別しない比較のニーズには、base sensitivityが最高のユーザーエクスペリエンスを提供します:
- ユーザーがアクセント記号なしでクエリを入力する検索機能
- 大文字小文字が問題にならないユーザー名のマッチング
- 最大限の柔軟性が必要なあいまい検索
Smithとsmithが一致すべきフォーム検証
アクセント感度を使用する場合:
- 言語がアクセント付き文字の区別を必要とする場合
- データにアクセント付きとアクセントなしの両方のバージョンが含まれ、異なる意味を持つ場合
- 大文字小文字を区別しないが、アクセントを認識する比較が必要な場合
includesを使用した大文字小文字を区別しない検索の実行
Intl.Collator APIは完全な文字列を比較しますが、部分文字列のマッチングは提供しません。大文字小文字を区別しない検索の場合、ロケールを考慮した比較を他のアプローチと組み合わせる必要があります。
1つのオプションは、部分文字列検索に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
国際的なテキストを正しく処理する、より洗練された検索を行うには、可能な部分文字列の位置を反復処理し、各比較にコレーターを使用する必要があります:
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インスタンスの作成には、ロケールデータの読み込みとオプションの処理が含まれます。複数の比較を実行する必要がある場合は、コレーターを一度作成して再利用します:
// Inefficient: creates collator for every comparison
function badCompare(items, target) {
return items.filter(item =>
new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
);
}
// Efficient: creates collator once, reuses it
function goodCompare(items, target) {
const collator = new Intl.Collator("en", { sensitivity: "base" });
return items.filter(item =>
collator.compare(item, target) === 0
);
}
効率的なバージョンは、フィルタリングの前にコレーターを一度作成します。各比較は同じインスタンスを使用し、繰り返しの初期化オーバーヘッドを回避します。
頻繁に比較を実行するアプリケーションの場合、アプリケーションの起動時にコレーターインスタンスを作成し、コードベース全体で使用するためにエクスポートします:
// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "base"
});
export const accentInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "accent"
});
// In your application code
import { caseInsensitiveCollator } from "./utils/collation";
const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;
このパターンは、パフォーマンスを最大化し、アプリケーション全体で一貫した比較動作を維持します。
toLowerCaseとIntl.Collatorのどちらを使用するか
テキストコンテンツを制御し、ASCII文字のみが含まれていることがわかっている英語のみのアプリケーションの場合、toLowerCase()は許容可能な結果を提供します:
// Acceptable for English-only, ASCII-only text
const isMatch = str1.toLowerCase() === str2.toLowerCase();
このアプローチはシンプルで高速であり、ほとんどの開発者にとって馴染み深いものです。アプリケーションが国際的なテキストを扱わない場合、ロケールを考慮した比較の複雑さを追加する価値はないかもしれません。
国際的なアプリケーションや、ユーザーが任意の言語でテキストを入力するアプリケーションの場合は、適切な感度を持つIntl.Collatorを使用してください。
// Required for international text
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
これにより、大文字小文字の違いに関係なくタグがマッチします。