Surrogate Pair文字数を正しく数える方法

2025 03 01
6

テキストエリアの文字数を数える

ある日、テキスト入力エリアの文字数が実際と異なって表示されるという問い合わせが入りました。
現在、ユーザーが入力できるテキストエリアには文字数制限が設定されていますが、制限以下の文字数を入力しているのに制限に引っかかり、希望するメッセージが入力できないという状況でした。

原因を調査したところ、文字数を測定する際にString.lengthを使用しており、一部の絵文字や特殊文字が長さ2として扱われていることが判明しました。
つまり、サロゲートペア文字の取り扱い方法を変更する必要がありました。


Surrogate Pair

UTF-16はUnicode文字を表現する方法の一つで、多くの一般文字は1つの16ビットユニットで表現できますが、表現可能な文字数が増加したことで限界に達しました。
UTF-32を使えば解決できますが、大半の文字は16ビットで表現でき、メモリ効率も良いため、16ビット環境でより多くの文字を表現する方法が求められました。
その解決策として登場したのがサロゲートペアです。
0x10000(65536)以上のコードポイントを持った文字は二つのユニットで表現されるサロゲートペアが必要です。


Surrogate Pairの構造

Surrogate PairはそれぞれHigh SurrogateとLow Surrogateの組み合わせで1文字を構成します。
High Surrogate (上位サロゲート): D800 ~ DBFF の範囲にあり、 1番めの16ビットユニットとして使用されます。
Low Surrogate (下位サロゲート): DC00 ~ DFFF の範囲にあり、 2番目の16ビットユニットとして使用されます。

たとえば絵文字「😊」はU+1F60Aで、UTF-16では2つのユニットD83D + DE0Aで表現されます。

Surrogate Pairを使用する文字

主に絵文字や学術・特殊記号で使用されます。

絵文字
😃😂❤️🐶🐱🐻🌹🌻🍏🍔🏈
古代文字セット
古代エジプト象形文字 (Egyptian Hieroglyphs): U+13000 – U+1342F 𓀀, 𓀁, 𓀂
線形文字 B (Linear B Syllabary): U+10000 – U+1007F 𐀀, 𐀁, 𐀂
ゴート文字 (Gothic): U+10330 – U+1034F 𐌰, 𐌱, 𐌲
音楽と芸術記号
音楽記号 (Musical Symbols): U+1D100 – U+1D1FF 𝄞, 𝄡
バイキングのルーン文字 (Runic Symbols): 一部はU+16A0 – U+16FF範囲外のコードポイントを使用 ᛮ, ᛯ, ᛰ
数学・技術用記号
数学的アルファベット (Mathematical Alphanumeric Symbols): U+1D400 – U+1D7FF 𝐀, 𝐁, 𝐂
マヤ数字 (Mayan Numerals): U+1D2E0 – U+1D2FF 𐋀, 𐋁, 𐋂
その他言語と記号
楔形文字 (Cuneiform): U+12000 – U+123FF (シュメール語およびアカディア語で使用された古代の粘土板文字) 𒀀, 𒀁, 𒀂
デゼレット文字 (Deseret Alphabet): U+10400 – U+1044F 𐐀, 𐐁,𐐂


Surrogate Pairの文字列の長さ

2つのコードユニットを使うため、既存の文字列長さの測定方式をサロゲートペア文字に適用すれば次のように出力されます。

let egao = "😊";
console.log(egao.length); // result: 2

ユーザーが認識する文字数は1つですが、実際の長さは2になります。

Surrogate Pairが含まれた文字列の長さ測定

以下のような方法でSurrogate Pairの長さをわかることができます。

  1. Array.from()メソッド使用
const text = "𐋀𐋁𐋂";
const length = Array.from(text).length;
console.log(length);  // result: 3
  1. スプレッド演算子(spread operator) (...)使用
const text = "𐋀𐋁𐋂";
const length = [...text].length;
console.log(length);  // result: 3
  1. 正規表現
    Unicode処理用の正規表現であるuを使用
const text = "𐋀𐋁𐋂";
const regex = /./gu;
const length = text.match(regex).length;
console.log(length);  // result: 3
  1. ライブラリ使用
    graphemesplitのようなライブラリを使って文字列を測定する方法

おわりに

目に見えるものと実際は違うかもしれないです。
文字数を処理することは簡単に見えるため、見落としがちです。
しかし、モニターに表示される文字の数と実際のプログラミング上で処理されるべきデータの量が異なる場合があり、これは開発·最適化する過程で非常に重要な考慮事項だと思います。
BMP(Basic Multilingual Plane)の範囲であるU+0000からU+FFFFまでは大分のグローバルコミュニケーションに必要な文字を含みますが、U+10000以上のコードを正確に処理するためにはUnicode-awareシステムが必須です。
様々なユーザー、国際化されたシステムを開発する際には、を超える文字まで考慮して包括的な文字支援ができるようにUnicode-aware開発アプローチを適用することが重要だと思います。