Unicode覚え書き

前にも書いたとおりAdaのStringは規格ではLatin-1*1ということになってまして、IO関係はWide_Text_IOであってもファイル名は軒並みStringですので、規格を鵜呑みにすると、Latin-1以外の文字が使われたファイル名を扱う方法が全くないとか、ワイド文字を識別子に使える規格なのに識別子を返す系の関数でLatin-1に収まらない文字がどうなるかはimplementation-definedだったりするとか、Wide_Characters.HandlingやWide_Wide_Maps.Wide_Wide_ConstantsがLatin-1の範囲外を扱わないと規格に明記されちゃってて仮に真面目にワイド文字を扱う実装をしたら逆に違反になってしまうとか、とにかく酷いとしか言いようがないわけです。
多バイト文字圏の人はAdaなんて使っちゃダメですマジで……。

とりあえず私の自作ランタイムはこの点については規格を無視してStringはUTF-8として作ってます。
で、ちょうどWide_Characters.HandlingやWide_Wide_Maps.Wide_Wide_Constantsあたりの実装をしてまして、真面目に規格を無視してワイド文字を扱う実装をしようとして、UCDを調べたりしてます。

以下具体例に乏しいのは、例に使える文字を探し出してくるのが面倒は、はてなEUC-JPだからですっ!

NFC/NFD

まかーであれば必然的に出くわす問題として、合成文字があります。具体的には「が」を表すために「が」単独の文字の他に「か」に濁点を続けたものを「が」として扱わなければならないルールで、結果として同じ文字に複数の表現ができてしまいました。世界中の全文字を2バイトに収めようとした馬鹿の考えた馬鹿な規格らしいです。
合成(後の単独の文字)をNFC、分解(後の複数のコードポイントからなる文字)をNFDと呼びます。
UCDで各文字にCombining Classという数値が定義されてまして、0が単独の文字、1以上のものが合成用です。0の文字の後ろに1以上の文字が続いていたら併せてひとつの文字として扱わないといけません。表引きをしないといけない分計算で済むサロゲートペアなんかよりもよっぽどタチが悪いです。(こっからここまでの範囲は合成用、とかそういうのはなくて、散らばってます……)

普通に考えてこんなことやってられませんので、世の中の大抵のアプリケーションは、Unicode対応を謳っていても合成文字については華麗にスルーしています。

なお、ここで言う濁点は、合成専用の濁点(COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK)で、JIS系のコードにも存在する単独の濁点「゙/゛」とは全く別のコードです。ここを誤解してる解説も多かったような。単独の濁点は単独の濁点で全角/半角ともに存在しており、JIS系のコードからUnicodeにしただけでは合成の対象にはなりません。(NFC/NFDよりも多くの文字を正規化するNFKC/NFKDでは対象ですが、NFC/NFDですらいろんな意味でハア?なのに、NFKC/NFKDなんて実行するアプリは無いと思っていいです)

で、まあ、世の中全部NFCなら平和なんですが、HFS+というファイルシステムは「一部の文字」をNFDで記録しますので、まかーは泣くわけです。(NFC形式が用意されておらずNFDでないと表現できないレアな文字もありますのでそういうレアな文字を使わないといけない人も泣いてると思います)

この「一部の文字」というのが更に厄介で、UCDの表のとおりに真面目に正規化をすると、NFC/NFD以外にも、旧字体新字体が全部同じ文字になってしまいます。まあNormalizationということで間違いはないんでしょうが、そういうのはNFKC/NFKDにしろよ馬鹿が……。というわけで合成文字をスルーしていない(でも手抜きで正規化だけで済ましている)アプリの中には、旧字体がいつのまにか新字体に化けてしまうものもそれなりの数がある……ようです。

例えばMac用のsvnのパッチ(MacPortssvn+unicode_pathをインストールすると入るやつ)はNFCに正規化してるだけですので、濁点問題が解決されている代わりに、「神」の旧字体なんかは「神」にされてしまいます。

それを避けるため、HFS+で使われている正規化は、UCDで定義されている合成分解のうち、字形の変わらない一部の文字のみをNFDに分解する、という少し賢いものです。はっきり言ってNFDをNFCに揃えるだけの用途では、全アプリはこのテーブルを使うべきでは……。フルのNFC/NFDなんて曖昧検索をするときだけで充分ですよ、ええ……。……と思ってしまうのはわたしがまかーの端くれだからでしょうか。

ちなみにHFS+以外のファイルシステム上でNFDでファイル名を付けると、NFCのファイル名とは別のファイルとして作れてしまうらしいです。
これ絶対Unicodeの全文字を含むUnicodeよりも賢い次の規格(当然同じ文字に複数の表現があるとかたわけた仕様は無し)ができたとき、互換性で問題になるんだぜ……。

Combining Class

その合成用の文字に付いてるCombining Classの数字ですが、なんで0か1かだけじゃなくて2以上もあるのかっていうと、どうもひとつの文字に複数の修飾が付くことがあるらしくて、そういう時はCombining Classの小さい順にソートしないといけないようです。
で、よくわからんヨーロッパの謎な言語(失礼!)の中には、合成順で別の文字になるのがあるらしいです。
アルファベットの上に点々を描いてその上に横線を引いたのと、上に横線を引いてその上に点々を描いたのとが別とか。こういうのはCombining Classが同じ数字になってるらしいです。
つまりここで使うソートは安定なソートでなければなりません。ライブラリにある実装の詳細が規定されてないソートなんて怖くて使えません。
正直最初から正しい順番で入力しろよ正規化ついでに直したりしないよバーカバーカでいいと思います……。

Case Folding

文字ケースを無視して比較を行うときに、一旦全ての文字を大文字に変換して比較、または小文字に変換して比較しているアプリは多いと思います。
しかし、よくわからんヨーロッパの謎な言語(失礼!)の中には、大文字小文字の対応が実によくわからんことになってるものがあり、複数の大文字がひとつの小文字に対応したり、その逆だったりで、どちらかに揃えるやり方ではこういうのを取りこぼす可能性があります。
で、大文字小文字変換とは別にCaseFolding.txtってのがありまして、変な対応をしてる文字も全部同じ文字に揃えてくれる変換テーブルになってます。文字ケースを無視したいときはこっちを使いましょう。
正直こんなの取りこぼしても日本人は何も困りませんよねええ……。

General Category

文字の分類で、30個ぐらいあります。
これは大文字とか小文字とか数字とか。iswlowerやiswupper用のデータと思っとけばいいです。
で……悩むのがiswxdigit(AdaならIs_Hexdecimal_Digit)の定義。
十進数のDecimal_Numberのほうにはローマ数字の0から9までの他に、各国の数字が収録されてますが、「各国のアルファベットでAからFまで」なんて便利な分類はありませんし、そもそもiswxdigitを使いたいときって16進数のパースでしょうから、非ASCIIな文字でTrueになっても困る気がします。
いやそれを言ったらiswdigit(Is_Decimal_Digit)を10進数のパースに使ってASCIIの'0'から'9'以外でTrueになっても困りますが、10進数についてはDerivedNumericValues.txtという便利なテーブルがありますのでどうにかしようと思えばどうにかなります。でも0から9は各国の文字を受け付けるのに、'A'から'F'はASCIIでないといけないってのもアンバランスですよね。Cランタイムはどうなってるんでしょう……。(←iswdigitの実際の挙動を試すのをサボってます)

考えてるのが、Is_Decimal_DigitとIs_Hexdecimal_DigitはASCIIのみ、Is_Alphanumericを使うのは識別子用でしょうからこっちはGeneral Category上で文字や数字になってるのは全部True。

参考URL

http://homepage1.nifty.com/nomenclator/unicode/index.htm
私のメモなんか信用せずにこっちを読むべき。

*1:Unicodeの最初の256文字に相当