JavaScript Stringでサロゲートペアを扱う

JavaScript強力なUnicodeを扱う方法について書きます!(嘘)

先月末に発売されたWEB+DB PRESS Vol.78で「フロントエンドの国際化」の記事を書いたのは前回書いた通り。

記事内で、JSの文字列は基本UTF-16なのでサロゲートペアがうまく扱えないっていう問題は書いたけど、じゃあどうすればいいの?っていうのは載せられなかったので書く。

文字数のカウント

「𠮷(U+20BB7、つちよしだ)」や「𩸽(U+29E3D、ほっけ)」はUTF-16ではサロゲートペアで表現するのでlengthが見た目とズレる。

console.log("𠮷野家で𩸽".length); // 7

これを「5文字」とカウントしたいという話。

正規表現を使う方法

たぶん実装が一番シンプルなのが正規表現サロゲートペアの個数を数える方法。

function strlen(str) {
  return str.length - (str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g)||[]).length;
}
strlen("𠮷野家で𩸽"); // 5

ループ回してcharCodeAtでも良いけどこっちの方が分かりやすいし短い。*1

ES6 String Iteratorを利用する方法

ES6のString Iteratorはサロゲートペアを考慮してくれるので、これだけでOK。(Firefox 27以降で実装済

function strlen(str) {
  return [c for (c of str)].length;
}
strlen("𠮷野家で𩸽"); // 5

ES6 Spread Operatorを使うともっと短く書けるのは teramakoさんが書かれている とおり。

function strlen(str) {
  return [...str].length;
}

文字数カウントの闇

サロゲートペアだけじゃなくて、やれ空白文字だ制御文字だ、無効な文字は省こうみたいなことを考え始めると大変なことになる。そういう向きにはTwitterの文字数カウントロジックなんかが参考になると思われる。おすすめはしないけど。

コードポイントの取得/コードポイントからの文字生成

もうひとつ面倒なのは、サロゲートペア文字のコードポイント取得やコードポイントからの文字生成。これはJSのcharCodeAtfromCharCodeサロゲートペアに対応していないため。

"𠮷".charCodeAt(0).toString(16); // "d842" 上位サロゲートの値
"𠮷".charCodeAt(1).toString(16); // "dfb7" 下位サロゲートの値

もちろん計算すれば*2できるんだけどちょっと面倒。

これがES6ではcodePointAt/fromCodePointで解決できる(Firefox 29に実装済)。

"𠮷".codePointAt(0).toString(16); // "20bb7"
String.fromCodePoint(0x20bb7); // "𠮷"

これでだいぶ楽になりそう。

ただしcodePointAtの引数は従来の文字数換算でのインデックスなので注意。つまりこうなる。

"𠮷".codePointAt(0).toString(16); // "20bb7": コードポイント
"𠮷".codePointAt(1).toString(16); // "dfb7": 下位サロゲート

この辺はUnicodeの扱いが似ているJavaみたいにisSurrogatePair, isHighSurrogateみたいなAPIがもう少し増えてくるとやりやすくなりそうだけど、残念ながらいまのところES6 draftにはないっぽい。

WEB+DB PRESS Vol.78

WEB+DB PRESS Vol.78

ということで、2013年2014年最初の記事でした。今年もどうぞよろしく。

*1:そんなに回す関数じゃないと思うので実行速度は計ってない。

*2:code_point = (high_surrogate - 0xD800) * 0x400 + low_surrogate - 0xDC00 + 0x10000