文字列長計算ツール

文字列を貼り付けると、このツールはその長さを4つの異なる形で報告します。すなわち、JavaScript形式の.length(UTF-16コードユニット)、Unicodeコードポイント、グラフェームクラスター(ユーザーが単一の文字として認識するもの)、およびUTF-8バイト(データベース列に実際に格納されている値)です。エモジコン、フラグ、または組み合わせ記号を含む文字列ではこれらの値が一致せず、このような不一致が多くのバグの原因となっています。

文字列長の4つの意味

  1. 1

    UTF-16コードユニット

    `str.length`がJavaScriptで返す値です。ほとんどの文字には1、U+FFFFを超えるすべてのコードポイント(絵文字、古代文字体系など)には2を返します。

  2. 2

    コードポイント

    Unicodeスカラー1つにつき1つです。絵文字はそれぞれ1つですが、ZWJシーケンスは複数になります。

  3. 3

    グラフェムクラスター

    ユーザーが「1文字」と呼ぶものです。`🇺🇸`は2つのコードポイントですが、1つのグラフェムです。`n̈`も2つのコードポイントですが、1つのグラフェムです。

  4. 4

    UTF-8バイト

    DBが保存するデータです。ASCIIは1バイト、標準欧州文字は2バイト、CJKは3バイト、ほとんどの絵文字は4バイトです。

例

文字列 JSの.length コードポイント グラフェム UTF-8バイト
hello 5 5 5 5
café 4 4 4 5
😀 2 1 1 4
🇺🇸 4 2 1 8
👨‍👩‍👧(家族) 8 5 1 18
n̈(n + 分音記号) 2 2 1 3

数値が異なる理由

JavaScriptの文字列はUTF-16形式であるため、U+FFFFを超えるコードポイントはサロゲートペア(2つのUTF-16コードユニット)として保存されます。"😀".length === 2となるのはこのためです。ユーザーは😀を1文字として捉えているため、この挙動を嫌います。

グラフェムはさらにその先を行きます。国旗は2つの地域識別子コードポイントで構成されますが、ユーザーはそれを1つの旗として認識します。JavaScriptでは"🇺🇸".length === 4となり、不合理に感じられますが、実際そうなっています。グラフェム単位で反復処理するには、Intl.Segmenter(モダンな手法)やgrapheme-splitterライブラリを使うか、手動で処理します。

UTF-8バイト:データベースが保存する値

型がVARCHAR(255)の列の場合:

  • MySQLのutf8(実体はutf8mb3)では、255はバイト数です。caféは5バイトを消費するため、51個分を格納できます。
  • MySQLのutf8mb4(絵文字も含む本来のUTF-8)でも依然としてバイト数ですが、エンコーディングは4バイトのシーケンスに対応します。
  • PostgresのVARCHAR(255)では、255はバイトではなく文字数(コードポイント)です。
  • SQL ServerのVARCHARでは照合順序(collation)によって異なり、NVARCHARは2バイトのUCS-2単位でカウントします。

データベースのフィールドサイズをユーザーに見える文字数の上限で決める場合は、UTF-8の列では安全を見てバイト数×4を確保してください。各グラフェムはコードポイント1つあたり最大4バイトを消費する可能性があり、ZWJシーケンスではさらに増えます。

Twitter形式の文字数カウント

Twitterはツイートを独自のルールでカウントします。基本はコードポイントですが、絵文字とCJK(中日韓)表意文字はそれぞれ2文字として数えます。100% ASCIIのツイートは280文字まで入力できますが、絵文字を140個使ったツイートは140「文字」が上限になります。

SMSの文字数カウント:GSM 7ビットでは160文字です。GSM以外の文字(á、é、ñ、絵文字)が1つでも含まれるとエンコーディングがUCS-2に切り替わり、メッセージの長さは70文字まで減少します。

実用的な用途

  • データベース列のサイズ設定、マイグレーションを書く前にバイト数を確認します。
  • APIクォータの適用、ほとんどのAPIは文字数ではなくバイト数をカウントします。
  • フォームのバリデーション、ユーザーの感覚に合った正確な文字数(グラフェム)を表示します。
  • パフォーマンスのデバッグ、なぜこの正規表現の反復処理が遅いのか? それは、入力がグラフェム単位よりもコードユニット単位で3倍長いからです。

よくある質問

その絵文字は、複数のコードポイントを組み合わせたZWJシーケンスです(例えば家族の絵文字は7つのコードポイントで構成されます)。TwitterはUnicodeの重み付けルールに基づいて2文字としてカウントしますが、エディターは1グラフェムとして表示します。どちらも技術的には正しく、ただ測定している対象が異なるだけです。

UTF-8データベースでは、安全のため想定文字数1文字あたり4バイトを見込んでください。100文字(グラフェム)のフィールドはVARCHAR(400)バイトにすべきです。Postgresのように文字数でカウントするデータベースの場合は、文字セットの設定とあわせてVARCHAR(100)を使います。

JavaScriptでは正規化形式によって異なります。NFCで合成された形(U+00E1)は長さ1ですが、NFDで分解された形(U+0061 + U+0301)は長さ2です。見た目の文字は同じでも、バイト列は異なります。

家族および職業の絵文字は長いZWJシーケンスです。完全なサポートを備えていないフォントでは各コードポイントが個別のグリフとして表示されるため、👨‍💻は👨+💻に変換されます。OSのフォントをアップグレードすると問題が解決します。

関連ツール

このツールは他の言語でも利用できます