文字コード地獄秘話 第3話:後戻りの効かないUnicode正規化

はじめに

おっと、またまた会いましたね。文字コードおじさんです。前回、Unicodeにおける結合文字列という話題を取り上げました。思わず「いやあ、結合文字列は強敵でしたね」と口走りそうになる代物でしたが、今回はそれに関連したUnicode正規化のお話をしてみようと思います。

ざっと前回のおさらい

詳しいことは前回の記事をご覧いただくとして、 最低限の用語についてざっくりおさらいしておきましょう

  • 結合文字列
    • 複数の文字を使って見かけ上の1文字を表現する仕組み
    • 「て(U+3066)」 の後に、 「濁点(U+3099)」 を配置することによって 「で」 を表現する
  • 合成済み文字
    • 「で(U+3067)」などのあらかじめ合成されている文字
  • Unicode正規化
    • 結合文字列を合成済みに統一したり、合成済み文字を結合文字列にしたりする処理

少々語弊がありますが、イメージがつかめればOKです。

正規化の4つの方法

Unicode ConsortiumではUnicode正規化について下記の4つの方法を定めています。

  • NFD(Normalization Form Canonical Decomposition)
    • 正準等価性に基づく分解
  • NFC(Normalization Form Canonical Composition)
    • 正準等価性に基づく分解後、正準等価性に基づいて再度合成
  • NFKD(Normalization Form Compatibility Decomposition)
    • 互換等価性に基づく分解
  • NFKC(Normalization Form Compatibility Composition)
    • 互換等価性に基づく分解後、正準等価性に基づいて再度合成

やたら小難しそうな文字が並んでいますが恐れることはありません。

分解とは、合成済み文字を結合文字列に変換するような処理を言います。「で(U+3067)」を分解すると「て(U+3066)」+「濁点(U+3099)」になるといった具合です。

合成は分解の逆で、結合文字列を合成済み文字に変換するような処理を言います。

そして、分解・合成の判断材料に用いるのが等価性(Equivalence)です。Unicodeの正規化においては「正準等価(Canonical Equivalence)」「互換等価(Compatibility Equivalence)」の二種類が規定されています。

正準等価と互換等価

正準等価とは?

正準等価(正規等価ともいいます)については、Unicode Standard Annex #15を見ると、以下のように規定されています。

Canonical equivalence is a fundamental equivalency between characters or sequences of characters that represent the same abstract character, and when correctly displayed should always have the same visual appearance and behavior.

同じ抽象文字を表現した合成済み文字と結合文字列は等価であり、それらが正しく表示された場合は同じ外観と振る舞いを持っているべき、としていますね。わけわかんねぇよトッツァンと思うかもしれませんが、要するに見た目と機能が同じ文字は合成済みだろうが結合文字列だろうが等価だということです。例を挙げてみてみましょう

結合文字列と合成済み文字

  • 「で(U+3067)」
  • 「て(U+3066)」 +「濁点(U+3099)」

これらは正準等価です。わかりやすいかと思います。

結合文字列のうち、順番がことなるもの

  • 「q(U+0071)」+「上付きのドット(U+0307)」+「下付のドット(U+0323)」 の組み合わせ
  • 「q(U+0071)」+「下付のドット(U+0323)」+「上付きのドット(U+0307)」 の組み合わせ

並び順が違いますが、これらは正準等価になります。

ハングル

  • 「가(U+AC00)」
  • 「ᄀ(U+1100)」+「ᅡ(U+1161)」

これらは正準等価になります。結合文字列と合成済み文字に似ていますね。

単体の文字

まぁ、当たり前ですよね。

互換等価とは?

互換等価もまた、Unicode Standard Annex #15に規定されています。内容は以下の通りです。

Compatibility equivalence is a weaker equivalence between characters or sequences of characters that represent the same abstract character, but may have a different visual appearance or behavior. The visual representations of the variant characters are typically a subset of the possible visual representations of the nominal character, but represent visual distinctions that may be significant in some contexts but not in others, requiring greater care in the application of this equivalence. If the visual distinction is stylistic, then markup or styling could be used to represent the formatting information. However, some characters with compatibility decompositions are used in mathematical notation to represent a distinction of a semantic nature; replacing the use of distinct character codes by formatting may cause problems.

ああ、長くて読む気が起きませんなぁ(棒

かいつまんで説明すると、互換等価は正準等価より広い範囲を等価と看做す緩い等価性のことです。正準等価な関係にある文字列は常に互換等価という関係になりますが、その逆は必ずしも成り立ちません。

っていわれてもイメージがつかないと思うのでまた例を挙げてみましょう。

フォントのバリエーション

  • 「H(U+0048:LATIN CAPITAL LETTER H)」
  • 「ℌ(U+210C:BLACK-LETTER CAPITAL H)」
  • 「ℍ(U+210D:DOUBLE-STRUCK CAPITAL H)」

一発目からなかなかハードなものが出てきましたが、上記3つはすべて互換等価です。

しかしながら、正準等価は成り立ちません。そもそも同じ外観を持っていないですから、、、

文字幅の違いや方向の違い

2つ例を挙げてみます

  • 「カ(U+30AB:KATAKANA LETTER KA)」
  • 「カ(U+FF76:HALFWIDTH KATAKANA LETTER KA)」

上記は抽象文字としてはカタカナのカですが、全角と半角という文字幅の違いがありますね。

これらは互換等価ですが、正準等価にはなりません。

  • 「{(U+007B:LEFT CURLY BRACKET)」
  • 「︷(U+FE37:PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET)」

上記は縦と横の違いですね。

もう縦の方の文字名がふざけてるとしか思えませんが、互換等価です。もちろん正準等価ではありません。

上付きや下付き

  • 「9(U+0039:DIGIT NINE)」
  • 「⁹(U+2079:SUPERSCRIPT NINE)」
  • 「₉(U+2089:SUBSCRIPT NINE)」

上付きや下付きのバリエーションでも、互換等価になります。これらは正準等価にはなりません。

丸囲み文字

  • 「1(U+0031:DIGIT ONE)」
  • 「①(U+2460:CIRCLED DIGIT ONE)」

丸囲みでも互換等価です。

組文字1など

  • 「㌀(U+3300:SQUARE APAATO)」
  • 「㈲(U+3232:PARENTHESIZED IDEOGRAPH HAVE)」
  • 「¼(U+00BC:VULGAR FRACTION ONE QUATER)」

だいぶあかんやつが出てきましたね。上から組文字のアパート、有限会社、1/4です。これらはNFKDやNFKCによる正規化を実行すると大変面白い挙動になります。

イメージがつきましたか?

実際に正規化してみた

等価性の話を読んだところで、まぁ4つの正規化の方法のうちどれを選べばいいかというのはなかなか判断がつかないと思います。そこで、今度は同じ文字列に対して4つの正規化を適用し、挙動の差を見てみることにします。

正規化に用いるのはjava.text.Normalizerクラスのnormalizeメソッドです。では早速参りましょう。

例1:シンプルな合成済み文字

コード

実行結果

この例では正準等価でやっても互換等価でやっても差は出ません。分解なら結合文字列に変化し、合成なら合成済み文字なので元の文字列と変化がありません。

正しく描画されていれば4つとも見かけ上は全く区別がつきませんね。

一点注意しなければならないのは、NFC・NFKCともに最終的には合成されますが、その前に必ず分解されている点です。後続の例でその影響がわかります。

例2:順番が変わる結合文字列

コード

実行結果

まず、このパターンだとすべて同じ結果になりますが、ドットの並び順が元の文字列とは異なる順序になります。

NFC・NFKCでも結合文字列のままなのは、対応する合成済み文字が無いというだけです。しかし、どの方法を用いても並び順の変化を確認できるため、先行して分解していることの影響が分かると思います。

なお、正規化によって変化した文字の並び順について、元に戻す術はありません。

例3:半角カタカナ

コード

実行結果

半角カタカナのガを例にしてみました。まず前提として、半角カタカナのガは結合文字列で、対応する合成済み文字はありません。

NFDとNFCは半角の結合文字列のままですが、NFKDとNFKCではそれぞれ全角に変化しています。NFKDでは全角の結合文字列に、NFKCでは全角の合成済み文字にそれぞれ変化しています。

なお、NFKD・NFKCで全角に変化した文字列を、正規化によって半角に戻すことはできません。

例4:上付きと下付き

コード

実行結果

数字の9を、通常・上付き・下付きと並べてみました。正準等価か互換等価かで結果が異なっていますね。

正準等価の場合はいずれも変化しません。

他方、互換等価の場合はすべて通常の9に変化しています。通常の9になった元上付き・下付きの文字を元に戻すことはできません。

例5:組文字

コード

実行結果

組文字のアパートを正規化してみました。だいぶ気持ち悪いですが見てみましょう。

正準等価ではいずれも組み文字のままで変化しません。

互換等価の場合、NFKDだと結合文字列で構成された全角カタカナになります。もはや組み文字ではありません。他方、NFKCでも全角カタカナに変化しますが、こちらは合成済み文字のカタカナのみで構成されます。

もうお分かりかと思いますが、互換等価で正規化した後から組み文字に戻す術はありません。

例6:神と神

コード

実行結果

一見すると、どの正規化の手法をとっても1文字たりとも変化しないように見えますが、現実は3文字目が分解されてしまうという結果になります。アイエエエ!!分解!?分解ナンデ!?

これは3文字目がCJK互換漢字であり、対応するCJK統合漢字が存在するため、統合漢字側に置き換えられてしまうというものです。正規化においては互換漢字の類は存在を許可されないようですね。

分解・合成という用語からはなかなか想像がつかない部分だと思います。

まとめ

  • Unicode正規化には4つの方法がある
  • Unicode正規化の基準となるのが等価性(正準等価・互換等価)
  • Unicode正規化後に変化した文字列は元に戻せないケースがありうる
  • CJK互換漢字などの一部の単体文字は、分解されて元に戻せなくなることがある

こんなところでしょうか。 いやぁキモスキモス。

ではまた。