サロゲートペアと日本語文字揃え問題
C#(.NET)の関数では基本Unicodeで文字を扱うことになるわけですが、サロゲートペアにも対応しています。……なんて得意げに語りましたが、あまりよく知りませんでした。
サロゲートペア、すなわち、基本16ビットの文字コードなのですが16ビット×2を使って1文字を表すというもの。
JISの第3水準以上の文字などにもがっつりと対応しようと思うと、サロゲートペア問題がにょきっとでてきてじわりと問題になります。
でも、「サロゲートペア」なんていうなんだか怪しげなお話よりも、「C#と日本語文字揃え」単純な観点で問題があるんです。
文字幅、簡単に揃えられない問題
等幅ピッチフォントを扱い、タブ文字も当てにならないという前提のDOSコマンドプロンプト画面で、リストを表示したいと思います。例えば、以下のような感じ。通し番号、任意の文字列、数字、こんな表示。
■は"𠮟"という漢字
String[] list = { "test", "IsSurrogate", "本日test", "■るtest" }; Int32 count = 0; foreach (String str in list) { Console.WriteLine(String.Format("{0,4} {1,-20} {2:0000}", count++, str, str.Length)); }
フォーマットの「-20」は20文字の左揃え指定ですが、きれいに表示されると思いますか?
こうなります。
右側の数字がずれちゃう。
出力 0 test 0004 1 IsSurrogate 0011 2 本日test 0006 3 ■るtest 0007
英語圏のヒトはこれだから困ります。
「𠮟」という文字はフォントがない場合「??」と表示されます。
半角1文字、全角2文字という計算が出来れば良いのですが、結果漢字は1文字「本日」と「𠮟る」もString.Lengthの結果が異なるという結果になりました。
「𠮟」はChar×2のサロゲートペアの文字なんです。
サロゲート文字の判別
Char.IsSurrogateでサロゲートチェック。■は"𡋤"という漢字
static void check(String s) { Console.WriteLine(s + " String.Length = " + s.Length); foreach (Char c in s) { Console.WriteLine(String.Format("\t0x{0} IsSurrogate={1}", Convert.ToString(c, 16), Char.IsSurrogate(c).ToString())); } } check("A"); check("あ"); check("漢"); check("■"); // Unicode 0x212E4 '■'
出力は以下の通り。
出力 A String.Length = 1 0x41 IsSurrogate=False あ String.Length = 1 0x3042 IsSurrogate=False 漢 String.Length = 1 0x6f22 IsSurrogate=False ■ String.Length = 2 0xd844 IsSurrogate=True 0xdee4 IsSurrogate=True
Char.IsSurrogate関数でサロゲート文字を判断できるというわけです。
Char.IsHighSurrogate関数でHigh側、Char.IsLowSurrogate関数でLow側かどうかを確認できます。
文字を揃えるよ
というわけで、サロゲートペアの問題を踏まえて文字を揃えるプログラムです。後述も読んでね。public static Int32 GetStringWidth(String s) { Int32 count = 0; foreach (Char c in s) { if (Char.IsSurrogate(c) == true) { if (Char.IsHighSurrogate(c) == true) { continue; // ハイサロゲートは文字幅として無視 } } if (c <= 0xff) { count += 1; // 半角(エスケープ文字も含む) continue; } count += 2; // 全角 } return count; } Int32 count = 0; foreach (String str in list) { String fname = str + new String(' ', 20 - GetStringWidth(str)); Console.WriteLine(String.Format("{0,4} {1} {2:0000}", count++, fname, GetStringWidth(str))); }
GetStringWidthという文字幅を返す関数を作り、文字幅を調節するようにしました。
■は"𠮟"という漢字
出力 0 test 0004 1 IsSurrogate 0011 2 本日test 0008 3 ■るtest 0008
やった、できた。
オチとしては、以下のようにするとGetStringWidth関数はシンプルになります。
public static Int32 GetStringWidth(String s) { Byte[] bytes = Encoding.GetEncoding("Shift_JIS").GetBytes(s); return bytes.Length; }
そう、シフトJISにすれば、全角は2バイト、それ以外は1バイトなのでそのバイト列の長さが文字幅となるのです。
つまるところ、StringをCharとして扱う場合にはサロゲートペア文字もあるので注意しないといけないよ、というお話でした。