--- breaks: false tags: public-tech --- # ある Unicode 文字が Shift_JIS(or CP932)で表現できるかを巨大 switch 文で調べる 人生をやっていると、ある Unicode 文字が Shift_JIS で表現できるかどうかを調べたくなることがある。大抵こういうのは言語の標準ライブラリでカバーされていたりするから悩まないのだが、残念ながら Elixir にはそういうのが無いっぽかったので、作った。 ## 何をやるか Unicode の code point を表す一つの整数値が与えられるので、その文字が SJIS で表現できるかを調べる。例えば「あ」(U+3042)では `0x3042` が与えられて `true` を返すし、「髙」(U+9AD9)では `0x9ad9` が与えられて `false` を返す。 ## Shift_JIS と SJIS と CP932 と MS932 と Windows-31J どれも文字コードを表す名称だが、指すものが微妙に違う。詳細は[ここ](http://una.soragoto.net/topics/13.html)などを参照してほしい。大雑把に言うと Shift_JIS を拡張したものが CP932 で、CP932, MS932, Windows-31J は同じものと思って良さそう。やっかいなのは SJIS で、Shift_JIS の別名かと思いきや、[Ruby では CP932 を指すらしい](https://developers.freee.co.jp/entry/sjis-is-not-an-alias-of-shift-jis)[^ruby-cp932]。ここでは CP932 をターゲットにする。 [^ruby-cp932]: そうはならんやろ。 ## 方針 あらかじめ Unicode code point のうち CP932 に存在する値だけを選び出しておき、これらを使って巨大な分岐を書く。入力された Unicode code point をこの分岐に通すことで判定を行う。あとに示すようにこの分岐は4000通り以上になるので、パフォーマンスの観点から Elixir でそのまま書かずに C で書いて NIF として使うことにする。 結局、C で巨大 switch 文を書けばよい。 ## 巨大 switch 文を作る [ここ](http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT)に Unicode と CP932 の対応表が plain text で置いてあるので、これをとってきて、適当にシェルを叩いて `case` 文に変形する。 ``` $ curl http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT | \ egrep -v '^#'| \ awk -F "\t" '{ print $2 }' | \ egrep -v '^\s*$' | \ awk '{ print strtonum($0) }' | \ sort -nu | \ awk '\ BEGIN{p=-1; printf "0"} \ { if($0==p+1){p += 1}else{printf "--%d\n", p;printf $0;p=$0}} \ END { printf "--%d\n", p }' | \ sed -E 's/^([0-9]+)--([0-9]+)$/case \1 ... \2:/' \ > check_sjis.c ``` 実行すると `check_sjis.c` というファイルができて、その中に次のように出力される。4247行ある。ちなみに `case n ... m` という表記は GCC 拡張である([Case Ranges](https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html))[^case-ranges]。 ``` case 0 ... 127: case 167 ... 168: case 176 ... 177: case 180 ... 180: case 182 ... 182: case 215 ... 215: case 247 ... 247: case 913 ... 929: case 931 ... 937: case 945 ... 961: (中略) case 40845 ... 40845: case 40853 ... 40853: case 40860 ... 40861: case 40864 ... 40864: case 63785 ... 63785: case 63964 ... 63964: case 64014 ... 64045: case 65281 ... 65374: case 65377 ... 65439: case 65504 ... 65509: ``` [^case-ranges]: GCC 拡張なんか使わずに `case X` を並べればええやんというのはその通りなのだが、これを作ったときの自分は `case n ... m` が C11 から導入されたつもりでいたのだった。全然そんなことなかった。 あとはこの前後にいい感じに C コードを書くとちゃんと動く。ちゃんと動く例は[ここ](https://github.com/ushitora-anqou/daidoquer2/commit/869ae6f55b0d8b51c286b5f4b648511479ac47d5)にある。 ## おまけ:Shift_JIS を使いたい場合 対応表が[ここ](http://x0213.org/codetable/sjis-0213-2004-std.txt)にあるので、似たようにやる。 ``` $ curl http://x0213.org/codetable/sjis-0213-2004-std.txt | \ egrep -v '^#'| \ awk '{ print $2 }' | \ awk 'match($0,/^U\+([0-9A-F]+)$/,a){ print strtonum("0x" a[1])}' | \ sort -n | \ awk '\ BEGIN{p=-1; printf "0"} \ { if($0==p+1){p += 1}else{printf "--%d\n", p;printf $0;p=$0}} \ END { printf "--%d\n", p }' | \ sed -E 's/^([0-9]+)--([0-9]+)$/case \1 ... \2:/' \ > check_sjis.c ``` ## まとめ これ本当にこんな泥臭いことしないといけないのかなぁ。いや結局誰かがやらないといけないとは思うのだが。 ## [2023/10/15 追記] OCaml で同じことをやる OCaml でも同じことをする必要が出てきたので、次のようなスクリプトを書いた。 ``` curl http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT | \ egrep -v '^#'| \ awk -F "\t" '{ print $2 }' | \ egrep -v '^\s*$' | \ awk '{ print strtonum($0) }' | \ sort -nu | \ awk '{ printf "| " $1 "\n" }' ```