JS: 文字数を数える・切るときは Intl.Segmenter で書記素単位に
str.length や slice() は UTF-16 のコード単位で数えるので、絵文字や結合文字を途中で切ってしまう。見た目の 1 文字(書記素)で扱うなら Intl.Segmenter を使う。
使う場面
「続きを読む…」のプレビューを N 文字で切る、入力欄の文字数カウンタ、ユーザー名の文字数上限チェックなど、ユーザーが見る「文字」の数が大事なところ。
たとえば家族の絵文字 "👨👩👧" は、人物 3 つと ZWJ(ゼロ幅接合子)2 つの計 5 コードポイントでできていて、.length は 8 になる。[...str].length(コードポイント単位)でも 5 になり、どちらも画面上の 1 文字とは合わない。
やり方
ts
const seg = new Intl.Segmenter("ja", { granularity: "grapheme" });
// 書記素の数
const graphemeLength = (s: string) => [...seg.segment(s)].length;
// 先頭 n 書記素で切る(途中で切れた絵文字が化けない)
function truncate(s: string, n: number, suffix = "…") {
const parts = [...seg.segment(s)];
if (parts.length <= n) return s;
return parts.slice(0, n).map((p) => p.segment).join("") + suffix;
}
"👨👩👧".length; // 8
graphemeLength("👨👩👧"); // 1
truncate("家族👨👩👧でお出かけ", 3); // "家族👨👩👧…"granularityは"grapheme"(既定)・"word"・"sentence"。"word"を使うと、スペースで区切らない日本語でも単語に分けられる(isWordLikeで記号を除ける)。
注意点
- DB やバックエンドの文字数上限(バイト数やコードポイント数)とは数え方が違う。フロントの表示用とサーバーの検証用で、どちらの単位なのかを揃えておく。
- 長い文字列を毎回すべて配列にすると重い。切り出すだけなら
for (const { segment } of seg.segment(s))で n 個に達した時点で抜ける。 - 主要ブラウザと Node.js 16 以降で使える(Firefox は 125 から)。