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);
// Output: ['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);
// Output: ['Apple', 'Zoo', 'banana', 'zebra']

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

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

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

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// Output: -1 (negative means 'a' comes before '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);
// Output: ['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);
// Output: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']

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

localeCompareを使用するタイミング

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

const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// Output: ['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}`);
}
// Output: "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);
// Output: ['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);
// Output: ['Berlin', 'Hamburg', 'Köln', 'München']

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

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

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

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

const collator = new Intl.Collator('en-US');
const products = [/* array with 10,000 product names */];
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'];

// Swedish locale
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Output: ['Anna', 'Åsa', 'Ärla']

// German locale
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Output: ['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'));
// Output: 0 (equal)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal)

console.log(collator.compare('a', 'b'));
// Output: -1 (different base characters)

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

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

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

console.log(collator.compare('a', 'A'));
// Output: 0 (equal, case ignored)

console.log(collator.compare('a', 'á'));
// Output: -1 (different, accent matters)

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

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

console.log(collator.compare('a', 'A'));
// Output: -1 (different, case matters)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal, accent ignored)

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

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

console.log(collator.compare('a', 'A'));
// Output: -1 (different)

console.log(collator.compare('a', 'á'));
// Output: -1 (different)

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

数値が埋め込まれた文字列をソートする方法

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

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

// Default sorting (wrong order)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// Numeric sorting (correct order)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Output: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

数値ソートを使用しない場合、文字列は1文字ずつソートされます。文字列102より前に来ます。これは最初の文字1のコードポイントが2より小さいためです。

数値ソートを有効にすると、collatorは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);
// Output: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

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

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

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

// Uppercase first
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Output: ['APPLE', 'Apple', 'apple']

// Lowercase first
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Output: ['apple', 'Apple', 'APPLE']

// Default (locale-dependent)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Output depends on locale

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

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

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

ignorePunctuationオプションは、文字列を比較する際に句読点を無視するようcollatorに指示します。これは、句読点を含む場合と含まない場合があるタイトルやフレーズをソートする際に便利です。

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

// Default (punctuation matters)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

// Ignore punctuation
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Output: ['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})`);
});

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

ロケール切り替えを伴うソート

アプリケーションでユーザーが言語を切り替えられる場合は、ロケールが変更されたときにcollatorを更新してください。

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

// User switches to Swedish
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Output: ['Anna', 'Åsa', 'Örjan']

// User switches to German
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Output: ['Udo', 'Uhr', 'Über']

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

localeCompareとIntl.Collatorの選択

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

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

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

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

const products = [/* large array */];
const sorted = products.sort(collator.compare);

どちらのアプローチも同じ結果を生成します。選択は、パフォーマンス要件とコード構成の好みによって異なります。