【JS体操第3問ヒント②】「コードポイント」と「コードユニット」

こんにちは!面白プロデュース事業部のおばらです。

本記事はJS体操第3問「Zalgo Text の生成」の問題のヒントにもなるかもしれないシリーズ第2弾。 第1弾は 「Zalgo Text のできるまで」でした。

今回は「コードポイント」、そして「コードポイント」と似ているけれどちょっと違う概念の「コードユニット」についてざっくりおさらいしてみます。

techblog.kayac.com

hubspot.kayac.com


目次


「Unicode」とは

「コードポイント」と「コードユニット」の説明の前に、まずは「Unicode」について。

Unicode(The Unicode Standard)」とは「文字コード(Character Encoding)」の業界標準の「規格(Standard)」です。

「文字コード(Character Encoding)」は(ざっくり言うと)「文字」に「数字」を紐づけるルール&その方法のことです。

文脈によっては「文字に紐づく数字」のことを「文字コード(Character Code)」という場合もあり、日本語で説明するとややこしいですね。 「Character Encoding」と「Character Code」を明確に区別するため、「文字エンコーディング(Character Encoding)」という日本語訳が使われる場合もあるようです。 また「文字集合(Character Set)」という概念もあり、もうわけがわかりません。

日本語に翻訳された2次情報よりも英語の1次情報を読むほうが混乱しないかもしれませんが、 Wikipedia と MDN の「Unicode」の説明を比較しても若干のニュアンスの違いを感じます。

Wikipedia

Unicode, formally The Unicode Standard, is a text encoding standard maintained by the Unicode Consortium designed to support the use of text in all of the world's writing systems that can be digitized.

MDN

Unicode is a standard character set that numbers and defines characters from the world's different languages, writing systems, and symbols.


本記事では

  • 「Unicode」はあくまで「規格(Standard)」である
  • 「UTF-8」「UTF-16」「UTF-32」は「Unicode」という「規格(Standard)」を採用した/に準拠した「文字コード(Character Encoding)」、より具体的に言うと「文字符号化形式(Character Encoding Form)」である

という前提で話を進めますが、もし間違いがあればご指摘ください。


「Unicode」で「文字」と「数字」がどう紐づいているか、いくつか例を見てみましょう。

  • 「あ」は 12354
  • 「ぃ」は 12355
  • 「い」は 12356
  • 「A」は 65
  • 「B」は 66
  • 「C」は 67

「あ」の次は「い」ではなく(捨て仮名、小書き文字の)「ぃ」なのですね。 なお、「文字コード(Character Encoding)」(の規格)が違えば、この「文字」と「数字」の対応が異なる場合があります。


「Unicode」についてもっと詳しく知りたい!という方は以下をご覧ください。

developer.mozilla.org

ja.wikipedia.org


「コードポイント」とは

「コードポイント(Code Point)」とは「文字に割り当てられた数字」のことです。「符号位置 」や「符号点」、また文脈によっては単に「点」と呼ぶこともあるようです。

「点(Point)」という単語が登場するのは、「空間(Codespace)」、「面(Code Plane)」などの概念と併せて使われるからですが、それら用語を使うととても難解になるのでここでは割愛します。

先程の(Unicode の)「文字」と「数字」の対応の例を「コードポイント」という言葉を使って書き直すと以下です。

  • 「あ」に割り当てられている「(Unicode の)コードポイント」は 12354
  • 「ぃ」に割り当てられている「(Unicode の)コードポイント」は 12355
  • 「い」に割り当てられている「(Unicode の)コードポイント」は 12356
  • 「A」に割り当てられている「(Unicode の)コードポイント」は 65
  • 「B」に割り当てられている「(Unicode の)コードポイント」は 66
  • 「C」に割り当てられている「(Unicode の)コードポイント」は 67

ちなみに、Unicode の「コードポイント」の範囲、難しく言うと Unicode の「文字集合の符号空間」は、01114111、16進数で表すと 0x0000000x10FFFF と仕様で決まっています。 ということはつまり、 Unicode の「コードポイント」すべてを表すには最低 21 ビット必要であることに注目しましょう。 1114111 を 2進数に直すと 0b100001111111111111111 で、21桁あるからです。

0x10FFFF.toString(2) // => '100001111111111111111'
0x10FFFF.toString(2).length // => 21

Unicode の「コードポイント」をすべて表すのに最低21ビット必要であることは、後で「コードポイント」と「コードユニット」の違いを意識するのに役立ちます。


なお、以降本記事で「コードポイント」と書く場合「Unicode のコードポイント」を指すものとします。

コードポイントについて詳しく知りたい!という方は以下をご覧ください。

developer.mozilla.org

ja.wikipedia.org


「文字」を「コードポイント」に変換する

JavaScript で、ある「文字(列)」の「コードポイント」の数値を取得するには String クラスの codePointAt() インスタンスメソッドを使うのが便利です。

使い方はこんな感じ。

'あ'.codePointAt(0) // => 12354
'A'.codePointAt(0) // => 65


引数の数値は、文字列が複数のコードポイントからなる場合に「何個目のコードポイントを取得するかのインデックス」です。よって、こんな使い方もできます。

'あA'.codePointAt(0) // => 12354
'あA'.codePointAt(1) // => 65


0個目(一番最初)のコードポイントを取得したい場合、分かりづらくなるのでオススメはしませんが 0 は省略できます。

'あ'.codePointAt() // => 12354
'あA'.codePointAt() // => 12354


developer.mozilla.org


「コードポイント」を「文字」に変換する

逆に「コードポイント」の数値を「文字(列)」に変換するには String クラスの fromCodePoint() クラスメソッドを使うのが便利です。

String.fromCodePoint(12354) // => 'あ'
String.fromCodePoint(65) // => 'A'


コンマ区切りで複数のコードポイントを渡すこともできます。

String.fromCodePoint(12354, 65) // => 'あA'


ちなみに整数でないとしっかりと怒られます。

String.fromCodePoint(12354.678) // => RangeError: Invalid code point 12354.678


Unicode のコードポイントの範囲を超える数値(0x10FFFF を超える数値)を渡しても怒られます。

String.fromCodePoint(0x10FFFF) // => '􏿿'
String.fromCodePoint(0x10FFFF + 1) // => RangeError: Invalid code point 1114112


developer.mozilla.org


「コードユニット」とは

「コードポイント」と似た言葉で「コードユニット」があります。 が、「コードユニット」を説明する前に「UTF-8」「UTF-16」「UTF-32」を説明します。


「UTF-8」「UTF-16」「UTF-32」

「Unicode」の「文字符号化形式(Character Encoding Form)」には以下の3つがあります。
ざっくり言い換えると、「Unicode」という「規格(Standard)」を採用した/に準拠した「文字コード(Character Encoding)」には以下の3つがあります。

  • UTF-8 (8-bit Unicode Transformation Format)
  • UTF-16 (16-bit Unicode Transformation Format)
  • UTF-32 (32-bit Unicode Transformation Format)

UTF-8 はウェブで用いられるテキストファイル(hoge.html、piyo.css、fuga.js など)で一般的に用いられる「文字コード(Character Encoding)」です。
UTF-16 は JavaScript が内部で文字列を保持するのに使われる「文字コード(Character Encoding)」です。
UTF-32 はウェブではあまり使わない気がするので本記事では割愛します。

ja.wikipedia.org ja.wikipedia.org ja.wikipedia.org


「コードユニット」とは

  • UTF-8 では、8ビットずつの単位(Unit)で情報が格納されます
  • UTF-16 では、16ビットずつの単位(Unit)で情報が格納されます
  • UTF-32 では、32ビットずつの単位(Unit)で情報が格納されます

この情報を格納する「単位(Unit)」のことを「コードユニット(Code Unit)」もしくは「コード単位」、「符号単位」などと呼びます。

以降本記事では「コードユニット」は「UTF-16 のコードユニット」を指すものとします。


「文字」を「コードユニット」に変換する

ある「文字(列)」を「(UTF-16 の)コードユニット」の数値に変換するには String クラスの charCodeAt() インスタンスメソッドを使うのが便利です。

codeUnitAt() ではなく
charCodeAt() なので注意しましょう。

「コードポイント」を取得するインスタンスメソッドは codePointAt() なので、
「コードユニット」を取得するインスタンスメソッドは codeUnitAt() でしょ!と思いきやそうではありません。

charCodeAt() は Char Code、つまり「文字コード(Character Code)」を取得するメソッドです。

日本語で「文字コード」と言うと「文字コード(Character Encoding)」のことなのか「文字コード(Character Code)」なのか曖昧になりますが、ここでは「Character Code」のことです。 また、「Character Code」も、「コードポイント(Code Point)」のことなのか「コードユニット(Code Unit)」 のことなのかが曖昧でカオスですね。この場合は「コードユニット(Code Unit)」 を指します。

歴史的には charCodeAt() は元々(ECMAScript 1 から)あり、あとから codePointAt()ES6 (ECMAScript 2015) で追加されました。

あのときメソッド名を charCodeAt() ではなく codeUnitAt() にすれば良かったと世界のどこかで誰かが後悔しているかもしれませんが、後方互換性を考えると charCodeAt() のままにするしか無かったのかもしれません。


とにかく、charCodeAt() の使い方はこんな感じです。

'あ'.charCodeAt(0) // => 12354
'A'.charCodeAt(0) // => 65


引数の数値は、文字列が複数のコードユニットからなる場合に「何個目のコードユニットを取得するかのインデックス」です。よって、こんな使い方もできます。

'あA'.charCodeAt(0) // => 12354
'あA'.charCodeAt(1) // => 65


0個目(一番最初)のコードユニットを取得したい場合、分かりづらくなるのでオススメはしませんが 0 は省略できます。

'あ'.charCodeAt() // => 12354
'あA'.charCodeAt() // => 12354


codePointAt() のときと使い方も結果も(今のところ)同じですね。
文字(列)によっては、codePointAt()charCodeAt() で違いがでてくるのですが、それはまた後ほど。


「コードユニット」を「文字」に変換する

逆に「コードユニット」の数値を「文字(列)」に変換するには String クラスの fromCharCode() クラスメソッドを使うのが便利です。

こちらも
fromCodeUnit() ではなく
fromCharCode() ですので注意してください。

String.fromCharCode(12354) // => 'あ'
String.fromCharCode(65) // => 'A'


コンマ区切りで複数の「コードユニット」の数値を渡すこともできます。

String.fromCharCode(12354, 65) // => 'あA'


ちなみに(String.fromCodePoint() とは違い)整数でなくても怒られません。小数点以下を切り捨ててくれます。やさしい。というか緩い。

String.fromCharCode(12354.678) // => 'あ'


1つの「コードユニット」に収まりきらない数値(0xFFFF を超える数値)を渡すと、その下位16ビットのみが使われます。これまた緩い。

String.fromCharCode(0x1234) === String.fromCharCode(0x00001234 | 0xFFFF0000) // => true


「コードユニット」について詳しく知りたい!という方は以下をご覧ください。 developer.mozilla.org


「コードポイント」と「コードユニット」の違い

これまでの内容を踏まえて、「コードポイント」と「コードユニット」の違いを簡単にまとめると以下です。

  • 「コードポイント」は文字に紐づく数字である
  • 「コードユニット」は「コードポイント」をデータとして格納するための1単位である

より具体的に UTF-16 の場合を考えると、

  • 1つの「(Unicode の)コードポイント」を1つの「(UTF-16 の)コードユニット」を使って表せる場合もある
  • 1つの「(Unicode の)コードポイント」を2つの「(UTF-16 の)コードユニット」を使って表す必要がある場合もある

と言えます。


Unicode の「コードポイント」の範囲は 0x0000000x10FFFF であること、それを表すには最低21ビット必要であることを思い出してみましょう。

  • 「(Unicode の)コードポイント」は最大21ビット
  • 「(UTF-16 の)コードユニット」は最大16ビット

「(UTF-16 の)コードユニット」ひとつ分、つまり16ビットでは、「(Unicode の)コードポイント」すべてを表すことができません(ビットが足りない)。 「(UTF-16 の)コードユニット」ひとつ分、つまり16ビットに格納できる(非負整数の)最大値は 0xFFFF です。

よって、「コードポイント」が 0xFFFF を超えると、その「コードポイント」をデータとして格納するには複数(UTF-16 では最大2つの)の「コードユニット」が必要になります。

const n = 0xFFFF;

String.fromCharCode(n) === String.fromCodePoint(n); // => true
const m = 0xFFFF + 1;

String.fromCharCode(m) === String.fromCodePoint(m); // => false


なお、1つの「コードポイント」を複数の「コードユニット」に格納しなければならない場合、1つめの「コードユニット」に格納しきれない分が単純に2つめの「コードユニット」に格納されるわけではありません。

0xFFFF + 1、つまり 65536 という(1つのコードユニットには収まりきらない)「コードポイント」は、5529656320、16進数に直すと 0xD8000xDC00 という2つの数値にあるルールで変換され、それぞれ1つずつのコードユニット、合計2つのコードユニットに格納されます。

const m = 0xFFFF + 1; // => 65536
const char = String.fromCodePoint(m);

char.charCodeAt(0); // => 55296
char.charCodeAt(1); // => 56320
String.fromCharCode(55296, 56320) === String.fromCodePoint(0xFFFF + 1); // => true

この「(UTF-16 の)コードユニット」1つでは Unicode の「コードポイント」すべてを格納できないという制限を回避するための仕組みが「サロゲートペア」ですが、その説明は本記事では割愛します。


とにかく、JavaScript を書くうえでは

  • 文字によっては「コードポイント」と「コードユニット」を明確に区別しなくてはいけない

ことに注意してください。


文字数とは?

良い機会なので「文字数」についても軽く触れておきます。

『JS体操』では「文字列の length プロパティの値」を「文字数」と定義しています。 実は「文字列の length プロパティの値」は「UTF-16 のコードユニットの数」です。 実際の、見た目上の文字数とは異なります。

例えば、以下は人間の目では15文字に見えますが、length の値はなんと 27 です。
つまり「UTF-16 のコードユニットの数」が 27 です。

'みんな👨‍👩‍👧‍👦で𩸽のお寿司食べたい🍣'.length // => 27

※ 上記サンプルコードのシンタックスハイライトの言語指定を javascript にしたら家族「👨‍👩‍👧‍👦」がバラバラになってしまったので、あえて言語指定をしていません
※「𩸽(ほっけ)」はサロゲートペア界隈ではとても有名な魚です🐟


それなら「コードポイントの数」を数えればいいのでは!となりそうですが、そうともいきません。 見た目上の文字数を数えるには「書記素」という概念が必要になります。でもその説明は長くなるのでまたの機会に。 きちんと説明するには前述の「サロゲートペア」や「ゼロ幅接合子」、JS体操第3問「Zalgo Textの生成」のテーマでもある「結合文字」などの特殊な文字について触れる必要もでてきます。


もし『JS体操』の文字数を「文字列の length の値」ではなく「コードポイントの数」や「書記素クラスタの数」とすると、またいろんなチートが可能になりそうですね。

developer.mozilla.org



まとめ

最後まで読んでいただき、ありがとうございます。
「コードポイント」と「コードユニット」についてざっくり解説してみましたが、名前も似ているし、日本語訳の問題も相まってとてもややこしいですね。

本記事の内容は、もしかしたら現在開催中のJS体操第3問「Zalgo Textの生成」のヒントにもなるかもしれません。

『JS体操』とは面白法人カヤックが主催する JavaScript のコードゴルフ大会。
その第3問は任意の文字列を「Zalgo Text」に変換する JavaScript の長〜いコードをできるだけ短くする!というお題です。 元々2222文字もあるコードを、なんと115文字以下まで減らせます。

みなさまのご参加をお待ちしております!

JS体操第3問「Zalgo Text の生成」

JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com