Intl.Segmenterを用いた日本語・絵文字の高精度文字数カウント

JavaScript Frontend Unicode WebAPI
結論

絵文字対応の文字数カウントには Intl.Segmentergranularity: 'grapheme')を使用します。

// MDN公式仕様:Intl.Segmenter による正確な文字数カウント
function getGraphemeCount(text, locale = 'ja') {
  const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
  const iterator = segmenter.segment(text)[Symbol.iterator]();
  let count = 0;
  while (!iterator.next().done) count++;
  return count;
}

// 👨‍👩‍👧‍👦(複合絵文字)を正確に「1文字」と判定
console.log(getGraphemeCount("👨‍👩‍👧‍👦")); // 結果: 1

従来手法と Intl.Segmenter の比較マトリクス

対象の文字列string.length (Code Unit)Array.from(str) (Code Point)Intl.Segmenter (Grapheme Cluster)
「あいうえお」555 (正常)
「𩸽」(サロゲートペア)2◯ 11 (正常)
「👨‍👩‍👧‍👦」(ZWJ結合絵文字)771 (正確に1文字と判定)
「🇯🇵」(国旗絵文字)421 (正確に1文字と判定)

実際に起こる事故:string.length によるDBバッファ溢れ・判定漏れ

JavaScriptの string.length は UTF-16 のコードユニット数を返す仕様です。

そのため、ユーザーが絵文字「👨‍👩‍👧‍👦」を1文字入力した際、length7 を返します。Twitter風の 140文字制限フォーム等で string.length やスプレッド構文([...str].length)を使っていると、絵文字多用の投稿で文字数が想定外にカウントされ、ユーザーが140文字未満と誤認して送信した結果、DBのVARCHAR制限を超えてサーバーエラー(500)を起こす障害が発生します。


実装手順

  1. new Intl.Segmenter('ja', { granularity: 'grapheme' }) をインスタンス化する
  2. 対象テキストに対して .segment(text) を呼ぶ
  3. イテレータをループして「書形素クラスタ」の数をカウントする
// 短縮形の実装パターン
const countGraphemes = (str) =>
  [...new Intl.Segmenter('ja', { granularity: 'grapheme' }).segment(str)].length;