C#・VB.NETで異体字を扱うライブラリItaijiを作っています。
対応環境は以下です。依存ライブラリはありません。
- .NET 5.0+
- .NET Framework 3.5+
リポジトリはこちらです。
まだ0.1.0ですが、nugetパッケージをpublishしてみました。
0.xのため、予告なく破壊的変更を加える場合があります。
また、使用は自己責任でお願いします。
制作動機
unicodeには 異体字 の仕組みがあります。異体字は、unicode上で字の形状を表す仕組みです。
主に人名など、字の形状を区別する必要がある場合に使用されます。
ある文字の後ろに異体字セレクタという専用のキャラクターを付与すると、字の形が変わります。
文字列ではあるので、char と string で問題なく扱えはするのですが、後述の問題のためにさまざまな面倒を引き起こします。
もっと簡単に扱うためにこのライブラリを作るに至りました。
サロゲートペア
面倒の1つはサロゲートペアです。
.NETは文字列を内部的にUTF-16、すなわち、1文字を基本16bitで扱います。
しかし、絵文字や一部の使用頻度の低い漢字などは、16bit(0xFFFF)を超えるコードポイントが割り当てられています。
unicodeは一部の領域をこれらの文字用に予約し、2つのコードポイントのペアでこれらの文字を表現することにしました。
つまり、1文字が1charで表されたり、2charで表されたりすることになります。
このため、stringを単純にcharの配列として扱うと、サロゲートの扱いに悩まされることになります。
var str = "夕飯は𩸽"; //ほっけ U+29E3D
foreach(var ch in str)
{
//サロゲートペアが含まれていると、char単位ではうまくいかない
Console.WriteLine(ch); // '夕', '飯', 'は', 0xD867, 0xDE3D
}
Console.WriteLine(str.Length); // 5!
そして、異体字セレクタ自体もサロゲートペアで表されます(漢字が主に使うものは)
見えている文字は1つでも、内部では ベース文字 2char + 異体字セレクタ 2charの4charで表されている可能性もあります。
救世主、System.Text.Rune……?
サロゲートペアの問題は、別に今に始まったことではありません。
サロゲートペアを気にせずコードポイントを扱うための手段として、.NET Core 3.1から System.Text.Rune という構造体が追加されています。
var str = "夕飯は𩸽"; //ほっけ U+29E3D
foreach(var rune in str.EnumerateRunes())
{
//Runeであれば、サロゲートペアを正しく扱える
Console.WriteLine(rune); // '夕', '飯', 'は', "𩸽"
}
Console.WriteLine(str.EnumerateRunes().Count()); // 4
ただ、.NET Framework向けには、バックポートパッケージすらも提供されていません。
.NET的には、いい加減.NET Frameworkから脱却してほしいということかもしれませんが・・・
また、Runeを使っても、異体字セレクタは1つのRuneとして扱われるため、
異体字を扱うには、前のRuneをキャッシュしておくなど、少し手間が要ります。
Itaijiの解決策
そこでItaijiは、KanjiChar という構造体を作って、「ベースのRune」と「異体字セレクタのRune」をセットで管理するようにしました。
using Itaiji;
var str = "辻󠄀さんの夕飯は𩸽"; //辻は異体字 𩸽 ほっけ U+29E3D
foreach(var kanji in str.EnumerateKanji())
{
//異体字セレクタを正しく扱える
Console.WriteLine(kanji); // "辻󠄀", 'さ', 'ん', 'の', '夕', '飯', 'は', "𩸽"
}
Console.WriteLine(str.EnumerateKanji().Count()); // 8
また、Rune をバックポートすることで、.NET Framework対応を実現しています。
2系統の異体字
異体字にはじつは、2系統が存在します。Adobe-Japan1とMoji_Joho(Hanyo-Denshi)です。
Adobe-Japan1はその名の通り、Adobeが定めた文字集合です。対してMoji_Johoは戸籍や住基といったデータで使用される、いわば国が定めた文字集合です(実際の管理団体は民間団体であるようです)。
これら2系統の異体字は、別々にIVD(Ideographic Variation Database)に登録されています。
何が問題かというと、「同じ(にしか見えない)文字であっても、Adobe-JapanとMoji_Johoの2つが定めている場合がある」ということです。次の画像を見てください。

https://moji.or.jp/mojikiban/aboutivs/
点が1つの「辻」はE0100とE0102の2つで定められています。つまり、unicodeには点が1つの「辻」を表す方法が2種類ある ということです。
そして、世の中のIME等の入力補助機能は、多くはAdobe-Japan1の異体字のみの対応である場合がほとんどです。
長くなりましたが、Moji_Johoの異体字を扱うことを想定したシステムでも、Adobe-Japan1の異体字が入力としてやってくる ということを現実的に想定しなければいけない場合が存在します。
Itaijiでは
Itaijiでは各コレクションのリストを持ち、字がどのコレクションに属するかを判定できるようにしました。
using Itaiji;
var str = "辻󠄀" // adobe-japanのツジ
if(str.HasInvalidIvsAsMojiJoho()){
throw new Exception("扱えない異体字が含まれています。");
}
なお、Moji_Joho<->AdobeJapan1への自動変換はできません。
Adobe-Japan1と、Moji_Joho(Hanyo-Denshi)の間のコードポイントの対応は、残念ながら定められていません……
その他のこだわり
- 一応、パフォーマンスを気にしています。見様見真似でアロケーションを抑えたり、コレクションのデータを圧縮したりなど
- .NET Framework向けには
Spanの対応はあえてせず、依存なしにこだわりました
おわりに
まだ0.1.0のライブラリであり、Issue・プルリク・マサカリなどなど大歓迎です。
まだ自由研究の域を出ないライブラリではありますが、役立つ場面がどこかにきっとある……あるのではないでしょうか。

