JavaScriptでのサロゲートペア文字列のメモ

  • 152
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

I. UnicodeとJavaScript文字列の前提知識

I-I. Unicodeのエスケープシークエンスで文字列を表す

"\uXXXX"形式の4桁の16進数で表す

// シングルクォートとダブルクォートの差はない
"\u3042"; // => "あ"
'\u3042'; // => "あ"

// 正規表現リテラルでも表現可能
/\u3042/.test('あ'); // => true

I-II. 文字列からUnicode番号を参照する

// 10進数の参照 (戻り値型: number)
'あ'.charCodeAt(0); // => 12354

// 16進数の参照 (戻り値型: string)
'あ'.charCodeAt(0).toString(16); // => "3042"

ちなみにcharCodeAtメソッドの引数は参照したい文字列のインデックス番号を渡す。
1個目の文字なら0、2個目なら1を渡す。

I-III. Unicode番号から文字列を参照する

// 10進数から参照
String.fromCharCode(12354); // => "あ"

// 16進数から参照 数値の16進数リテラルを利用すれば可能
String.fromCharCode(0x3042); // => "あ"

II. サロゲートペア文字列

II-I. サロゲートペアとは

Unicode番号が16進数で10000以上の文字を、UTF-8(およびUTF-16)では表現できないため、
Unicode番号D800DBFFDC00DFFFの組み合わせで表現した仕組み。

たとえば「𩸽(ほっけ)」のUnicode番号は16進数で29E3Dであるのに対して、
実際のコードとしてはD867DE3Dのふたつの文字から構成された文字になる。

'\uD867\uDE3D'; // => "𩸽"

サロゲートペア対象になる文字

漢字

10000以降のUnicode番号の文字はすべてサロゲートペアでないと表現できないが、日本語に関わる文字として関係があるのは以下の範囲の文字

  • 200002A6DF「CJK統合漢字拡張B (CJK Unified Ideographs Extension B)」※JIS第3水準、JIS第4水準の文字を含む
  • 2A7002B73F「CJK統合漢字拡張C (CJK Unified Ideographs Extension C)」
  • 2B7402B81F「CJK統合漢字拡張D (CJK Unified Ideographs Extension D)」
  • 2F8002FA1F 「CJK互換漢字補助 (CJK Compatibility Ideographs Supplement)」

「CJK統合漢字拡張B (CJK Unified Ideographs Extension B)」の範囲はMac OS Xならほぼすべて表示できる。(2A6D72A6DFは未割り当てっぽい)

0 1 2 3 4 5 6 7 8 9 A B C D E F
2000 𠀀 𠀁 𠀂 𠀃 𠀄 𠀅 𠀆 𠀇 𠀈 𠀉 𠀊 𠀋 𠀌 𠀍 𠀎 𠀏
2001 𠀐 𠀑 𠀒 𠀓 𠀔 𠀕 𠀖 𠀗 𠀘 𠀙 𠀚 𠀛 𠀜 𠀝 𠀞 𠀟
:
2A6C 𪛀 𪛁 𪛂 𪛃 𪛄 𪛅 𪛆 𪛇 𪛈 𪛉 𪛊 𪛋 𪛌 𪛍 𪛎 𪛏
2A6D 𪛐 𪛑 𪛒 𪛓 𪛔 𪛕 𪛖
絵文字

絵文字もUnicodeで既にしっかりと定義されている。

  • 1F6001F64F 「絵文字 (Emoticons)」

以下のような絵文字がある。(直訳は感情アイコンだと思うんだけど…)

0 1 2 3 4 5 6 7 8 9 A B C D E F
1F60 😀 😁 😂 😃 😄 😅 😆 😇 😈 😉 😊 😋 😌 😍 😎 😏
1F61 😐 😑 😒 😓 😔 😕 😖 😗 😘 😙 😚 😛 😜 😝 😞 😟
1F62 😠 😡 😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 😭 😮 😯
1F63 😰 😱 😲 😳 😴 😵 😶 😷 😸 😹 😺 😻 😼 😽 😾 😿
1F64 🙀 🙁 🙂 🙅 🙆 🙇 🙈 🙉 🙊 🙋 🙌 🙍 🙎 🙏

他にも色々な範囲の文字定義の中のものが所謂「Emoji」として割り当てられていたりする。


III. サロゲートペア文字列の問題

III-I. 5桁以上のUnicode番号から参照できない

'\u29E3D'; // => "⧣D"

\u29E3でひとつの文字として解釈されてしまう。

III-II. 文字列長を正しく取得できない

実際はふたつの文字から構成されているので2文字分取得されてしまう。

'𩸽'.length; // => 2

III-III. 文字列を分割するとサロゲートで分割される

console.log('𩸽'.split('')); // => ["�", "�"]

つまり反復処理も難しい。

III-IV. 文字列から正しく文字を抜き取れない

'𩸽のひらき'.charAt(1); // => "�"
'𩸽のひらき'[2]; // => "の"
'𩸽のひらき'.slice(1, 4); // => "�のひ" ※表示媒体によっては下位サロゲート以降の文字が表示されない

つまりまともに文字列として扱えない!


IV. 解決方法

IV-I. Unicode番号から文字を生成する (fromCharCodeの代替)

ECMAScript6では String.formCodePoint が使えるのでそれを利用する。

String.fromCodePoint(0x29E3D); // => "𩸽"

もしくは新しいUnicodeリテラルを利用する。
ただしこの記述はES5以前では、Syntax Errorとなるので注意。

"\u{29E3D}"; // => "𩸽"

ECMAScript5以前に対応する (自前で作る)

// シンプルに書くとこう
// ※String.fromCodePointに似せるなら可変長引数に対応する必要あり
function stringFromCodePoint (codeNum) {
    var cp = codeNum - 0x10000;
    var high = 0xD800 | (cp >> 10);
    var low = 0xDC00 | (cp & 0x3FF);
    return String.fromCharCode(high, low);
}

stringFromCodePoint(0x29E3D); // => "𩸽"

IV-II. サロゲートペアに対応した配列化

function stringToArray (str) {
    return str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
}

stringToArray('𩸽のひらき'); // => ["𩸽", "の", "ひ", "ら", "き"]

ひとまずこれを作れば他にも応用できる

IV-III. 文字列長を取得する

stringToArray('𩸽のひらき').length; // => 5

IV-IV. 文字を抜き取る

stringToArray('𩸽のひらき')[0]; // => "𩸽"
stringToArray('𩸽のひらき')[1]; // => "の"

あとはよしなに関数化するなりそのまま扱うなりすればよい


V. その他

V-I. HTMLのmaxlength

input要素やtextarea要素のmaxlength属性に関する挙動は、JavaScriptのString.prototype.lengthと全く同じ長さで挙動をとるので、実際問題正確な長さは測れない。

※ドキュメントのcharsetをUTF-32とかにしたら直るのかどうかわからないけど(そもそもutf-32規格あるの?)、まだ未検証。

参考リンク