JavaScriptでロケールに基づいて文字列をアルファベット順にソートする方法

任意の言語で文字列を正しくソートするには Intl.Collator と localeCompare() を使用する

はじめに

JavaScriptで文字列の配列をソートする場合、デフォルトの動作ではUTF-16コードユニット値に基づいて文字列を比較します。これは基本的なASCIIテキストには有効ですが、名前、製品タイトル、アクセント記号付き文字、非ラテン文字、大文字小文字が混在するテキストをソートする場合には適切に機能しません。

言語によってアルファベット順のルールは異なります。スウェーデン語ではå、ä、öはzの後のアルファベットの最後に配置されます。ドイツ語ではほとんどの場合、äはaと同等に扱われます。フランス語では特定の比較モードではアクセント記号が無視されます。これらの言語的ルールによって、各言語でソートされたリストがどのように表示されるかの期待が決まります。

JavaScriptはロケール対応の文字列ソートのために2つのAPIを提供しています。String.prototype.localeCompare()メソッドは単純な比較を処理します。Intl.Collator APIは大きな配列をソートする際により良いパフォーマンスを提供します。このレッスンでは、両方の仕組み、それぞれをいつ使用するか、そして異なる言語に対してソート動作をどのように設定するかを説明します。

デフォルトのソートが国際的なテキストに対して失敗する理由

デフォルトのArray.sort()メソッドはUTF-16コードユニット値によって文字列を比較します。これは大文字が常に小文字の前に来ることを意味し、アクセント記号付き文字はzの後にソートされます。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// 出力: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

この出力はスウェーデン語では正しくありません。スウェーデン語では、å、ä、öはアルファベットの最後に属する別の文字です。正しい順序ではAnnaが最初に来て、次にBengt、そしてÅsa、Ärla、Örjanとなるべきです。

この問題は、デフォルトのソートが言語的な意味ではなくコードポイント値を比較するために発生します。文字ÅはコードポイントがU+00C5で、zのコードポイント(U+007A)よりも大きいです。JavaScriptはスウェーデン語話者がÅをアルファベット内の特定の位置を持つ別の文字と考えていることを知る方法がありません。

大文字小文字の混在も別の問題を引き起こします。

const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// 出力: ['Apple', 'Zoo', 'banana', 'zebra']

すべての大文字は小文字よりも低いコードポイント値を持っています。これによりAppleとZooがbananaの前に表示されますが、これはどの言語でもアルファベット順ではありません。

localeCompareが言語規則に従って文字列をソートする方法

localeCompare()メソッドは、特定のロケールのソート順ルールに従って2つの文字列を比較します。最初の文字列が2番目の文字列より前にある場合は負の数を、同等の場合はゼロを、最初の文字列が2番目の文字列の後にある場合は正の数を返します。

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// 出力: -1 (負の値は'a'が'b'より前にあることを意味します)

localeCompare()を比較関数として渡すことで、Array.sort()と直接組み合わせて使用できます。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// 出力: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

スウェーデンのロケールでは、AnnaとBengtが標準のラテン文字を使用しているため最初に配置されます。その後、特殊なスウェーデン文字を持つÅsa、Ärla、Örjanが最後に来ます。

同じリストをドイツのロケールでソートすると、異なる結果が得られます。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// 出力: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']

ドイツ語ではソートの目的でäをaと同等に扱います。これにより、スウェーデン語のように最後ではなく、ÄrlaがAnnaの直後に配置されます。

localeCompareを使用するタイミング

小さな配列をソートしたり、2つの文字列を比較したりする必要がある場合は、localeCompare()を使用します。照合オブジェクトを作成して管理する必要なく、シンプルなAPIを提供します。

const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// 出力: ['apple', 'Banana', 'Cherry']

このアプローチは、数十項目の配列に適しています。小さなデータセットでのパフォーマンスへの影響は無視できるレベルです。

配列全体をソートせずに、ある文字列が別の文字列より前にあるかどうかを確認するためにもlocaleCompare()を使用できます。

const firstName = 'Åsa';
const secondName = 'Anna';

if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
  console.log(`${firstName} comes before ${secondName}`);
} else {
  console.log(`${secondName} comes before ${firstName}`);
}
// 出力: "Anna comes before Åsa"

この比較は、完全な配列をソートする必要なく、スウェーデンのアルファベット順を尊重します。

Intl.Collatorがパフォーマンスを向上させる方法

Intl.Collator APIは、繰り返し使用のために最適化された再利用可能な比較関数を作成します。大きな配列をソートしたり、多くの比較を実行したりする場合、コレーターは各比較ごとに localeCompare() を呼び出すよりも大幅に高速です。

const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// 出力: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

collator.compare プロパティは Array.sort() で直接動作する比較関数を返します。アロー関数でラップする必要はありません。

コレーターを一度作成して複数の操作に再利用することで、比較ごとにロケールデータを検索するオーバーヘッドを回避できます。

const collator = new Intl.Collator('de-DE');

const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);

const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);

console.log(sortedCities);
// 出力: ['Berlin', 'Hamburg', 'Köln', 'München']

console.log(sortedNames);
// 出力: ['Fischer', 'Müller', 'Schmidt', 'Schröder']

同じコレーターが、新しいインスタンスを作成する必要なく両方の配列を処理します。

Intl.Collatorを使用するタイミング

数百または数千のアイテムを持つ配列をソートする場合は、Intl.Collatorを使用してください。ソート中に比較関数が何度も呼び出されるため、配列サイズが大きくなるほどパフォーマンス上の利点が増加します。

const collator = new Intl.Collator('en-US');
const products = [/* 10,000の製品名を持つ配列 */];
const sorted = products.sort(collator.compare);

数百以上のアイテムを持つ配列の場合、コレーターはlocaleCompare()よりも数倍高速になることがあります。

また、同じロケールとオプションで複数の配列をソートする必要がある場合にもIntl.Collatorを使用してください。コレーターを一度作成して再利用することで、繰り返しのロケールデータ検索が不要になります。

const collator = new Intl.Collator('fr-FR');

const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];

const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);

このパターンは、テーブルビューや複数のソートされたリストを表示する他のインターフェースを構築する際に効果的です。

ロケールの指定方法

localeCompare()Intl.Collator の両方は、最初の引数としてロケール識別子を受け入れます。この識別子はBCP 47形式を使用し、通常は言語コードとオプションの地域コードを組み合わせています。

const names = ['Åsa', 'Anna', 'Ärla'];

// スウェーデンのロケール
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// 出力: ['Anna', 'Åsa', 'Ärla']

// ドイツのロケール
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// 出力: ['Anna', 'Ärla', 'Åsa']

ロケールによって適用される照合規則が決まります。スウェーデン語とドイツ語ではåとäに対する規則が異なるため、異なるソート順序が生成されます。

ブラウザからユーザーのデフォルトロケールを使用するには、ロケールを省略できます。

const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);

このアプローチでは、特定のロケールをハードコーディングせずに、ユーザーの言語設定を尊重します。ソートされた順序は、ユーザーのブラウザ設定に基づいて期待される順序と一致します。

フォールバックオプションを提供するために、ロケールの配列を渡すこともできます。

const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);

APIは配列から最初にサポートされているロケールを使用します。スウェーデン(スウェーデン)が利用できない場合は、一般的なスウェーデン語を試し、その後米国英語にフォールバックします。

大文字小文字の区別の制御方法

sensitivityオプションは、比較が大文字小文字とアクセントの違いをどのように扱うかを決定します。baseaccentcasevariantの4つの値を受け入れます。

base感度は大文字小文字とアクセントの両方を無視し、基本文字のみを比較します。

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

console.log(collator.compare('a', 'A'));
// 出力: 0 (等しい)

console.log(collator.compare('a', 'á'));
// 出力: 0 (等しい)

console.log(collator.compare('a', 'b'));
// 出力: -1 (異なる基本文字)

このモードでは、a、A、áは同じ基本文字を共有しているため、同一として扱われます。

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

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

console.log(collator.compare('a', 'A'));
// 出力: 0 (等しい、大文字小文字は無視)

console.log(collator.compare('a', 'á'));
// 出力: -1 (異なる、アクセントが重要)

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

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

console.log(collator.compare('a', 'A'));
// 出力: -1 (異なる、大文字小文字が重要)

console.log(collator.compare('a', 'á'));
// 出力: 0 (等しい、アクセントは無視)

variant感度(デフォルト)はすべての違いを考慮します。

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

console.log(collator.compare('a', 'A'));
// 出力: -1 (異なる)

console.log(collator.compare('a', 'á'));
// 出力: -1 (異なる)

このモードは最も厳密な比較を提供し、あらゆる違いを重要なものとして扱います。

埋め込まれた数字を含む文字列のソート方法

numeric オプションは、数字を含む文字列に対して数値ソートを有効にします。有効にすると、比較は数字の並びを文字ごとに比較するのではなく、数値として扱います。

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

// デフォルトのソート(誤った順序)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// 出力: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// 数値ソート(正しい順序)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// 出力: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

数値ソートがなければ、文字列は文字ごとにソートされます。文字列 102 の前に来ます。これは最初の文字 1 のコードポイントが 2 よりも小さいためです。

数値ソートを有効にすると、照合器は 10 を数字の10として、2 を数字の2として認識します。これにより、210 の前に来るという期待通りのソート順序が得られます。

このオプションは、ファイル名、バージョン番号、またはテキストと数字が混在する任意の文字列のソートに役立ちます。

const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// 出力: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

大文字と小文字のどちらを先にするかの制御方法

caseFirst オプションは、大文字と小文字のみが異なる文字列を比較する際に、大文字と小文字のどちらを先にソートするかを決定します。このオプションは upperlower、または false の3つの値を受け付けます。

const words = ['apple', 'Apple', 'APPLE'];

// 大文字を先に
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// 出力: ['APPLE', 'Apple', 'apple']

// 小文字を先に
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// 出力: ['apple', 'Apple', 'APPLE']

// デフォルト(ロケールに依存)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// 出力はロケールに依存します

false はロケールのデフォルトの大文字小文字の順序を使用します。ほとんどのロケールでは、デフォルトの感度設定を使用する場合、大文字と小文字のみが異なる文字列を等しいものとして扱います。

このオプションは、sensitivity オプションが大文字と小文字の違いを考慮する場合にのみ効果があります。

ソート時に句読点を無視する方法

ignorePunctuationオプションは、文字列を比較する際に句読点をスキップするようコレーターに指示します。これは、句読点を含む場合と含まない場合があるタイトルやフレーズをソートする際に役立ちます。

const titles = [
  'The Old Man',
  'The Old-Man',
  'The Oldman',
];

// デフォルト(句読点が影響する)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// 出力: ['The Old Man', 'The Old-Man', 'The Oldman']

// 句読点を無視
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// 出力: ['The Old Man', 'The Old-Man', 'The Oldman']

句読点を無視すると、比較は「Old-Man」のハイフンが存在しないかのように扱い、すべての文字列が「TheOldMan」であるかのように比較されます。

異なる国のユーザー名をソートする

世界中のユーザーの名前をソートする場合は、ユーザーの言語的期待に応えるために、ユーザーの優先ロケールを使用します。

const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);

const users = [
  { name: 'Müller', country: 'Germany' },
  { name: 'Martin', country: 'France' },
  { name: 'Andersson', country: 'Sweden' },
  { name: 'García', country: 'Spain' },
];

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

sorted.forEach(user => {
  console.log(`${user.name} (${user.country})`);
});

このコードはブラウザからユーザーのロケールを検出し、それに応じて名前をソートします。ドイツのユーザーはドイツのルールでソートされたリストを見ることができ、スウェーデンのユーザーはスウェーデンのルールでソートされたリストを見ることができます。

ロケール切り替えによるソート

アプリケーションでユーザーが言語を切り替えられるようにする場合は、ロケールが変更されたときにコレーターを更新します。

let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);

function setLocale(newLocale) {
  currentLocale = newLocale;
  collator = new Intl.Collator(currentLocale);
}

function sortItems(items) {
  return items.sort(collator.compare);
}

// ユーザーがスウェーデン語に切り替え
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// 出力: ['Anna', 'Åsa', 'Örjan']

// ユーザーがドイツ語に切り替え
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// 出力: ['Udo', 'Uhr', 'Über']

このパターンにより、ソートされたリストがユーザーが選択した言語に合わせて更新されることが保証されます。

localeCompare と Intl.Collator の選択

一回限りの比較や100項目未満の小さな配列をソートする場合は localeCompare() を使用してください。構文がシンプルで読みやすく、小さなデータセットでは性能差はほとんどありません。

const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));

大きな配列のソート、多数の比較、または同じロケールとオプションで複数の配列をソートする場合は Intl.Collator を使用してください。コレーターを一度作成して再利用することで、より良いパフォーマンスが得られます。

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

const products = [/* 大きな配列 */];
const sorted = products.sort(collator.compare);

両方のアプローチで同じ結果が得られます。選択はパフォーマンス要件とコード構成の好みによって異なります。