Qiitaを使うのは初めてなので、まずは気軽なネタを書こうと思う。
Wikipediaの中文(中国語)のページでは、繁体字と簡体字を切り替えることが出来る。簡体字は主に中華人民共和国(Mainland China)で使われている文字で、繁体字は主に台湾や香港で使われている。簡体字と繁体字は等価交換できる。今回はそのコードを動かしてみよう。
簡体字の文字と繁体字の文字は、どちらもUnicode Character Database (UCD)に別々の文字として定義されている。「同じ文字」として同一のコードポイントが割り当てられているというようなことはない。Unicodeで定義されている膨大な漢字の情報は、Unicode仕様の一部として、Unicode Han Database (unihan)と呼ばれるデータベース上に定義されている。これはUnicodeコンソーシアムで行われたHan Unificationという活動の成果だ。unihanデーターベースは、zipファイルで5〜6MB近くある、それなりに大きいファイルだ。
このunihanデータベースでは、それぞれの文字について「フィールド」を定義している。フィールドの定義はUTR #38という文書にまとめられているが、その中にkTraditionalVariantとkSimplifiedVariantというものが定義されていて、これらには、その文字に対応する繁体字・簡体字の代替が規定されている。この情報は、unihanデータベースの中では、Unihan_Variants.txtというファイルで定義されている。例えば、U+343D (㐽) については、次のように定義されている。
U+343D kTraditionalVariant U+5051
'㐽'に対応する繁体字は'偑'ということになるわけだ。もちろん、これに対応する定義がU+5051にもある。
U+5051 kSimplifiedVariant U+343D
というわけで、マッピングの情報をどうすればいいのかはわかった。ではコードにしてみよう。このUnihan_Variants.txtを読んで、簡体字-繁体字の2文字を繋げた文字列を作る。中にはUnicode BMP範囲外の文字もあるので、その場合はサロゲートペアを出力するようにする。
static string GenerateTraditionalVariantsMap (string unihanVariantsFile)
{
StringBuilder sb = new StringBuilder ();
int n = 0;
foreach (var line in File.ReadAllLines (unihanVariantsFile)) {
var ents = line.Split (null);
if (ents.Length < 3 || ents [1] != "kTraditionalVariant")
continue;
AppendHexAsChar (sb, ents [0]);
AppendHexAsChar (sb, ents [2]);
if (++n % 16 == 0)
sb.Append ('\n');
}
return sb.ToString ();
}
static void AppendHexAsChar (StringBuilder sb, string s)
{
int x = int.Parse (s.Substring (2), NumberStyles.HexNumber);
if (x < 0x10000)
sb.Append ((char) x);
else
sb.Append ((char) ((x - 0x10000) / 0x400 + 0xD800)).Append ((char) ((x - 0x10000) % 0x400 + 0xDC00));
}
マッピングを読んで文字列から変換するコードは書いていないので今回はとりあえずここまで…じゃいくらなんでもアレなので一応。20万文字くらいあるstringからIndexOf()を呼んでいるので大変効率が悪いからKeyValuePairの配列を作ってArray.BinarySearchを呼んだほうがいいと思う。
static string ConvertToTraditional (string variantsFile, string textFile)
{
var sb = new StringBuilder ();
var db = GenerateTraditionalVariantsMap (variantsFile);
Console.WriteLine ("Database size: " + db.Length);
char hs = '\0';
int c = 0;
foreach (var ch in File.ReadAllText (textFile)) {
if (char.IsSurrogate (ch)) {
if (hs == '\0')
hs = ch;
else {
int l = 0;
do {
l = db.IndexOf (hs, l, db.Length - l);
if (l < db.Length - 3 && db [l + 1] == ch) {
sb.Append (db [l + 2]).Append (db [l + 3]);
break;
}
} while (true);
if (l < 0)
sb.Append (hs).Append (ch);
}
}
else {
c = db.IndexOf (ch);
sb.Append (c < 0 || c % 2 != 0 ? (char) ch : db [c + 1]);
}
}
return sb.ToString ();
}