1
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?

文字数カウントツールをブラウザで実装する — 絵文字が2文字になる.lengthのUTF-16問題

1
Last updated at Posted at 2026-09-01

文字数カウンターを作ると必ずぶつかるのが「絵文字を入れたら2文字と数えられた」問題。これはバグではなく、JavaScriptの String.prototype.length が文字の見た目ではなくUTF-16のコード単位を数えているから。ぱんだツールズの文字数カウンターは、文字数・行数・UTF-8バイト数をリアルタイムで表示するツール。この記事では、その裏側にあるコード単位・コードポイント・書記素の違いを実装ベースで書く。

.length はUTF-16のコード単位を数えている

カウンターの中身は驚くほど短い。入力のたびにこれを計算しているだけ。

function countText(text: string): CountResult {
  const total = text.length                              // 文字数(スペース含む)
  const noSpace = text.replace(/\s/g, '').length         // 文字数(スペース除く)
  const lines = text === '' ? 0 : text.split('\n').length // 行数
  const bytes = new TextEncoder().encode(text).byteLength // UTF-8バイト数
  return { total, noSpace, lines, bytes }
}

問題は text.length。これはJavaScriptの文字列の内部表現であるUTF-16のコード単位(16ビット=2バイトの箱)の個数を返す。ふだん使うほとんどの文字(英数字・ひらがな・漢字の大半)は1コード単位に収まるので、"あ".length は 1 で直感通り。

ところが、Unicodeの広い範囲(BMPの外=コードポイントが U+FFFF を超える文字)は、UTF-16ではサロゲートペアという2つのコード単位の組で表現される。絵文字や一部の漢字がこれに当たる。

"👍".length   // 2(サロゲートペア)
"𩸽".length   // 2(「ほっけ」。JIS第4水準の漢字)
"🇯🇵".length  // 4(国旗は2つの記号文字の組み合わせ)

だから「👍」1個が .length では2と数えられる。見た目の1文字と、内部のコード単位の数が一致しないのが原因。

コード単位・コードポイント・書記素

「1文字」の数え方は、実は3層ある。

  • コード単位(code unit) … UTF-16の箱の数。.length が返すもの。絵文字=2
  • コードポイント(code point) … Unicodeの文字番号の数。[...text].length や Array.from(text).length はこちらで、サロゲートペアを1つにまとめて数える。絵文字=1
  • 書記素クラスタ(grapheme cluster) … 人が「1文字」と感じる単位。Intl.Segmenter で数える。ゼロ幅接合子(ZWJ)で繋いだ絵文字(👨‍👩‍👧 など)も1と数える
const s = "👨‍👩‍👧"        // 家族の絵文字(ZWJで3人を結合)
s.length                    // 8(コード単位)
[...s].length               // 5(コードポイント。人3つ+ZWJ2つ)
// Intl.Segmenter なら 1(書記素)

このツールは一番素朴な .length(コード単位)を採用している。「見た目の文字数」を厳密に出すなら Intl.Segmenter が正しいが、あえてコード単位で数えるのには理由がある。

コード単位カウントは意外と実務に合う

「正確なのは書記素なのに、なぜコード単位?」と思うが、世の中の多くの文字数制限が実はコード単位(またはそれに近い)で動いている。

  • HTMLの maxlength 属性はUTF-16コード単位で数える。フォームで「あと何文字入力できるか」はコード単位ベース
  • 一部のDBの文字数制限や、古いAPIの上限もUTF-16やバイト単位のことが多い
  • 「◯◯字以内」の応募要項・SEOのtitle文字数なども、絵文字を厳密に1と数えたいケースは稀

つまり .length の値は、「厳密な見た目の文字数」ではないが、「システム上の文字数制限に引っかかるかどうか」の目安としてはむしろ実用的。だから素朴なコード単位カウントを一次情報として出しつつ、絵文字を多用するなら数がズレる、という前提を知っておけばいい。

スペース除去と行数の小さな仕様

残り3つの数値にも地味な仕様がある。

スペース除去は text.replace(/\s/g, '') で空白を消してから数える。JavaScriptの \s は半角スペース・タブ・改行だけでなく、全角スペース(U+3000)もマッチする。日本語文書で全角スペースを使っていても、ちゃんと空白として除外される。

行数は text.split('\n').length。改行(LF)で分割した断片の数を数える。空文字だけは特別扱いで 0 にしている。ここで一つ癖があって、末尾に改行があるテキスト("あ\n")は ["あ", ""] に割れて2行と数えられる。最後の改行の後ろに空の行があるとみなす、というよくある挙動。

バイト数は new TextEncoder().encode(text).byteLength。TextEncoderは常にUTF-8で符号化するので、日本語1文字はふつう3バイトになる。API上限やファイルサイズの見積もりに使える。同じ日本語でもUTF-8とShift_JISではバイト数が変わる(SJISなら2バイト)——このあたりの話はCSV文字コード変換の記事で詳しく書いた。

まとめ

  • String.prototype.length はUTF-16のコード単位を数える。サロゲートペアで表現される絵文字や一部の漢字は2(以上)と数えられる
  • 「1文字」には3層ある。コード単位(.length)・コードポイント([...text].length)・書記素(Intl.Segmenter)。厳密な見た目の文字数は書記素
  • あえてコード単位で数えるのは、maxlength やDB・API制限の多くがコード単位ベースで、実務の「文字数制限」の目安として使いやすいから
  • UTF-8バイト数は TextEncoder().encode().byteLength。日本語は1文字3バイト。文字コードが変わればバイト数も変わる

X(旧Twitter)の文字数確認や、字数制限のある文章を書くときにどうぞ。入力はブラウザの外に出ない。

ぱんだツールズ では他にも 文字コード変換・全角半角変換・大文字小文字変換・テキスト整形など、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・入力はサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

1
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
1
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?