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番目の後に来る

この三方向比較パターンは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の呼び出しごとにロケール設定が一から処理されます。大きな配列をソートする場合、これは大きなオーバーヘッドを生じさせます:

// 非効率的:比較ごとにロケールを処理する
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));

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

大きな配列をソートする場合、繰り返しのlocaleCompare呼び出しと比較して60%から80%のパフォーマンス向上が見られます。

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

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

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

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

感度レベルの理解

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

基本感度(Base sensitivity)

基本感度はアクセントと大文字小文字を無視します:

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オプションはソートまたは検索のためにcollatorの動作を最適化します:

// ソート用に最適化
const sortCollator = new Intl.Collator("en", { usage: "sort" });

// 検索用に最適化
const searchCollator = new Intl.Collator("en", { usage: "search" });

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

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

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 オプションは大文字または小文字のどちらを先にソートするかを決定します:

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

// 小文字優先
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です。句読点が文字列の順序付けやマッチングに影響を与えるべきでない場合に使用します。

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

一部のロケールでは、特殊な並べ替えのために複数の照合タイプをサポートしています:

// 中国語のピンイン順
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");

// ドイツ語の電話帳順
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

// 絵文字のグループ化
const emoji = new Intl.Collator("en-u-co-emoji");

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

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

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

アプリケーション全体での照合インスタンスの再利用

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

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

// コンポーネント内で
import { germanCollator } from "./utils/collation";

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

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

プロパティによるオブジェクト配列の並べ替え

オブジェクトのプロパティにアクセスする比較関数で照合器を使用します:

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

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

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

Intl.Collatorは大規模なデータセットの並べ替えでより良いパフォーマンスを提供します:

// 遅い方法:比較ごとにロケール設定を再作成
const slow = items.sort((a, b) => a.localeCompare(b, "de"));

// 速い方法:事前計算されたロケール設定を再利用
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);

小さな配列(100項目未満)では、その差はわずかです。大きな配列(数千項目)では、Intl.Collatorは60〜80%速くなる可能性があります。

ChromeなどのV8ベースのブラウザでは例外が存在します。localeCompareはルックアップテーブルを使用したASCIIのみの文字列に対する最適化があります。純粋なASCII文字列を並べ替える場合、localeCompareはIntl.Collatorと同等のパフォーマンスを発揮する可能性があります。

Intl.CollatorとlocaleCompareの使い分け

Intl.Collatorを使用する場合:

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

localeCompareを使用する場合:

  • 一度限りの比較
  • 小規模な配列(100アイテム未満)のソート
  • シンプルさがパフォーマンスの懸念よりも重要な場合
  • セットアップなしでインライン比較が必要な場合

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

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

resolvedOptionsメソッドは、照合順序で実際に使用されているオプションを返します:

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互換性テーブルを確認してください。

よくある間違いと対策

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

// 間違い: コレーターを繰り返し作成している
items.sort((a, b) => new Intl.Collator("de").compare(a, b));

// 正しい: 一度作成して再利用する
const collator = new Intl.Collator("de");
items.sort(collator.compare);

デフォルトのソートが国際的なテキストに対応していると思い込まないでください:

// 間違い: ASCII以外の文字で問題が発生する
names.sort();

// 正しい: ロケール対応のソートを使用する
names.sort(new Intl.Collator("de").compare);

検索時に感度(sensitivity)を指定し忘れないでください:

// 間違い: variant感度は完全一致が必要
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);

// 正しい: あいまい一致にはbase感度を使用
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);

実用的なユースケース

Intl.Collatorの用途:

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

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