アクセント記号を無視して文字列を比較する

JavaScriptの正規化とIntl.Collatorを使用して、発音区別記号を無視しながら文字列を比較する方法を学ぶ

はじめに

複数の言語で動作するアプリケーションを構築する際、アクセント記号を含む文字列を比較する必要がよくあります。「cafe」を検索するユーザーは「café」の結果を見つけるべきです。「Jose」のユーザー名チェックは「José」と一致するべきです。標準的な文字列比較ではこれらを異なる文字列として扱いますが、アプリケーションロジックではこれらを等しいものとして扱う必要があります。

JavaScriptはこの問題を解決するための2つのアプローチを提供しています。文字列を正規化してアクセント記号を削除するか、組み込みの照合APIを使用して特定の感度ルールで文字列を比較することができます。

アクセント記号とは

アクセント記号は、文字の上、下、または貫通して配置され、発音や意味を変更する記号です。これらの記号は発音区別記号と呼ばれます。一般的な例には、「é」のアキュートアクセント、「ñ」のチルダ、「ü」のウムラウトなどがあります。

Unicodeでは、これらの文字は2つの方法で表現できます。単一のコードポイントで完全な文字を表現するか、複数のコードポイントで基本文字と別個のアクセント記号を組み合わせることができます。文字「é」は、U+00E9として、または「e」(U+0065)と結合アキュートアクセント(U+0301)として保存できます。

比較でアクセント記号を無視すべき場合

検索機能は、アクセント非依存比較の最も一般的な使用例です。アクセント記号なしでクエリを入力するユーザーは、アクセント付き文字を含むコンテンツを見つけることを期待しています。「Muller」の検索は「Müller」を見つけるべきです。

ユーザー入力検証では、ユーザー名、メールアドレス、またはその他の識別子が既に存在するかどうかを確認する際に、この機能が必要です。「maria」と「maría」の重複アカウントを防ぎたいと考えます。

大文字と小文字を区別しない比較では、同時にアクセント記号も無視する必要があることがよくあります。大文字と小文字に関係なく2つの文字列が一致するかどうかを確認する場合、通常はアクセント記号の違いも無視する必要があります。

正規化を使用してアクセント記号を削除する

最初のアプローチでは、文字列を基本文字とアクセント記号が分離された正規化形式に変換し、その後アクセント記号を削除します。

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を使用して文字列を比較する

2番目のアプローチでは、Intl.Collator APIを使用します。これは、設定可能な感度レベルを持つロケール対応の文字列比較を提供します。

Intl.Collatorオブジェクトは、言語固有のルールに従って文字列を比較します。sensitivityオプションは、文字列を比較する際にどの違いが重要かを制御します。

「base」感度レベルは、アクセント記号と大文字小文字の違いの両方を無視します。アクセント記号または大文字小文字のみが異なる文字列は等しいと見なされます。

const collator = new Intl.Collator('en', { sensitivity: 'base' });

console.log(collator.compare('café', 'cafe')); // 0 (equal)
console.log(collator.compare('Café', 'cafe')); // 0 (equal)
console.log(collator.compare('café', 'caff')); // -1 (first comes before second)

compareメソッドは、文字列が等しい場合は0を、最初の文字列が2番目の文字列より前にある場合は負の数を、最初の文字列が2番目の文字列より後にある場合は正の数を返します。

これを等価性チェックや配列のソートに使用できます。

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); // Groups variants together

Intl.Collator APIは、さまざまなユースケースに対応する他の感度レベルを提供します。

「accent」レベルは大文字小文字を無視しますが、アクセント記号の違いは区別します。「Café」は「café」と等しくなりますが、「cafe」とは等しくなりません。

const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (equal)
console.log(accentCollator.compare('café', 'cafe')); // 1 (not equal)

「case」レベルはアクセント記号を無視しますが、大文字小文字の違いは区別します。「café」は「cafe」と等しくなりますが、「Café」とは等しくなりません。

const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (equal)
console.log(caseCollator.compare('café', 'Café')); // -1 (not equal)

「variant」レベルはすべての違いを区別します。これがデフォルトの動作です。

const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (not equal)

正規化と照合の選択

どちらの方法もアクセント記号を区別しない比較において正しい結果を生成しますが、それぞれ異なる特性を持っています。

正規化メソッドは、アクセント記号のない新しい文字列を作成します。正規化されたバージョンを保存またはインデックス化する必要がある場合は、このアプローチを使用してください。検索エンジンやデータベースは、効率的な検索のために正規化されたテキストを保存することがよくあります。

Intl.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'));
// Returns both Café Latte and 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'));
// Returns 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はこのメソッドをサポートしていません。

Intl.Collator APIは、すべてのモダンブラウザと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' });

比較戦略を選択する際は、アプリケーションがサポートする言語を常に考慮してください。