サロゲートペア

UTF-16 で基本多言語面 (BMP) 外の文字を 2 つの 16 ビットコードユニットで表現する仕組み。

サロゲートペアとは、UTF-16 エンコーディングで基本多言語面 (BMP: U+0000〜U+FFFF) の範囲外にある文字を表現するための仕組みです。上位サロゲート (U+D800〜U+DBFF) と下位サロゲート (U+DC00〜U+DFFF) の 2 つの 16 ビットコードユニットを組み合わせて 1 文字を表します。Unicode のコードポイント U+10000〜U+10FFFF の範囲にある約 100 万文字がこの方式で表現されます。

サロゲートペアが必要になった背景には、Unicode の拡張の歴史があります。当初 Unicode は 16 ビット (65,536 文字) で世界中の文字を収録する計画でしたが、漢字の異体字や歴史的文字、そして絵文字の追加により 16 ビットでは不足することが判明しました。そこで UTF-16 では、BMP 内の未使用領域 (U+D800〜U+DFFF) をサロゲート用に予約し、2 つのコードユニットで 1 文字を表す仕組みが導入されました。JavaScript 絵文字処理の書籍で正確なカウント方法を学べます。

絵文字の多くは BMP 外に配置されているため、サロゲートペアで表現されます。たとえば「😀」(U+1F600) は上位サロゲート U+D83D と下位サロゲート U+DE00 のペアで表されます。JavaScript の String.length は UTF-16 コードユニット数を返すため、「😀」の length は 1 ではなく 2 になります。同様に charAt()charCodeAt() もコードユニット単位で動作するため、サロゲートペア文字を正しく扱えません。

正確な文字数を得るには [...str].lengthArray.from(str).length を使います。これらはイテレータプロトコルを利用してコードポイント単位で文字列を分解するため、サロゲートペアを 1 文字として扱います。ES2015 以降では codePointAt() メソッドや for...of ループもコードポイント単位で動作します。ただし、書記素クラスタ (結合文字や ZWJ シーケンスで構成される視覚的な 1 文字) を正確にカウントするには Intl.Segmenter API が必要です。

サロゲートペアは UTF-16 固有の概念であり、UTF-8 や UTF-32 には存在しません。UTF-8 は可変長 (1〜4 バイト) でコードポイントを直接エンコードし、UTF-32 は固定 4 バイトですべてのコードポイントを表現します。データベースの文字型カラム (MySQL の utf8utf8mb4 の違いなど) でもサロゲートペア文字の格納可否が問題になることがあります。文字コード詳解の書籍でサロゲートペアの技術的な詳細が解説されています。

文字数カウントの観点では、サロゲートペアの存在は「1 文字とは何か」という根本的な問いを提起します。UTF-16 コードユニット数、コードポイント数、書記素クラスタ数のいずれを「文字数」とするかによって結果が異なります。文字数カウントツールを設計する際は、どの単位でカウントするかを明確にし、ユーザーの期待する結果と一致させることが重要です。