たにしきんぐダム

プログラミングやったりゲームしてます

JSで CodePoint 数えたい

ここで一句

JSで文字列を16bit単位ではなくUnicode Code Point単位で数える方法はいくつかあるが、結局2017年5月時点で(IE11のようなブラウザも含めて)ほとんどの環境で動作する方法はどれなんだろう。調べたのでまとめておきます、ご指摘あればどしどし(ง ‘-’ )ง

参考

Unicode コードポイント

Unicode では全ての文字にID(コードポイント)(0 ~ 0x10FFFF)をふっている。 コードポイントを表す時は U+{16進数} と書く。

UTF-16 では U+0000 ~ U+FFFF までのコードポイントは 16bitの1符号単位で表現されて U+10000 以降のコードポイント(𩸽のコードポイントなど)は、16bitだけでは表現できないため後述するサロゲートペアで表現される。

サロゲートペア

UTF-16 で16bitだけでは表現できないコードポイントを表現するために、一部の文字(U+10000 以降のコードポイントに対応する文字)は 16bit x 2 の 32bit で表現する。これらの文字(表現)をサロゲートペアと呼ぶ。

サロゲートコードポイント

サロゲートペアを構成する 16bit の値の範囲は U+D800 ~ U+DFFF であり、これらをサロゲートコードポイントと呼ぶ(このサロゲートコードポイントには文字が割り当てられていない)。さらに

と呼び、上位サロゲートと下位サロゲートの組み合わせでサロゲートペアを表現する。

lengthプロパティ

JavaScriptstr.lengthUnicodeコードポイント単位での文字列の長さではなく、UTF-16 符号単位の長さを返す。

例えば「𩸽」はサロゲートペアであるため、length は 2 となってしまう。

"𩸽".length; // 2

「𩸽」のようなサロゲートペアも1としてカウントしたい。そのためにはコードポイント単位での長さを調べれば良い。

どうやってコードポイント単位の長さを調べるか

いかれたメンバーを紹介するぜ!

for of

ES2015で追加された for ... of... を使うとコードポイント単位で繰り返しが可能

for (let c of '𩸽定食') console.log(c)
// 𩸽
// 定
// 食

しかし現在(2017年5月)IE11で未対応 https://kangax.github.io/compat-table/es6/#test-for..of_loops

スプレッド演算子

分割代入時の分割もコードポイントが意識されている模様。 スプレッド演算子を使えばシュッとコードポイント単位で文字列を配列に変形できる。

[...'𩸽定食']
// ['𩸽', '定', '食']

これも現在(2017年5月)IE11未対応 [https://kangax.github.io/compat-table/es6/#test-spread(…)operator]

RegExp unicode flag

ES2015より unicode flag が導入された、このフラグを使うとパターンをコードポイントの羅列として扱ってくれる。

'𩸽定食'.match(/./ug);
// ['𩸽', '定', '食']

残念ながら unicode flag も現在(2017年5月)IE11未対応 https://kangax.github.io/compat-table/es6/#test-RegExp_y_and_u_flags

for文で頑張る

文字列を16bit単位でループし、サロゲートペアは1としてカウントする。

function stringLength(str) {
  let count = 0;
  for (let i = 0; i < str.length; i++) {
    count++;
    // i番目の 16bit が得られる
    const code = str.charCodeAt(i);
    if (0xD800 <= code && code <= 0xDBFF) {
      // i番目の 16bit が上位サロゲートなら
      // 次の 16bit (下位サロゲート) はスキップ
      i++;
    }
  }
  return count;
}

これならIE11でも動く

正規表現(Unicode Sequence)

サロゲートペアを非サロゲートペア文字に変換してlengthをとる

function stringLength(str) {
  // サロゲートペアを _ にreplace
  return str.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_').length;
}

stringLength('𩸽定食'); // 3

配列に変換したい場合

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

stringToArray('𩸽定食');
// ['𩸽', '定', '食']

まとめ

以上だ!

随時更新していきたい