アジョブジ星通信

日常系バンザイ。

Unicode正規化を実装する (2) 正規分解・互換分解

前回: Unicode正規化を実装する (1) UCDにふれる - アジョブジ星通信

さて、前回 UnicodeData.txt の読み方をやりましたので、これを使って実際に正規分解・互換分解を実装してみましょう。

サンプルコードと見比べながら説明を読んでいただければと思います。

目次

分解テーブルとCCCテーブルをつくる

正規化の主な作業はテーブルのルックアップです。というわけでまずはテーブル作りから。

つくるテーブルは

キー
コードポイント Decomposition_Type, Decomposition_Mapping

キー
コードポイント Canonical_Combining_Class

です。

それぞれ、分解できないものや CCC が 0 のものをテーブルに入れる必要はありません。不要なメモリはつかわないっ!

分解テーブルに沿って分解する

分解は再帰に行います。ある文字を分解マッピングが「XY」の 2 文字だったとしたら、さらに X と Y に対してそれぞれ分解を試みる必要があります。

前回の記事で説明したように、 Decomposition_Mapping にはコードポイントが並んでいるだけのものと、「<形式>」のあとにコードポイントが並んでいるものがあり、それぞれ正規分解マッピング、互換分解マッピングといいます。正規分解時には正規分解マッピングを使用し、互換分解時には両方マッピングを使用します。

試しに U+0390(ΐ) を正規分解してみましょう。関連する文字の Decomposition_Mapping はこのようになっています。

コードポイント(昇順) Decomposition_Mapping
0301
0308
0390 03CA 0301
03B9
03CA 03B9 0308

図で表すとこんな感じ。

0390
 ├ 03CA
 │ ├ 03B9
 │ └ 0308
 └ 0301

というわけで U+0390 の正規分解結果は

03B9 0308 0301

となります。正規分解なので表示はまったく同じです。

ハングル音節文字を分解する

ハングルとかよく知らないので知らないんですけど、音節文字(よくみるハングル)は、初声(L)、中声(V)、終声(T)から成り立っていて、 Unicode にはこれらバラバラの字母と LV 型、 LVT 型の音節文字が含まれているそうです。知らんけど。

で、この音節文字には Decomposition_Mapping は設定されていません。しかし分解してあげる必要があります。どうやって?以下 Unicode 8.0.0 Core Specification 3.12 Conjoining Jamo Behavior に書いてあるサンプルコードです。

static final int
    SBase = 0xAC00,
    LBase = 0x1100, VBase = 0x1161, TBase = 0x11A7,
    LCount = 19, VCount = 21, TCount = 28,
    NCount = VCount * TCount, // 588
    SCount = LCount * NCount; // 11172
 
public static String decomposeHangul(char s) {
    int SIndex = s - SBase;
    if (SIndex < 0 || SIndex >= SCount) {
        return String.valueOf(s);
    }
    StringBuffer result = new StringBuffer();
    int L = LBase + SIndex / NCount;
    int V = VBase + (SIndex % NCount) / TCount;
    int T = TBase + SIndex % TCount;
    result.append((char)L);
    result.append((char)V);
    if (T != TBase) result.append((char)T);
    return result.toString();
}

どうしてこのようになるのか、それは計算すればパパっと求められる、そういう仕様にしたからです。説明放棄してるように見えるかもしれませんが、説明しようにもこうやって分解できるように仕様が制定されてるとしか言いようがないです。。

というわけで、 SIndex を計算して範囲内ならばハングル音節文字として分解、そうでないならば分解テーブルを使って分解するようにしましょう。

正規順序(Canonical Ordering)に並べ替える

分解が終わったら、次はソートのお時間です。これをやるまでが NFD, NFKD の仕様なので頑張りましょう。

やることは CCC を使ったソートです。 CCC が 0 ではない連続した 2 文字 A, B が CCC(A) > CCC(B) ならば、順番を入れ替えて B, A とします。つまり CCC の昇順に並べ替えろということです(と言ってしまうと間違った実装をしてしまうので、定義を注意深く覚えておいてください)。

例はこちらを見ていただくとわかりやすいかと: http://nomenclator.la.coocan.jp/unicode/normalization.htm#ordering

完成した正規化の実装をテストする

UCD の NormalizationTest.txt には正規化できる全文字と、引っかかりやすい仕様の重箱の隅をつつくようなテストが収録されています。これをクリアできれば、あなたの実装は成功です。

使い方は、最初の方にコメントで書いてあるのでわかると思います。今回は NFD と NFKD の実装を行ったので、それに対するテストを実行してみましょう。

次回予告

はい、お疲れ様でした。分解に関しては、本当に用意されているマッピングに沿ってやっていくだけなので簡単だと思います。

次回は合成について説明します。また3週間くらい空いちゃったらごめんね!!

追記: 3週間と言っていたら3ヶ月になってしまった

Unicode正規化を実装する (3) 正規合成 - アジョブジ星通信