kickflow Tech Blog

株式会社kickflowの開発チームによるブログ

JavaScriptで文字列の長さを正しくカウントする

文字列の長さをカウントするのは難しい

こんにちは、CTOの小林です。

JavaScriptで文字列の長さをカウントする際、特にサロゲートペアや合字を含む場合には注意が必要です。この記事では、String.length[...str].length、およびIntl.Segmenterを使用して文字列の長さを正しくカウントする方法について説明します。

String.lengthでのカウント

JavaScriptのString.lengthプロパティを使うと、文字列内のコードユニット(UTF-16コードポイント)の数を返します。半角英数字や多くのひらがな・カタカナ・漢字はこの方法で文字数を取得できます。

console.log("Hello".length) // 出力: 5
console.log("こんにちは世界".length) // 出力: 7

サロゲートペア

しかし、String.lengthプロパティではサロゲートペア(一部の漢字や絵文字)を正しくカウントできません。 サロゲートペアとは、UTF-16エンコーディングで1つの文字を表現するために使用される2つのコードユニットの組み合わせです。基本多言語面(BMP)に収まらない文字(例:一部の漢字や絵文字)は、サロゲートペアとして表現されます。JavaScriptのString.lengthプロパティはコードユニットの数を返すため、これではサロゲートペアを正しくカウントできません。

const str = "𠮷野家"
console.log(str.length) // 出力: 4

上記の例では、"𠮷"(サロゲートペア)が1文字としてカウントされず、2つのコードユニットとしてカウントされるため、正確な文字数を取得できません。

[...str].lengthでのカウント

サロゲートペアを正確にカウントするには、スプレッド構文を使用して文字列を配列に変換し、その長さを取得する必要があります。これにより、サロゲートペアは正しく1文字としてカウントされます。

const str = "𠮷野家"
console.log([...str].length) // 出力: 3

合字

ところが、サロゲートペア以外にも厄介な存在がいます。合字です。

合字とは、複数の文字が組み合わさって一つの文字として表示されるものです。特に絵文字の世界では、ゼロ幅接合子(ZWJ; Zero Width Joiner)や絵文字修飾子シーケンス(Emoji Modifier Sequence)を使用して、複数の絵文字を組み合わせて新しい絵文字を作成することができます。

ゼロ幅接合子(ZWJ)

ja.wikipedia.org

ゼロ幅接合子は、隣接する文字を結合して一つの合字として表示するために使用されます。例えば、家族の絵文字 👨‍👨‍👧‍👦 は以下のように複数の個別の絵文字をZWJで結合した絵文字です。

👨‍👨‍👧‍👦 = 👨 + ZWJ + 👨 + ZWJ + 👧 + ZWJ + 👦

絵文字修飾子シーケンス(Emoji Modifier Sequence)

https://unicode.org/emoji/charts/full-emoji-modifiers.html

絵文字修飾子シーケンスは、基本の絵文字に修飾子を追加して異なるバリエーションを表示するために使用されます。例えば、肌の色を変更するために使用される修飾子があります。

こうして表現される合字は、先程紹介した方法でも正しくカウントできません。

// はてなブログのコードブロックでは合字が分解されてしまうが、実際は家族の合字
const emoji = "👨<200d>👨<200d>👧<200d>👦"
console.log([...emoji].length) // 出力: 7

この例では、合字が1文字としてカウントされず、ZWJを含む各部分が個別にカウントされてしまいます。

Intl.Segmenterを使ったカウント

合字も正しくカウントするためには、Intl.Segmenterを使います。 Intl.Segmenterは文字列を指定した粒度(書記素、単語、文)で正確にセグメント化するAPIです。 Intl.Segmenterを使って書記素の配列に変換することで、サロゲートペアや合字を含む文字列の長さを正しくカウントできます。

const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" })
const countGraphemes = (input) => {
  return [...segmenter.segment(input)].length
}

// サロゲートペア
const str = "𠮷野家"
console.log(countGraphemes(str)) // 出力: 3

// はてなブログのコードブロックでは合字が分解されてしまうが、実際は家族の合字
const emoji = "👨<200d>👨<200d>👧<200d>👦"
console.log(countGraphemes(emoji)) // 出力: 1

このように、サロゲートペアも合字も正確にカウントできました。

まとめ

JavaScriptで文字列の長さをカウントする場合、String.lengthではサロゲートペアを正しくカウントできず、[...str].lengthでは合字が正しくカウントできません。Intl.Segmenterを使用することで、サロゲートペアや合字を含む文字列の長さを正確にカウントすることができます。文字列の正確な長さを取得する必要がある場合は、Intl.Segmenterの使用を検討してください。

Intl.Segmenterは非常に便利な機能ですが、最近までFirefoxではサポートされていませんでした。Firefox 125以降では利用可能ですが、古いバージョンのブラウザをサポートしている場合は注意が必要です。ブラウザの対応状況については、以下のようなサイトで確認してください。

caniuse.com

なお、今日紹介した文字列のカウント方法はString.lengthプロパティのMDNにバッチリ書いてありました。困ったらMDNを読みましょう。

developer.mozilla.org

We are hiring!

kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。

kickflow.com

サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談、ご応募お待ちしています!

careers.kickflow.co.jp