[Java]IVSの異体字を元の字と同一視して比較する

IVSと正規化について - Togetter

正直あまりちゃんと付いていけてないんだけど、結局のIVS入りの文字列をVS抜きのものとを同一と看做してワンメソッドで比較するような仕組みはJava 6でも入ってないらしい。

リンク先より引用:

@ogwata @moji_memo IVSがそもそもUnicode正規化の対象外なのだと思います。互換漢字を統合漢字にする合成除外とは違って、IVSを基底文字だけにするのならVSを削除しちゃえばいいだけですし。

ということでVSを削除するコードを書いてみたり、異体字を実際に表示してみたりした。

予備知識

JavaのStringとcodePointの扱いについてはこのへんを参照。


IVSについてはUTS #37: Ideographic Variation Databaseを参照。統合漢字(UNIFIED IDEOGRAPH)の後ろにU+E0100〜U+E01EFのコードポイント一つを付けることで、元の漢字の異体字を表現するものらしい。


例えば「芦(U+82A6)」に「VARIATION SELECTOR-18(U+E0101)」を続けることで芦の異体字「芦󠄁(<82A6 E0101>)」*1を表す仕組みらしい。


つまり元の文字に戻すには、文字列の前からコードポイントを見ていってVSが来たらスキップするだけで本当に良いっぽい。

環境

OSはMac OS X 10.6.6 (Snow Leopard)
JavaJ2SE 1.6.0_22


異体字を実際に表示してみる為に、IVS対応フォントの例としてWikipediaに載ってた「Y.OzFont」というフォントをインストールしてみた。(http://yozvox.web.infoseek.co.jp/)


フォントをインストールするのが面倒なら文字列を「Flash Text Engine によるIVS表示デモ - しろもじ作業室」というページにコピペして確認しても良いかも。*2


ちなみにインストールしたのは「ペン字版」のところにある「Y.OzFont TTCパック」のVer.13.03というやつ。ダウンロード&解凍して、「YOzBN.TTC」をFont Book.appでインストールし、表示確認の際には「YOzFontN90」ファミリというのを使った。


ファイルの圧縮フォーマットが7zなのだが、手元で解凍するツールが無かったのでp7zipをインストールしてコマンドラインから解凍した。

ソースコード

例によって動かしやすいようにJUnitのUnitTestの一部として書いているので、もし使いたかったUtilsクラスにでも移動したりする方が良いかも。
内部イテレータいいよね。処理を追加したい時はチェーンにしてもよい。

package sample.ivs;

import java.io.File;
import java.io.IOException;
import java.lang.Character.UnicodeBlock;

import junit.framework.TestCase;

public class RemoveVSTest extends TestCase {
    /** コードポイントを逐次的に受け取って処理するインタフェース */
    public interface CodePointProcessor {
        void process(int codePoint);
    }
    /** 文字列をCodePointに分解してCodePointProcessorに渡すメソッド */
    private static void explode(String source, CodePointProcessor processor) {
        for (int i = 0, c = source.length(); i < c; i = source.offsetByCodePoints(i, 1)) {
            processor.process(source.codePointAt(i));
        }
    }
    /**
     * @return 文字列を受け取りVSを取り除いて戻す。
     * @param source 元の文字列。null不可。
     */
    public static String removeVariationSelector(String source) {
        final StringBuilder stringBuilder = new StringBuilder();
        explode(source,
            new CodePointProcessor() {
                public void process(int codePoint) {
                    if (UnicodeBlock.of(codePoint) != UnicodeBlock.VARIATION_SELECTORS_SUPPLEMENT) {
                        stringBuilder.appendCodePoint(codePoint);
                    }
                }
            });
        return stringBuilder.toString();
    }

    // 実行サンプル
    public void testIVS() throws IOException {
        String sample = "芦田さんは芦\uDB40\uDD01屋のお嬢様だ";
        String normal = "芦田さんは芦屋のお嬢様だ";

        assertEquals(normal, removeVariationSelector(sample));
    }
}

簡単な説明

上の実行確認用のISVの方の文字列、

        String sample = "芦田さんは芦\uDB40\uDD01屋のお嬢様だ\n";

"\uDB40\uDD01"というのはVARIATION SELECTORの"U+E0101"をUTF-16で表現したもの。上記のJ2SE 5.0 Tiger 虎の穴のページに変換の仕方が書いてるのでそれを見て変換するか、直に書きたいなら下のようなやり方でも良いかも。

        //まあどれかで。
        String sample = String.format("芦田さんは芦%c屋のお嬢様だ", 0xE0101);
//      String sample = "芦田さんは芦"+ String.format("%c", 0xE0101) +"屋のお嬢様だ";
//      String sample = "芦田さんは芦"+ String.valueOf(Character.toChars(0xE0101)) +"屋のお嬢様だ";

ソースコード中の処理の内容は、一旦コードポイントに分解した後、コードポイントの種類がUnicodeBlock.VARIATION_SELECTORS_SUPPLEMENTだったらスキップし、それ以外ならStrinbBuilderにapeendしていって、最後に文字列化しているだけです。

IVSによる異体字のサンプル

折角なのでTextEdit.appで表示してみた。
まずファイルに出力させる。(commons-io-1.4.jar使用。)

import org.apache.commons.io.FileUtils;
...
        FileUtils.writeStringToFile(new File("sample.txt"), sample + "/" + normal, "UTF-8");

出力されたsample.txtをTextEdit.appで(文字コードUTF-8を指定して)開いて、フォントパネルからフォントを「YOzFontN90 48.0pt」に変更して見たのが下の状態。芦屋の方の芦の字が違う。



一応ページ上で表示すると「芦田さんは芦󠄁屋のお嬢様だ」となる。(多分表示できなくて、豆腐かなにかになっているはず。)


IVD(Ideographic Variation Database)には「漢字は常用漢字の字形など日本において標準的な字形も登録されて」*3いるらしく、例えば、「MacBook Air 11インチ欲しい!」というまったく同じ字形に対して、以下のような二つの異なった表現も存在する。

        String normal = "MacBook Air 11インチ欲しい!";
        String sample = "MacBook Air 11インチ欲\uDB40\uDD00しい!"; // \uDB40\uDD00はU+E0100

*1:多分表示されてないけど

*2:すみませんちゃんと確認したら結局フォントをインストールしないと見えないらしい。

*3:前述のWikipedia参照