数値を含む文字列を正しくソートする方法

数値照合を使用して、ファイル名、バージョン番号、その他の数値を含む文字列を自然な順序でソートする

はじめに

数値を含む文字列をソートする場合、file1.txtfile2.txtfile10.txtがこの順序で表示されることを期待します。しかし、JavaScriptのデフォルトの文字列比較では、file1.txtfile10.txtfile2.txtという順序になります。これは、文字列が1文字ずつ比較され、10の文字1が文字2より前に来るためです。

この問題は、ファイル名、バージョン番号、住所、製品コード、その他の数値を含む文字列をソートする際に発生します。誤った順序はユーザーを混乱させ、データのナビゲーションを困難にします。

JavaScriptは、この問題を解決する数値オプションを持つIntl.Collator APIを提供しています。このレッスンでは、数値照合の仕組み、デフォルトの文字列比較が失敗する理由、および数値を含む文字列を自然な数値順序でソートする方法について説明します。

数値照合とは

数値照合は、数字の連続を個々の文字ではなく数値として扱う比較方法です。文字列を比較する際、照合器は数字の連続を識別し、その数値で比較します。

数値照合が無効の場合、文字列file10.txtfile2.txtより前に来ます。これは、1文字ずつの比較で、最初に異なる位置で文字1が文字2より前に来ることが判明するためです。照合器は、102より大きい数値を表すことを考慮しません。

数値照合が有効の場合、照合器は102を完全な数値として認識し、数値的に比較します。10は2より大きいため、file2.txtは正しくfile10.txtより前に来ます。

この動作により、自然ソートまたは自然順序と呼ばれるものが生成されます。これは、数値を含む文字列が厳密にアルファベット順ではなく、人間が期待する方法でソートされることを意味します。

デフォルトの文字列比較が数値で失敗する理由

JavaScriptのデフォルトの文字列比較は辞書式順序を使用し、Unicodeコードポイント値を使用して左から右へ文字を1文字ずつ比較します。これはアルファベットテキストでは正しく機能しますが、数値では予期しない結果を生成します。

辞書式比較がこれらの文字列をどのように処理するかを考えてみましょう。

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

console.log(files);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

比較は各文字位置を独立して検査します。fileの後の最初の異なる位置で、12を比較します。12よりも低いUnicode値を持つため、file1で始まる文字列は、後に続く内容に関係なく、file2で始まる文字列の前に来ます。

これにより、file1.txtfile10.txtfile2.txtfile20.txtという順序が生成され、数値の順序に関する人間の期待に反します。

numericオプションを使用したIntl.Collatorの使用

Intl.Collatorコンストラクタは、numericプロパティを持つオプションオブジェクトを受け入れます。numeric: trueを設定すると数値照合が有効になり、照合器が数字の並びを数値として比較するようになります。

const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

files.sort(collator.compare);

console.log(files);
// Output: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

照合器のcompareメソッドは、最初の引数が2番目の引数の前に来る場合は負の数を、等しい場合はゼロを、最初の引数が後に来る場合は正の数を返します。これはJavaScriptのArray.sort()メソッドが期待するシグネチャと一致します。

ソートされた結果は、ファイルを自然な数値順序で配置します。照合器は1 < 2 < 10 < 20であることを認識し、人間が期待する順序を生成します。

英数字混在文字列のソート

数値照合は、数字が末尾だけでなく任意の位置に現れる文字列を処理します。照合器はアルファベット部分を通常通り比較し、数値部分を数値的に比較します。

const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];

addresses.sort(collator.compare);

console.log(addresses);
// Output: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']

照合器は各文字列の先頭にある数字の並びを識別し、数値的に比較します。字句的比較では異なる順序になりますが、5 < 45 < 123 < 1234であることを認識します。

バージョン番号のソート

バージョン番号は数値照合の一般的な使用例です。1.2.10のようなソフトウェアバージョンは1.2.2の後に来るべきですが、字句的比較では誤った順序になります。

const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];

versions.sort(collator.compare);

console.log(versions);
// Output: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']

照合器は各数値コンポーネントを正しく比較します。1.2.21.2.51.2.10の順序では、3番目のコンポーネントが数値的に増加していることを認識します。1.10.5では、2番目のコンポーネントが10であり、2より大きいことを認識します。

製品コードと識別子の処理

製品コード、請求書番号、その他の識別子は、文字と数字を混在させることがよくあります。数値照合により、これらが論理的な順序でソートされることが保証されます。

const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];

products.sort(collator.compare);

console.log(products);
// Output: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']

アルファベットの接頭辞PROD-はすべての文字列で一致するため、照合器は数値の接尾辞を比較します。結果は字句的順序ではなく、数値の増加順序を反映します。

異なるロケールでのソート

numericオプションは任意のロケールで機能します。ロケールによってアルファベット文字のソート規則は異なる場合がありますが、数値比較の動作は一貫しています。

const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });

const items = ['item1', 'item10', 'item2'];

console.log(items.sort(enCollator.compare));
// Output: ['item1', 'item2', 'item10']

console.log(items.sort(deCollator.compare));
// Output: ['item1', 'item2', 'item10']

文字列にはASCII文字と数字のみが含まれているため、両方のロケールで同じ結果が生成されます。文字列にロケール固有の文字が含まれている場合、アルファベット比較はロケール規則に従いますが、数値比較は一貫性を保ちます。

ソートせずに文字列を比較する

配列全体をソートせずに2つの文字列間の関係を判定するには、照合器のcompareメソッドを直接使用できます。

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

console.log(collator.compare('file2.txt', 'file10.txt'));
// Output: -1 (negative number means first argument comes before second)

console.log(collator.compare('file10.txt', 'file2.txt'));
// Output: 1 (positive number means first argument comes after second)

console.log(collator.compare('file2.txt', 'file2.txt'));
// Output: 0 (zero means arguments are equal)

これは、ソート済みリストにアイテムを挿入する場合や、値が範囲内にあるかどうかを確認する場合など、配列を変更せずに順序を確認する必要がある場合に便利です。

小数の制限について理解する

数値照合は数字の並びを比較しますが、小数点を数値の一部として認識しません。ピリオド文字は小数点区切り記号ではなく、区切り文字として扱われます。

const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];

measurements.sort(collator.compare);

console.log(measurements);
// Output: ['0.005', '0.05', '0.5']

照合器は各測定値を3つの独立した数値コンポーネントとして扱います。ピリオドの前の部分、ピリオド自体、ピリオドの後の部分です。00を比較し(等しい)、次にピリオドの後の部分を別々の数値として比較します。5、5、5(等しい)。次に小数第2位を比較します。なし、5、なし。これにより、小数の順序が正しくなくなります。

小数をソートするには、実際の数値に変換して数値的にソートするか、文字列パディングを使用して正しい辞書順を確保してください。

数値照合を他のオプションと組み合わせる

numericオプションは、sensitivitycaseFirstなどの他の照合オプションと併用できます。数値比較の動作を維持しながら、照合器が大文字小文字やアクセント記号をどのように処理するかを制御できます。

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

const items = ['Item1', 'item10', 'ITEM2'];

items.sort(collator.compare);

console.log(items);
// Output: ['Item1', 'ITEM2', 'item10']

sensitivity: 'base'オプションは、比較を大文字小文字を区別しないようにします。照合器はItem1item1ITEM1を同等として扱いながら、数値部分を正しく比較します。

パフォーマンスのためのコレーターの再利用

新しい Intl.Collator インスタンスを作成すると、ロケールデータの読み込みとオプションの処理が発生します。複数の配列をソートしたり、多数の比較を実行したりする必要がある場合は、コレーターを一度作成して再利用してください。

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

const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];

files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);

このアプローチは、ソート操作ごとに新しいコレーターを作成するよりも効率的です。多数の配列をソートしたり、頻繁に比較を実行したりする場合、パフォーマンスの差が顕著になります。