UTF-8 のバイト長を返す
「1 文字 1 バイト」で見積もると、足りなくなる
入力欄を 100 文字までにしたい。保存先を 100 バイトで用意したら、日本語で 40 文字ほど入れた時点で溢れました。英語でテストしていたときは、何ともなかったのにです。
原因は単純で、UTF-8 では 1 文字の大きさが一定ではないからです。A は 1 バイトですが、あ は 3 バイトを使います。文字数とバイト数は別の数え方で、掛け算では行き来できません。
Python
print(len('あA')) # 2 文字数
print(len('あA'.encode('utf-8'))) # 4 バイト数先頭のバイトが、続きが何個あるかを宣言している
長さが一定でないなら、どこで 1 文字が終わるのか分からなくなりそうです。UTF-8 はそこを、先頭バイトの上位のビットで解決しています。
プレーンテキスト
0xxxxxxx 1 バイト
110xxxxx 10xxxxxx 2 バイト
1110xxxx 10xxxxxx 10xxxxxx 3 バイト
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4 バイト先頭が 0 なら 1 バイトで終わり、110 なら続きが 1 つ、1110 なら続きが 2 つ、という約束です。続きのバイトは必ず 10 で始まるので、途中から読み始めても文字の切れ目を見つけ直せます。
先頭が 0 の形は ASCII とまったく同じ並びです。だから英数字だけのファイルは、UTF-8 として読んでも 1 バイトも変わりません。
日本語が 3 バイトになるのは、11 ビットに入らないから
上の並びで、x の数を数えてください。これが文字の番号に使える本体のビット数です。1 バイト形式は 7 個、2 バイト形式は 5 個と 6 個で 11 個、3 バイト形式は 4 個と 6 個と 6 個で 16 個になります。
あ の番号は 0x3042 で、2 進数にすると 14 桁あります。11 桁には入りません。だから 2 バイト形式では表しきれず、16 桁まで置ける 3 バイト形式に回されます。日本語の大半がこの範囲に並んでいるので、まとめて 3 バイトになります。
つまりバイト数は、文字の種類ではなく番号の大きさだけで決まります。何ビットあれば足りるか、を考えれば、境目の値は自分で組み立て直せます。
数え方を混ぜると、途中で切れる
文字化けの多くは、バイト数で長さを切ったときに起きます。3 バイトの真ん中で切ると、先頭バイトだけが残った半端な並びができます。読む側は続きを待っているのに来ないので、置換文字に化けます。
長さを測るとき、切り出すとき、保存するときで、文字数とバイト数のどちらで数えているのかを毎回はっきりさせてください。データベースの列の長さ制限も、文字数で指定するものとバイト数で指定するものが混在しています。
要件
- 3 つの境界 (0x80, 0x800, 0x10000) で判定する
- 1〜4 の整数を返す
- 範囲外の入力は考慮しなくてよい
入出力例
utf8Len(65) → 1
utf8Len(127) → 1
utf8Len(128) → 2
utf8Len(233) → 2
utf8Len(2047) → 2
utf8Len(2048) → 3
utf8Len(12354) → 3
utf8Len(65535) → 3
utf8Len(65536) → 4
utf8Len(128013) → 4