チェック・トラブル対処Read this article in English

アイテム一覧の並び順が「おかしい」と言われる — ソートの落とし穴

プレイヤーがアルファベット順のアイテム一覧やランキング、キャラクター選択画面を見て、何かがおかしいと感じる。本来隣り合うはずの名前が離れていたり、アクセント付きの文字が変な位置に固まっていたり、母国語の電話帳や辞書で見慣れた順番と単純に一致しなかったりします。技術的な意味では何も壊れていません。すべての文字列は正しく表示され、欠落も文字化けもありません。並び順そのものが、表示している言語にとって間違っているのです。

これは多くのローカライズ不具合とは種類が違うバグです。問題はテキストではなく比較処理にあります。直すには「アルファベット順」とは何かを理解する必要があります — それは文字そのものの性質ではなく、言語ごとの慣習です。

バイト順・コードポイント順はアルファベット順ではない

単純なソートは、文字列の内部的な文字コードを1文字ずつ比較して順序を決めます。これはJavaScriptのデフォルトの Array.prototype.sort() を含め、多くの言語での単純な文字列比較演算子がやっていることです。これは確かに決定的な順序を生みますが、それは文字エンコーディングの順序であって、その言語のネイティブ話者が使う順序ではありません。

最もわかりやすい失敗例は大文字小文字とアクセントです。コードポイント順ではすべての大文字ラテン文字がすべての小文字より前に並ぶため、Zebra が apple より前に来てしまいます。人間の感覚でアルファベット順にする方法とは一致しません。アクセント付きの文字はUnicodeがたまたま割り当てたコードポイントの位置に並ぶため、アクセントなしの元の文字の隣に来るとは限らず、café と cafe が読み手の期待に反して離れた位置に並んでしまうことがあります。

["café", "cafe", "Zebra", "apple"].sort();
// → ["Zebra", "apple", "cafe", "café"]  — コードポイント順であってアルファベット順ではない
// 大文字のZが小文字のaより前に来る。caféのアクセントがcafeより後ろに押し出す

並び順のルール(照合順序)は言語ごとに異なる

何が「正しい」順序かを決めるルール(照合順序、collation)は言語によって異なり、単一の全世界共通ルールではなく、ロケールごとに標準化されています。アクセント付き文字をどこに置くか、大文字小文字を区別するかしないか、句読点や数字を文字とどう混ぜて並べるかは、すべてロケールごとの決定です。ある言語で正しい順序になる比較処理が、まったく同じ文字列のリストであっても、表示言語が変わっただけで別の言語では明らかに間違って見える順序を生むことがあります。

日本語の並び順 — かなと漢字は別扱いが必要

日本語のテキストには、単純なソートではまったく対応できない層がもう一つあります。漢字は文字コードに発音の情報を持たないため、漢字の文字列をコードポイント順に並べても、日本語話者が読み(よみがな)でアルファベット順にする感覚とは何の関係もない順序になります。日本語の名前や用語のリストは慣習的に、そのかな読みを五十音順に並べたものです。つまりソートに使うべきキーは表示されている文字列そのものではなく、それに紐づく別の「読み」情報だということです。

これはデータ設計に直接影響します。ゲーム内で日本語の名前やアイテム名を表示し、それらが自然な順に並ぶことを期待するなら、漢字の文字列とは別に、ソート用のふりがな(かな読み)を一緒に保持しておく必要があります。漢字の文字だけではその情報を持っていないからです。

  • かなのみのテキストは、ロケール対応の比較だけでも五十音順にある程度うまく並ぶ
  • 漢字のテキストは、紐づくかな読みを実際のソートキーとして使う必要がある。漢字のコードポイントは発音を符号化していない
  • かな・漢字・ラテン文字が混在するリスト(アイテム名などでよくある)は、個別の特例ではなく、ロケール対応の照合を一貫して適用する必要がある

対処法: 単純な比較ではなくロケール対応の照合を使う

汎用的な対処法は、文字列を直接比較するのをやめ、照合専用に作られた比較処理を対象ロケールで設定して使うことです。JavaScriptの Intl.Collator(ECMAScript国際化APIの一部で、CLDRの照合データを利用する)は、追加の依存なしにこれを実現し、ロケールごとに正しい大文字小文字・アクセント対応の順序を自動的に生成します。

const collator = new Intl.Collator("de");
["café", "cafe", "Zebra", "apple"].sort(collator.compare);
// → ["apple", "cafe", "café", "Zebra"] — 大文字小文字を区別せず、アクセントを考慮した順序

// ロケールが違えば、同じリストでも別の順序が「正しい」
new Intl.Collator("sv").compare("z", "ö"); // スウェーデン語ではöがzより後ろに並ぶ

予防策

プレイヤー向けの翻訳可能な文字列(名前、アイテム名、チャット、ランキングなど)を並び替える一覧は、デフォルトの比較やロケールを固定した比較ではなく、表示中の言語に連動した単一のロケール対応比較処理を必ず経由させます。日本語については特に、並び順が重要な場面すべてで名前やタイトルにかな読みフィールドを持たせておくこと。リリース後に追加しようとすると、既存の全エントリに読みを後付けする作業が発生します。リスト機能を出す前に、少なくとも1つの非ラテン文字言語のネイティブ話者に実際の並び順を確認してもらうこと。自分には問題なく見える順序でも、相手には明らかにおかしく見えることがあり、UI上にはその理由を示すヒントが何もありません。

関連記事