JavaScriptはlengthや正規表現などでは文字数をカウントできない

lengthだけでは正確にカウントできない

JavaScriptでlengthは文字数をカウントするものと参考書などで習いますが、正確には文字数ではなくUTF-16のコード単位の数を返します。

そのため、「🇯🇵𠮷野家」のような文字列は人間の目には4文字に見えますが、lengthを使用すると8文字としてカウントされます。

JavaScript
const text = '🇯🇵𠮷野家'
const result = text.length
console.log(result) // 8

これは各文字のコード単位数が以下のようになっているからです。

lengthはUTF-16コード単位の合計値を返します。

文字コードポイントサロゲートペアUTF-16コード単位
🇯🇵U+1F1EF, U+1F1F524
𠮷U+20BB712
U+91CE01
U+5BB601

サロゲートペアとは、Unicodeの文字符号化方式であるUTF-16において、1つの文字を2つの16ビット文字で表現する方式です。

🇯🇵はU+1F1EFとU+1F1F5の2つの16ビット文字で表現されています。

Array.fromでも正確にカウントできない

JavaScriptでArray.fromで文字を分割してカウントしているコードを見かけることがありますが、🇯🇵は前述した通り、U+1F1EFとU+1F1F5の2つの16ビット文字で表現されています。

そのため、分割してもU+1F1EFとU+1F1F5の2つの文字になるため正確にカウントできません。

JavaScript
const text = '🇯🇵𠮷野家'
console.log(Array.from(text).length) // 5

スプレッド構文でも正確にカウントできない

[…text].length のようにスプレッド構文でもArray.fromと同様の理由により正確にカウントできません。

ChatGPTなどの生成AIだと、この方法で文字数をカウントできるという嘘を回答してくるので注意が必要です。

JavaScript
const text = '🇯🇵𠮷野家'
console.log([...text].length) // 5

正規表現でも正確にカウントできない

某サイトを見ていたら絵文字は正規表現を使用すれば正確にカウントできると書かれていました。

しかし、\p{Emoji} の正規表現も🇯🇵のようにサロゲートペアが2つの絵文字は2文字文としてマッチするため、正規表現でも正確にカウントできません。

JavaScript
const text = '🇯🇵𠮷野家'
const regex = /(\p{Emoji}(?:\u200D\p{Emoji})*|\S)/gu
const emojiLength = (str) => str.match(regex).length
const result = emojiLength(text)
console.log(result) // 5

Intl.Segmenterなら正確にカウントできる

Intl.Segmenterは、JavaScriptの国際化API(Intl)の一部で、テキストを言語ごとのルールに従ってセグメント(分割)するための機能を提供するオブジェクトです。

Intl.Segmenterによる分割は🇯🇵を1文字として分割します。

JavaScript
const text = '🇯🇵𠮷野家'
const segmenter = new Intl.Segmenter('ja-JP', { granularity: 'grapheme' })
console.log([...segmenter.segment(text)])
// [
//   {
//       "segment": "🇯🇵",
//       "index": 0,
//       "input": "🇯🇵𠮷野家"
//   },
//   {
//       "segment": "𠮷",
//       "index": 4,
//       "input": "🇯🇵𠮷野家"
//   },
//   {
//       "segment": "野",
//       "index": 6,
//       "input": "🇯🇵𠮷野家"
//   },
//   {
//       "segment": "家",
//       "index": 7,
//       "input": "🇯🇵𠮷野家"
//   }
// ]

そのため、[…segmenter.segment(text)].length で正確に文字数をカウントできます。

JavaScript
const text = '🇯🇵𠮷野家'
const segmenter = new Intl.Segmenter('ja-JP', { granularity: 'grapheme' })
const segLength = (text) => [...segmenter.segment(text)].length
console.log(segLength(text)) // 4

最近では入力フォームにサロゲートペアの文字や絵文字を許可しているケースが多くなってきています。

そのような文字列をJavaScriptで扱う際は前述のIntl.Segmenterを利用すれば正確に文字数をカウントできます。