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

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

はじめに

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

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

アクセント記号とは

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

Unicodeでは、これらの文字は2つの方法で表現できます。1つのコードポイントで完全な文字を表すか、複数のコードポイントで基本文字と分離アクセント記号を組み合わせることができます。文字「é」は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 (等しい)
console.log(collator.compare('Café', 'cafe')); // 0 (等しい)
console.log(collator.compare('café', 'caff')); // -1 (最初の文字列が2番目より前)

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); // バリアントをグループ化

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

「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メソッドは文字列を変更せずに比較します。重複チェックやリストの並べ替えなど、文字列を直接比較する必要がある場合にこのアプローチを使用してください。照合器は単純な文字列比較では処理できない言語固有の並べ替えルールを尊重します。

パフォーマンスの考慮事項はユースケースによって異なります。照合器オブジェクトを一度作成して再利用することは、複数の比較に効率的です。一度正規化して何度も比較する場合は、文字列の正規化が効率的です。

正規化メソッドはアクセント情報を永続的に削除します。照合メソッドは指定したルールに従って比較しながら、元の文字列を保持します。

アクセント記号を無視した検索でアレイをフィルタリングする

一般的なユースケースは、アクセントの違いを無視してユーザー入力に基づいてアイテムの配列をフィルタリングすることです。

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はこのメソッドをサポートしていません。

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' });

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