アジョブジ星通信

進捗が出た頃に更新されるブログ。

Unicode正規化を実装する (3) 正規合成

バックナンバー

  1. Unicode正規化を実装する (1) UCDにふれる - アジョブジ星通信
  2. Unicode正規化を実装する (2) 正規分解・互換分解 - アジョブジ星通信

PCの死亡を言い訳に3ヶ月空いてしまいましたが、その間に Unicode 9.0.0 がリリースされたようです。サンプルリポジトリに入っている UCD のコピーを 9.0.0 にアップデートしました。 NormalizationTest.txt のテストケースは前回のサンプルコードのまま問題なくクリアしています。

さて、今回は合成です。合成は分解テーブルのキーと値が逆のテーブルを作って、ひたすらルックアップしていく作業になります。ただし、合成できる文字には細かい規定があるので気をつけましょう。

前回同様、サンプルと見比べながら読み進めてもらえると良いです。結構ファイルが分散しているので、「定義へ移動」が使える Visual Studio を使うと読みやすいかと思います。

目次

合成テーブルをつくる

合成に必要なテーブルは、合成テーブルと、前回作った CCC テーブルです。

合成テーブルは次のようなものにします。

キー
Decomposition_Mapping (2文字) コードポイント

正規合成は、 Decomposition_Mapping が指定されている文字すべてを逆変換すれば良いわけではなく、いくつか条件があります。条件は以下のとおりです。

  1. Decomposition_Type がない = 正規分解である
  2. 1文字の変換ではない = 正規分解すると2文字になるもの
  3. Full_Composition_Exclusion プロパティがついていない

1, 2 については分解テーブルを作るときに扱った内容なので、すぐにわかると思います。 3 は UCD の DerivedNormalizationProps.txt に書かれています。

# Derived Property: Full_Composition_Exclusion
#  Generated from: Composition Exclusions + Singletons + Non-Starter Decompositions

0340..0341    ; Full_Composition_Exclusion # Mn   [2] COMBINING GRAVE TONE MARK..COMBINING ACUTE TONE MARK
0343..0344    ; Full_Composition_Exclusion # Mn   [2] COMBINING GREEK KORONIS..COMBINING GREEK DIALYTIKA TONOS
0374          ; Full_Composition_Exclusion # Lm       GREEK NUMERAL SIGN
037E          ; Full_Composition_Exclusion # Po       GREEK QUESTION MARK
0387          ; Full_Composition_Exclusion # Po       GREEK ANO TELEIA
0958..095F    ; Full_Composition_Exclusion # Lo   [8] DEVANAGARI LETTER QA..DEVANAGARI LETTER YYA
09DC..09DD    ; Full_Composition_Exclusion # Lo   [2] BENGALI LETTER RRA..BENGALI LETTER RHA
以下略

DerivedNormalizationProps.txt には他にもいろいろなプロパティが定義されていますが、 Full_Composition_Exclusion だけ取り出してください。「..」は範囲を表していて、「0958..095F」ならコードポイントが 0958 以上 095F 以下の文字すべてが対象であることを表しています。

仕様通りの手順

正規合成なら正規分解、互換合成なら互換分解を行います。

次に、文字列の2文字目から最後まで、繰り返し次のことを実行します。ここで、対象としている文字を C とします。

  1. C より前の CCC が 0 の文字を探す。見つけたら、それを L とする(用語としては、「スターター」といいます)。
  2. 合成テーブルから L, C をキーとするエントリーを探す。
  3. エントリーが存在し、かつブロック(後述)されていないならば、 L を合成結果で置き換え、文字列から C を削除する。

ブロックとは、 L と C の間の任意の文字の CCC を x、 C の CCC を y とすると、 x ≧ y を満たしている状況を言います。ただし、 C が L の次の文字で、 C の CCC が 0 のときはブロックされていません。ところで、仕様上は「x ≧ y」ですが、分解のときに正規順序に並び替えを行っているので、実質 x = y です。

例として、 U+0044 U+031B U+0323(Ḍ̛)を合成してみましょう。関係する文字の情報は以下のとおりです。

コードポイント 実際の文字 CCC Decomposition_Mapping
0044 D 0
031B ̛ 216
0323 ̣ 220
1E0C 0 0044 0323

順番にやっていきましょう。まず、2文字目からスタート。

文字 U+0044 U+031B 0+0323
CCC 0 216 220
L C

「0044 031B」にマッチする合成はないので次に行きます。

文字 U+0044 U+031B 0+0323
CCC 0 216 220
L C

「0044 0323」にマッチする「1E0C」があり、 CCC も 216 < 220 なので、合成できます。

文字 U+1E0C U+031B
CCC 0 216

これ以上文字はないので、これで終了です。

実装するための手順

毎回「C より前の CCC が 0 の文字を探す」のは非常に無駄なので、 CCC が 0 の文字に出会った時にそれを変数に入れておくという方法をとっておけば、効率化できそうです。

というわけで具体的な手順としては、このようになります。

  1. 最初のスターターを探しておく
  2. 最初のスターターの次の文字からループ
    1. 1つ前の文字との比較でブロック判定(正規順序を信じる)
    2. 合成できるならば、スターターを合成結果で置き換え、現在見ている文字を削除
    3. 合成できない、かつ CCC が 0 ならば、スターターを更新

サンプルを見るとわかりますが、変数をいっぱい使うことになります。

また、サンプルでは、「現在見ている文字を削除」の部分は、 insertIndex 変数をインクリメントしないことで実現しています。

ハングルの扱い

前回と同じく Unicode 8.0.0 Core Specification 3.12 Conjoining Jamo Behavior のサンプルコードを見ましょう!

public static String composeHangul(String source) {
    int len = source.length();
    if (len == 0) return "";
    StringBuffer result = new StringBuffer();
    char last = source.charAt(0); // copy first char
    result.append(last);

    for (int i = 1; i < len; ++i) {
        char ch = source.charAt(i);

        // 1. check to see if two current characters are L and V
        int LIndex = last - LBase;
        if (0 <= LIndex && LIndex < LCount) {
            int VIndex = ch - VBase;
            if (0 <= VIndex && VIndex < VCount) {

                // make syllable of form LV

                last = (char)(SBase + (LIndex * VCount + VIndex) * TCount);

                result.setCharAt(result.length()-1, last); // reset last
                continue; // discard ch
            }
        }
        
        // 2. check to see if two current characters are LV and T
        int SIndex = last - SBase;
        if (0 <= SIndex && SIndex < SCount
                && (SIndex % TCount) == 0) {
            int TIndex = ch - TBase;
            if (0 < TIndex && TIndex < TCount) {

                // make syllable of form LVT
                
                last += TIndex;
                result.setCharAt(result.length()-1, last); // reset last
                continue; // discard ch
            }
        }
        // if neither case was true, just add the character
        last = ch;
        result.append(ch);
    }
    return result.toString();
}

やっていることは、分解の逆なのですが、 L と V の合成と LV と T の合成の処理が分かれているので長くなっています。

次回予告

はい、お疲れ様でした。テスト方法は分解のときと同じなので省略します。

次回は、クイックチェックを使った高速化方法を紹介して、このシリーズを終了させたいと思っています。よろしくお願いします。

Unicode正規化を実装する (4) クイックチェック - アジョブジ星通信