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


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

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

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

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

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

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

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

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

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

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

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

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

***

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

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

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

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

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

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

##### 漢字

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

- `20000`〜`2A6DF`「CJK統合漢字拡張B (CJK Unified Ideographs Extension B)」※JIS第3水準、JIS第4水準の文字を含む
- `2A700 `〜`2B73F`「CJK統合漢字拡張C (CJK Unified Ideographs Extension C)」
- `2B740`〜`2B81F`「CJK統合漢字拡張D (CJK Unified Ideographs Extension D)」
- `2F800`～`2FA1F` 「CJK互換漢字補助 (CJK Compatibility Ideographs Supplement)」


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

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

##### 絵文字

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

- `1F600`〜`1F64F` 「絵文字 (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番号から参照できない

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

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

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

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

```js
'𩸽'.length; // => 2
```

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

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

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

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

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

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

***

## IV. 解決方法

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

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

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

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

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

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

```js
// シンプルに書くとこう
// ※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. サロゲートペアに対応した配列化

```js
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規格あるの？）、まだ未検証。

## 参考リンク
- [サロゲートペア - Wikipedia](http://ja.wikipedia.org/wiki/Unicode#.E3.82.B5.E3.83.AD.E3.82.B2.E3.83.BC.E3.83.88.E3.83.9A.E3.82.A2)
- [サロゲートペア入門 - CodeZine](http://codezine.jp/article/detail/1592)
- [JavaScript Stringでサロゲートペアを扱う - teppeis blog](http://teppeis.hatenablog.com/entry/2014/01/surrogate-pair-in-javascript)
- [[JavaScript] サロゲート・ペアに対応した文字列操作関数を書いてみた - LiosK-free Blog](http://liosk.blog103.fc2.com/blog-entry-162.html)