ここで一句
JSで文字列を16bit単位ではなくUnicode Code Point単位で数える方法はいくつかあるが、結局2017年5月時点で(IE11のようなブラウザも含めて)ほとんどの環境で動作する方法はどれなんだろう。調べたのでまとめておきます、ご指摘あればどしどし(ง ‘-’ )ง
参考
- JavaScript における文字コードと「文字数」の数え方 | blog.jxck.io
- Unicode のサロゲートペアとは何か - ひだまりソケットは壊れない
- JavaScriptでのサロゲートペア文字列のメモ - Qiita
- ECMAScript 6 compatibility table
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プロパティ
JavaScript の str.length
は Unicodeコードポイント単位での文字列の長さではなく、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('𩸽定食'); // ['𩸽', '定', '食']
まとめ
以上だ!
随時更新していきたい