3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

絵文字を含む文字列の length が思った数にならない理由と、正しい文字数の数え方

3
Posted at

'😀'.length は 1 ではなく 2 になります。入力欄の「あと○文字」表示や、文字列の切り詰めで絵文字が壊れる原因はほぼこれです。本記事では、length・スプレッド構文・Intl.Segmenter の3つで「1文字」の数え方がどう違うかを実測します。

検証環境は Node.js v24.14.1 です。表示している出力はすべて実際に実行した結果です。

結論: 数え方は3種類ある

方法 数えているもの
str.length UTF-16コードユニット(16ビットの塊)の数
[...str].length コードポイント(Unicodeの番号1つ)の数
Intl.Segmenter(grapheme) 人間が1文字と感じる単位(書記素)の数

実測する

const seg = new Intl.Segmenter('ja', { granularity: 'grapheme' });
for (const s of ['abc', 'あいう', '😀', '👍🏽', '👨‍👩‍👧', '🇯🇵', '𠮷']) {
  console.log(JSON.stringify(s).padEnd(10),
    'length:', s.length,
    '| [...s]:', [...s].length,
    '| grapheme:', [...seg.segment(s)].length);
}
"abc"      length: 3 | [...s]: 3 | grapheme: 3
"あいう"      length: 3 | [...s]: 3 | grapheme: 3
"😀"       length: 2 | [...s]: 1 | grapheme: 1
"👍🏽"     length: 4 | [...s]: 2 | grapheme: 1
"👨‍👩‍👧" length: 8 | [...s]: 5 | grapheme: 1
"🇯🇵"     length: 4 | [...s]: 2 | grapheme: 1
"𠮷"       length: 2 | [...s]: 1 | grapheme: 1

ひらがなや英数字では3つの方法が一致するので、ここで気づけません。ズレるのは、サロゲートペア(UTF-16で2コードユニットを使う文字)と、複数のコードポイントを合成した文字です。

  • 😀 は U+1F600 の1コードポイントですが、UTF-16では2コードユニットなので length は2
  • 👍🏽 は「👍」(U+1F44D)+肌色修飾子(U+1F3FD)の2コードポイント
  • 👨‍👩‍👧 は3人の絵文字を U+200D(ゼロ幅接合子)でつないだ5コードポイント。length は8
  • 🇯🇵 は国旗を表す2つの地域指示記号。見た目は1文字
  • 「𠮷」(つちよし)のような一部の漢字もサロゲートペアで、length は2

切り詰めると壊れる

console.log(JSON.stringify('A😀B'.slice(0, 2)));
console.log('A😀B'.slice(0, 2).isWellFormed());
"A\ud83d"
false

slice(0, 2) が絵文字の途中(上位サロゲートだけ)で切れ、不正な文字列(lone surrogate)になりました。これを encodeURIComponent に渡すと実際に例外になります。

URIError: URI malformed

画面では A� のように表示され、APIに送れば文字化けや例外の原因になります。String.prototype.isWellFormed() は不正なサロゲートが混じっていないかの確認に使えます。

安全に切る: Intl.Segmenter

const text = '今日は🇯🇵👨‍👩‍👧とご飯';
console.log(JSON.stringify(text.slice(0, 5)));
console.log([...seg.segment(text)].slice(0, 5).map(x => x.segment).join(''));
console.log(text.length, [...text].length, [...seg.segment(text)].length);
"今日は🇯"
今日は🇯🇵👨‍👩‍👧
18 13 8

slice は国旗の片割れ(🇯 単体の記号)だけを残して、国旗として壊れます。しかもこの結果は isWellFormed() では true になります(Node v24.14.1 で確認)。サロゲートペアは壊れていなくても、書記素は壊れている、という状態です。Intl.Segmenter で書記素単位に分けて5つ取れば、国旗も家族の絵文字も壊れません。同じ文字列でも、length は18、スプレッドは13、書記素は8で、3通りの答えになります。

どれを使うか

  • 文字数制限(入力フォームなど。X のように独自の重み付けで数えるサービスもある点に注意)を「人間に見える文字数」で扱うなら Intl.Segmenter
  • DBのカラム長やAPIの仕様が「UTF-16」「バイト」「コードポイント」のどれで数えるかは、仕様側を確認してから決める。length が正しい場面もある
  • 絵文字を含まないと確実に言える範囲(ID、英数字のコードなど)なら length で問題ない
  • 文字を1つずつ逆順にしたいときの split('').reverse().join('') も、サロゲートペアを壊す。[...str] ならサロゲートペアは守れますが、合成絵文字や国旗はまだ壊れます([...'🇯🇵'].reverse().join('') は 🇵🇯 になりました)。絵文字を含むなら Segmenter で分けてから reverse します

Intl.Segmenter は最近のブラウザとNode.jsで使えますが、古い実行環境では未対応の可能性があります。対象環境は事前に確認してください。

AIが書いたコードで .length や .slice を見かけたら

Claude Code などのAIは、文字数制限のコードを if (text.length > 140) や text.slice(0, 140) + '…' と書くことがあります。英数字だけのテストでは動くので、そのままレビューを通りやすい箇所です。次の2点を確認すると安全です。

  1. 絵文字や国旗を含む入力でテストしたか('👨‍👩‍👧'.length が8になる前提で数字を決めているか)
  2. 「何を1文字と数える仕様か」を決めたうえで書かれているか。決まっていなければ、AIに「絵文字を含めた場合の文字数の数え方を比較して」と聞いて比較させると、確認が早いです

仕様が決まっていないまま「とりあえず length」と書かれたコードが、AIコード特有の見落としどころです。


未経験者向けの講座を運営しています

未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。

※ Qiita 読者の方には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?