Intl.Collator API

言語間で文字列を正しくソートおよび比較

はじめに

JavaScriptで文字列をソートすることは、国際的なテキストに遭遇するまでは簡単に思えます。デフォルトの文字列比較はUnicodeコードポイント値を使用するため、多くの言語で不正確な結果が生成されます。Intl.Collator APIは、文化的なソートルールを尊重し、特殊文字を正しく処理するロケール対応の文字列比較を提供します。

デフォルトのソートが失敗する理由

ドイツ語の名前のリストをソートすることを考えてみましょう。

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort());
// ["Ava", "Zoe", "Änder", "Ärzte"]

この出力はドイツ語話者にとって間違っています。ドイツ語では、äのようなウムラウト付き文字は、末尾ではなく基本文字aの近くにソートされるべきです。この問題は、JavaScriptがUnicodeコードポイント値を比較することに起因しており、Ä(U+00C4)はZ(U+005A)の後に来ます。

言語によってソートルールは異なります。スウェーデン語ではäをアルファベットの末尾にソートし、ドイツ語ではaの近くにソートし、フランス語ではアクセント付き文字を異なる方法で扱います。バイナリ比較はこれらの文化的慣習を無視します。

文字列照合の仕組み

照合とは、言語固有のルールに従って文字列を比較および順序付けするプロセスです。Unicode照合アルゴリズムは、文字、発音区別符号、大文字小文字、句読点を個別に分析することで文字列を比較する方法を定義しています。

2つの文字列を比較する際、照合関数は数値を返します。

  • 負の値:最初の文字列が2番目の文字列の前に来る
  • ゼロ:現在の感度レベルで文字列が等価
  • 正の値:最初の文字列が2番目の文字列の後に来る

この3方向比較パターンはArray.sortで機能し、どの違いが重要かを正確に制御できます。

基本的なロケール対応ソートにlocaleCompareを使用

localeCompareメソッドはロケール対応の文字列比較を提供します。

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort((a, b) => a.localeCompare(b, "de")));
// ["Ava", "Änder", "Ärzte", "Zoe"]

これにより、正しいドイツ語のソートが生成されます。最初のパラメータはロケールを指定し、localeCompareは文化的なルールを自動的に処理します。

3番目のパラメータとしてオプションを渡すことができます。

const items = ["File10", "File2", "File1"];
console.log(items.sort((a, b) =>
  a.localeCompare(b, "en", { numeric: true })
));
// ["File1", "File2", "File10"]

numericオプションは、「2」が「10」の前に来る自然なソートを有効にします。これがないと、「1」が「2」の前に来るため、「10」が「2」の前にソートされます。

localeCompareの繰り返しによるパフォーマンスの問題

各localeCompare呼び出しは、ロケール設定を最初から処理します。大きな配列をソートする場合、これにより大きなオーバーヘッドが発生します。

// Inefficient: processes locale for every comparison
const sorted = items.sort((a, b) => a.localeCompare(b, "de"));

1000個のアイテムをソートするには、約10000回の比較が必要です。各比較でロケール設定が再作成されるため、パフォーマンスコストが倍増します。このオーバーヘッドは、大規模なデータセットを持つユーザーインターフェースで顕著になります。

効率的な文字列比較のためのIntl.Collatorの使用

Intl.Collatorは、ロケール設定を一度だけ処理する再利用可能な比較オブジェクトを作成します。

const collator = new Intl.Collator("de");
const sorted = items.sort((a, b) => collator.compare(a, b));

collatorインスタンスは、ロケール設定と比較ルールを保存します。compareメソッドは、これらの事前計算されたルールをすべての比較に使用し、繰り返しの初期化オーバーヘッドを排除します。

大きな配列をソートする場合、繰り返しのlocaleCompare呼び出しと比較して、パフォーマンスの改善は60%から80%の範囲になります。

compareメソッドへの直接アクセス

compareメソッドを直接sortに渡すことができます。

const collator = new Intl.Collator("de");
const sorted = items.sort(collator.compare);

これは、compareがcollatorインスタンスにバインドされているため機能します。このメソッドは2つの文字列を受け取り、比較結果を返します。これは、Array.sortが期待するシグネチャと一致します。

感度レベルの理解

sensitivityオプションは、比較中にどの文字の違いが重要かを制御します。4つのレベルがあります。

base感度

base感度は、アクセントと大文字小文字を無視します。

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("a", "b")); // -1

基本文字のみが異なります。このレベルは、ユーザーがアクセントを正しく入力しない可能性があるあいまい検索に適しています。

アクセント感度

アクセント感度はアクセントを考慮しますが、大文字小文字は無視します:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("á", "A")); // 1

アクセント付き文字とアクセントなし文字は区別されます。同じ文字の大文字と小文字は一致します。

大文字小文字感度

大文字小文字感度は大文字小文字を考慮しますが、アクセントは無視します:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

大文字小文字の違いは重要ですが、アクセントは無視されます。このレベルは実際にはあまり一般的ではありません。

異体字感度

異体字感度はすべての違いを考慮します:

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

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

これはソートのデフォルトです。すべての文字の違いが異なる比較結果を生成します。

ユースケースに基づく感度の選択

異なるシナリオには異なる感度レベルが必要です:

  • リストのソート: 厳密な順序を維持するために異体字感度を使用
  • コンテンツの検索: アクセントや大文字小文字に関係なく一致させるために基本感度を使用
  • オプションのフィルタリング: 大文字小文字を無視する場合はアクセント感度を使用
  • 大文字小文字を区別する検索: アクセントを無視する場合は大文字小文字感度を使用

usageオプションは、一般的なシナリオのデフォルト感度設定を提供します。

ソートモードと検索モードでのusageの使用

usageオプションは、ソートまたは検索のためにコレーターの動作を最適化します:

// Optimized for sorting
const sortCollator = new Intl.Collator("en", { usage: "sort" });

// Optimized for searching
const searchCollator = new Intl.Collator("en", { usage: "search" });

ソート用途はデフォルトで異体字感度を使用し、すべての違いが一貫した順序を生成することを保証します。検索用途は一致を見つけることを最適化し、通常はより緩和された感度を使用します。

大文字小文字とアクセントを区別しない検索の場合:

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

const items = ["Apple", "Äpfel", "Banana"];
const matches = items.filter(item =>
  collator.compare(item, "apple") === 0
);
console.log(matches); // ["Apple"]

このパターンにより、ユーザーが正確な文字を入力する必要のないあいまい一致が可能になります。

自然な順序付けのための数値ソートの有効化

numericオプションは、埋め込まれた数値を数値として扱います:

const collator = new Intl.Collator("en", { numeric: true });

const files = ["File1", "File10", "File2"];
console.log(files.sort(collator.compare));
// ["File1", "File2", "File10"]

数値ソートを使用しない場合、「File10」は「File2」より前にソートされます。これは文字列「10」が「1」で始まるためです。数値ソートは数値シーケンスを解析し、数学的に比較します。

これにより、ファイル名、バージョン番号、番号付きリストに対する人間の期待に合致する自然な順序が生成されます。

数値ソートでの小数の処理

数値ソートには小数に関する制限があります。

const collator = new Intl.Collator("en", { numeric: true });

const values = ["1.5", "1.10", "1.2"];
console.log(values.sort(collator.compare));
// ["1.2", "1.5", "1.10"]

小数点は句読点として扱われ、数値の一部とは見なされません。句読点間の各セグメントは個別にソートされます。小数のソートには、値を数値として解析し、数値比較を使用してください。

caseFirstによる大文字小文字の順序制御

caseFirstオプションは、大文字と小文字のどちらを先にソートするかを決定します。

// Uppercase first
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]

// Lowercase first
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]

デフォルトはfalseで、ロケールのデフォルト順序を使用します。sensitivityがbaseまたはaccentの場合、これらのレベルは大文字小文字を無視するため、このオプションは効果がありません。

比較時の句読点の無視

ignorePunctuationオプションは、比較時に句読点をスキップします。

const collator = new Intl.Collator("en", { ignorePunctuation: true });

console.log(collator.compare("hello", "he-llo")); // 0
console.log(collator.compare("hello", "hello!")); // 0

このオプションはタイ語ではデフォルトでtrue、その他の言語ではfalseです。句読点が文字列の順序付けやマッチングに影響を与えるべきでない場合に使用します。

言語固有のルールに対する照合タイプの指定

一部のロケールは、特殊なソート用に複数の照合タイプをサポートしています。

// Chinese pinyin ordering
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");

// German phonebook ordering
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

// Emoji grouping
const emoji = new Intl.Collator("en-u-co-emoji");

照合タイプは、Unicode拡張構文を使用してロケール文字列で指定されます。一般的なタイプには以下が含まれます。

  • pinyin: ローマ字化された発音による中国語のソート
  • stroke: 画数による中国語のソート
  • phonebk: ドイツ語の電話帳順序
  • trad: 特定の言語の伝統的なソートルール
  • emoji: 絵文字をカテゴリ別にグループ化

環境で利用可能な照合タイプについては、Intl.supportedValuesOfを確認してください。

アプリケーション全体でcollatorインスタンスを再利用する

collatorインスタンスを一度作成し、アプリケーション全体で再利用します。

// utils/collation.js
export const germanCollator = new Intl.Collator("de");
export const searchCollator = new Intl.Collator("en", {
  sensitivity: "base"
});
export const numericCollator = new Intl.Collator("en", {
  numeric: true
});

// In your components
import { germanCollator } from "./utils/collation";

const sorted = names.sort(germanCollator.compare);

このパターンはパフォーマンスを最大化し、コードベース全体で一貫した比較動作を維持します。

プロパティによるオブジェクト配列のソート

オブジェクトプロパティにアクセスする比較関数でcollatorを使用します。

const collator = new Intl.Collator("de");

const users = [
  { name: "Zoe" },
  { name: "Änder" },
  { name: "Ava" }
];

const sorted = users.sort((a, b) =>
  collator.compare(a.name, b.name)
);

このアプローチは、あらゆるオブジェクト構造に対応します。比較する文字列を抽出し、collatorに渡します。

Intl.CollatorとlocaleCompareのパフォーマンス比較

Intl.Collatorは、大規模なデータセットをソートする際に優れたパフォーマンスを提供します。

// Slower: recreates locale settings for each comparison
const slow = items.sort((a, b) => a.localeCompare(b, "de"));

// Faster: reuses precomputed locale settings
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);

小規模な配列(100項目未満)では、差はほとんどありません。大規模な配列(数千項目)では、Intl.Collatorは60〜80%高速になる可能性があります。

Chromeなどのv8ベースのブラウザには1つの例外があります。localeCompareには、ルックアップテーブルを使用したASCII専用文字列の最適化があります。純粋なASCII文字列をソートする場合、localeCompareはIntl.Collatorと同等のパフォーマンスを発揮する可能性があります。

Intl.CollatorとlocaleCompareの使い分け

Intl.Collatorを使用する場合:

  • 大規模な配列(数百または数千の項目)をソートする場合
  • 繰り返しソートする場合(ユーザーがソート順を切り替える、仮想リスト)
  • 再利用可能な比較ユーティリティを構築する場合
  • ユースケースでパフォーマンスが重要な場合

localeCompareを使用する場合:

  • 1回限りの比較を行う場合
  • 小規模な配列(100項目未満)をソートする場合
  • パフォーマンスよりもシンプルさを優先する場合
  • セットアップなしでインライン比較が必要な場合

両方のAPIは同じオプションをサポートし、同一の結果を生成します。違いは純粋にパフォーマンスとコード構成に関するものです。

解決されたオプションの確認

resolvedOptionsメソッドは、collatorが使用する実際のオプションを返します。

const collator = new Intl.Collator("de", { sensitivity: "base" });
console.log(collator.resolvedOptions());
// {
//   locale: "de",
//   usage: "sort",
//   sensitivity: "base",
//   ignorePunctuation: false,
//   collation: "default",
//   numeric: false,
//   caseFirst: "false"
// }

これは照合動作のデバッグとデフォルト値の理解に役立ちます。システムが正確なロケールをサポートしていない場合、解決されたロケールは要求されたロケールと異なる場合があります。

ロケールサポートの検証

現在の環境でサポートされているロケールを確認します。

const supported = Intl.Collator.supportedLocalesOf(["de", "fr", "xx"]);
console.log(supported); // ["de", "fr"]

サポートされていないロケールはシステムのデフォルトにフォールバックします。このメソッドは、要求されたロケールが利用できない場合の検出に役立ちます。

ブラウザと環境のサポート

Intl.Collatorは2017年9月以降、広くサポートされています。すべてのモダンブラウザとNode.jsバージョンでサポートされており、APIは環境間で一貫して動作します。

一部の照合タイプとオプションは、古いブラウザでは限定的なサポートとなる場合があります。古い環境をサポートする場合は、重要な機能をテストするか、MDN互換性テーブルを確認してください。

避けるべき一般的な間違い

比較のたびに新しいコレーターを作成しないでください。

// Wrong: creates collator repeatedly
items.sort((a, b) => new Intl.Collator("de").compare(a, b));

// Right: create once, reuse
const collator = new Intl.Collator("de");
items.sort(collator.compare);

国際的なテキストに対してデフォルトのソートが機能すると想定しないでください。

// Wrong: breaks for non-ASCII characters
names.sort();

// Right: use locale-aware sorting
names.sort(new Intl.Collator("de").compare);

検索時にsensitivityを指定することを忘れないでください。

// Wrong: variant sensitivity requires exact match
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);

// Right: base sensitivity for fuzzy matching
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);

実用的なユースケース

Intl.Collatorは以下の用途に使用します。

  • ユーザー生成コンテンツのソート(名前、タイトル、住所)
  • 検索とオートコンプリート機能の実装
  • ソート可能な列を持つデータテーブルの構築
  • フィルタリングされたリストとドロップダウンオプションの作成
  • ファイル名とバージョン番号のソート
  • 連絡先リストでのアルファベット順ナビゲーション
  • 多言語アプリケーションインターフェース

ユーザーにソートされたテキストを表示するあらゆるインターフェースは、ロケール対応の照合から恩恵を受けます。これにより、ユーザーの言語に関係なく、アプリケーションがネイティブで正確に感じられるようになります。