Intl.Segmenterを用いた日本語・絵文字の高精度文字数カウント
JavaScript Frontend Unicode WebAPI
結論
絵文字対応の文字数カウントには Intl.Segmenter(granularity: '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) |
|---|---|---|---|
| 「あいうえお」 | 5 | 5 | 5 (正常) |
| 「𩸽」(サロゲートペア) | ❌ 2 | ◯ 1 | 1 (正常) |
| 「👨👩👧👦」(ZWJ結合絵文字) | ❌ 7 | ❌ 7 | 1 (正確に1文字と判定) |
| 「🇯🇵」(国旗絵文字) | ❌ 4 | ❌ 2 | 1 (正確に1文字と判定) |
実際に起こる事故:string.length によるDBバッファ溢れ・判定漏れ
JavaScriptの string.length は UTF-16 のコードユニット数を返す仕様です。
そのため、ユーザーが絵文字「👨👩👧👦」を1文字入力した際、length は 7 を返します。Twitter風の 140文字制限フォーム等で string.length やスプレッド構文([...str].length)を使っていると、絵文字多用の投稿で文字数が想定外にカウントされ、ユーザーが140文字未満と誤認して送信した結果、DBのVARCHAR制限を超えてサーバーエラー(500)を起こす障害が発生します。
実装手順
new Intl.Segmenter('ja', { granularity: 'grapheme' })をインスタンス化する- 対象テキストに対して
.segment(text)を呼ぶ - イテレータをループして「書形素クラスタ」の数をカウントする
// 短縮形の実装パターン
const countGraphemes = (str) =>
[...new Intl.Segmenter('ja', { granularity: 'grapheme' }).segment(str)].length;