Unicode正規化を実装する (3) 正規合成
バックナンバー
PCの死亡を言い訳に3ヶ月空いてしまいましたが、その間に Unicode 9.0.0 がリリースされたようです。サンプルリポジトリに入っている UCD のコピーを 9.0.0 にアップデートしました。 NormalizationTest.txt のテストケースは前回のサンプルコードのまま問題なくクリアしています。
さて、今回は合成です。合成は分解テーブルのキーと値が逆のテーブルを作って、ひたすらルックアップしていく作業になります。ただし、合成できる文字には細かい規定があるので気をつけましょう。
前回同様、サンプルと見比べながら読み進めてもらえると良いです。結構ファイルが分散しているので、「定義へ移動」が使える Visual Studio を使うと読みやすいかと思います。
目次
合成テーブルをつくる
合成に必要なテーブルは、合成テーブルと、前回作った CCC テーブルです。
合成テーブルは次のようなものにします。
キー | 値 |
---|---|
Decomposition_Mapping (2文字) | コードポイント |
正規合成は、 Decomposition_Mapping が指定されている文字すべてを逆変換すれば良いわけではなく、いくつか条件があります。条件は以下のとおりです。
- Decomposition_Type がない = 正規分解である
- 1文字の変換ではない = 正規分解すると2文字になるもの
- 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 とします。
- C より前の CCC が 0 の文字を探す。見つけたら、それを L とする(用語としては、「スターター」といいます)。
- 合成テーブルから L, C をキーとするエントリーを探す。
- エントリーが存在し、かつブロック(後述)されていないならば、 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つ前の文字との比較でブロック判定(正規順序を信じる)
- 合成できるならば、スターターを合成結果で置き換え、現在見ている文字を削除
- 合成できない、かつ 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 の合成の処理が分かれているので長くなっています。
次回予告
はい、お疲れ様でした。テスト方法は分解のときと同じなので省略します。
次回は、クイックチェックを使った高速化方法を紹介して、このシリーズを終了させたいと思っています。よろしくお願いします。