キモいけどすごいUnicode

#この記事は
Unicode…本当は触りたくない領域だったので無視してきたのですが、ひょんなことから調べざるを得なかったので調べた内容をまとめたものです。

#すごキモい
Unicodeは非英語圏の人たちがテクノロジーへの市民権を得る足掛かりを作ってくれました。
Unicodeはできる限り少ないリソースで多くの言語に対応してくれています。

Unicodeは調べるほどにやっぱりキモいです。直感的に分かることあんまないし。
でもこのキモさは既存の枠組みの中で試行錯誤した凄みを感じるキモさだと分かってきます。

#Unicodeの仕組み
Unicode以前から使われてきたASCIIコードは16進数で00~FFの16*16=256通りが最大。
英語圏の人たちにとってはそれで何ら問題ないのですが、アルファベット以外の記号を
使いたいというニーズに対してASCIIでは応えられないものがありました。

Unicodeの基本的な考え方は、16進数で00~FFの16*16=256通りを1単位として、
これを2単位並べることで256*256=65536通りの文字を表すことにしましょうというものです。

例えばUnicodeで00と24を組み合わせると U+0024 またはプログラム中では \u0024
これは$マークを表示するものとして定められており、
同様に65536通りの文字と対応するコードを定めることができる仕組みです。

16進数から機械語の2進数に直して考えると
00 ~ FF → 0000 0000 ~ 1111 1111　が１単位になっていて、2つ並べると
00 00 ~ FF FF → 0000 0000 0000 0000 ~ 1111 1111 1111 1111 ということになります。
$の例では
00 24 → 0000 0000 0010 0100 となります。

##Unicodeのすごキモい発想
なるほど、じゃあこの65536通りについては2単位だから２バイト文字ということか。

と思ったら
2バイトなのは 00 80 (0000 1000) ~ 07 FF (0111 1111)であって
それ以上は3バイトに変換して表すようになっているんですねー。うーん？なんだそれキモい。

2単位なのになぜ3バイトにするのかという事なんですが、ASCIIとの互換性の問題があります。
例えば先ほどの 00 24 はASCIIコードでは 24 なので、同じ 24 に揃えたい…
でも、ただ00を捨てていいというルールにしてしまうと
U+2424 = ␤ と U+0024 U+0024 = $$
の区別がつかなくなりますよね。

そこで 00 00 ~ 00 7F については頭の00を捨てて1バイトとしてよい代わりに
00 80 ~ 07 FF については 0000 0sss pppp pppp → 110s sspp 10pp pppp (2バイト)
08 00 ~ FF FF については ssss ssss pppp pppp → 1110 ssss 10ss sspp 10pp pppp (3バイト)
としたんだそうです。え、なんか意味あるのコレ。

よく見ると2バイトも3バイトも先頭バイト以外が10xx xxxxになってますよね。
先頭バイトも110x xxxxと1110 xxxxなので、どれも1xxx xxxxです。
1バイト文字は00 ~ 7F (0000 0000 ~ 0111 1111)までしか存在しない為、
1xxx xxxxは元々欠番になっていて絶対に読み間違いが発生しないんです。なるほど！？

でも考えてみると1バイトの部分をASCIIに合わせなければUnicodeをそのまま使えたはずだし、
バイト半分にして切り離そうなんて普通考えます？やっぱりキモい。

##UTF-8とUTF-16
ちなみに、こうしてUnicodeから変換された1~3バイトの文字コードがUTF-8です。
そんなわけでUTF-8を表記するときはUnicode自体のU+0800 (\u0800)という表記と
UTF-8の E0 B0 00 (\xE0\xB0\x00)みたいな表記が出てきます。

一方でASCIIとの互換を捨てればUnicodeをそのまま使える説に則ってできたのがUTF-16です。
U+0000 ~ U+FFFF までならUTF-16は2バイトで表現できるのでUTF-8より高速だとか。

#UTF-8と4バイト文字
めでたくASCIIの128文字から65536文字まで扱えるようになったわけですが、
世界中の文字を集めてみると全く足りないという事実が明らかになります。

Unicodeとしては更に文字を増やすために、桁を1つ追加することになりました。
これによって U+10000 番台が登場します。後の4バイト文字に当たる部分です。
Unicode単体で見れば桁を増やすだけでいいのでもっと多くの文字がカバーできそうですね。

ですが、これをUTF-8に変換するロジックが存在しません。UTF-16もそうです。

##サロゲートペア
そこでUTF-8、UTF-16にはサロゲートペアという大変キモい仕組みが導入されました。
まだU+D800より後ろが空いていたので、U+D800 U+DC00 ~ U+DBFF U+DFFF までを
2コード1文字で使ってしまう案です。これでU+10000番台が表現できるようになりました。

いやさあ、それどう考えてもおかしいよね。
javascriptで4バイト文字を含む文字列の長さを調べるとおかしな結果が返るのはこのせい。
4バイト文字を含んだ文字列をspritするとなかなか楽しいことになります。

##現在のUTF-8
さすがにサロゲートペアの考え方は無理があったのか、現在のUTF-8は
U+10000 ~ U+FFFFF に対して
0000 tttt ssss ssss pppp pppp → 1111 00tt 10tt ssss 10ss sspp 10pp pppp
として表現するようになっています。いわゆる4バイト文字です。 

#U+10000 ~ に対する対応状況
結構バラバラみたいで個別に調べないとダメっぽい。
##MySQL
文字コードutf8だと非対応ですが、utf8mb4にすると対応してくれます。

##フォント
Noto sansはどうやら対応しているっぽいです。
ヒラギノは対応してません。つまり使う前に検討したほうが良い。

##javascript
UTF-16で保持されているのでサロゲートペアでの対応になります。非常にめんどい。
ただ \u0000 式の記法が標準的に対応しているので、U+10000 以降をサポートしない場合は
/^[\u0000-\uD7FF]+$/ とこれだけでフィルタできるっぽいです。

一応MDN曰く \u{10000} みたいな書き方でマッチングできるらしいが…地雷があるとか。

#終わりに
日本人の私らなんかからするとUnicodeによる恩恵は非常に大きくて、
例えばjavascriptのハッシュキーに平気で漢字突っ込めるのはUnicodeのおかげなんですよね。

有難いことに日本語は優先的にUnicodeに採用された文字が多くて、
常用レベルではU+D7FFまでで問題ないですが、絵文字使うにはU+10000 以降必須だし、
非英語圏代表としてUnicodeを使いこなしたいものです。

#追記
コメントで記事のあいまいな部分を補足していただいています。そちらもぜひ併せてご参照ください。