Edited at

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

More than 3 years have passed since last update.


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規格あるの?)、まだ未検証。


参考リンク