UnicodeのEastAsianWidthが何か話題になっているらしい。 http://tech.albert2005.co.jp/blog/2014/04/21/mco-eaw/
というのをコレで知った。 https://twitter.com/ishisaka/status/458165828165578753
.NETに無い? じゃあ実装してみましょう。
EastAsianWidthの、特にAmbiguousの挙動がこわいこわいって言われているみたいだけど、文字の全角・半角の判別処理を実装するのは、ちっとも難しくない。やることは2つだけだ。
EastAsianWidthの値がわかっていれば、全角・半角を判断するのは簡単だ:
public static bool IsFullWidth (int c, bool isEastAsian)
{
switch (GetKind (c)) {
case EastAsianWidthKind.Ambiguous:
return isEastAsian;
case EastAsianWidthKind.Full:
case EastAsianWidthKind.Wide:
return true;
default:
return false;
}
}
文脈によっては、isEastAsianはCultureInfoから判別してもいい(CultureInfoにそんな情報は無いので、外側から力技で判別するしかないと思うけど)。知る限りでは、.NETでEastAsianWidthがAPIレベルで獲得できるのはWindows Storeアプリのテキストレンダリングに使うフォントの情報くらいだ。 http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.documents.typography.eastasianwidth.aspx
UnicodeにおけるEastAsianWidthの値は、Unicode Character Database (UCD)の中に、これをリストアップしたファイルが存在するので、これをパースしてリストとして格納すればいいだけだ。UCDのデータ形式は単純なものだ。 http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
とりあえず、EastAsianWidthを判別するクラス、このテキストの内容をいちいち解析したくはないので、いったんEmbeddedResourceのかたちにして、EastAsianWidthを判別させてくれるEastAsianWidth
クラスと一緒のdllに格納できるようにしよう。そのためには、いったんこのファイルをパースして、生データとして書き出すことにする。
EastAsianWidth.txtの記法は、次のようなものだ。
000D;N # <control>
F0000..FFFFD;A # <Plane 15 Private Use, First>..<Plane 15 Private Use, Last>
内容は全て「文字または文字範囲;
EastAsianWidth種別 #
コメント」の形式になっている。範囲指定がある部分は、別途範囲指定のリストを作って、それ以外の1文字指定の部分は単純に配列に格納しよう。
というわけで、パーサおよびデータジェネレータのコードは、こんな感じだ:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
public class Driver
{
public static void Main (string [] args)
{
new Driver ().Run (args);
}
struct RangeMap<T>
{
public int Start;
public int End;
public T Value;
}
List<RangeMap<char>> ranges = new List<RangeMap<char>> ();
char [] eaw = new char [0x10FFFF];
// list of optimizible mappings.
RangeMap<char> [] optmap = {
new RangeMap<char> () {Start = 0x2F800, End = 0x2FA1D, Value = 'W'},
new RangeMap<char> () {Start = 0x4DC0, End = 0x4DFF, Value = 'N'},
new RangeMap<char> () {Start = 0xD7B0, End = 0xD7C6, Value = 'N'},
new RangeMap<char> () {Start = 0xD7CB, End = 0xD7FB, Value = 'N'},
new RangeMap<char> () {Start = 0xE0001, End = 0xE0001, Value = 'N'},
new RangeMap<char> () {Start = 0xE0020, End = 0xE007F, Value = 'N'},
new RangeMap<char> () {Start = 0xE0100, End = 0xE01EF, Value = 'A'},
};
public void Run (string [] args)
{
string text = null;
string output = "EastAsianWidth.dat";
string mapoutput = "EastAsianWidth.opt";
string source = args.Length == 0 && File.Exists ("EastAsianWidth.txt") ? "EastAsianWidth.txt" : args.FirstOrDefault ();
if (source != null)
text = File.ReadAllText (source);
else {
string univer = args.Length > 0 ? args [0] : "UCD/latest";
string url = string.Format ("http://www.unicode.org/Public/{0}/ucd/EastAsianWidth.txt", univer);
text = new WebClient ().DownloadString (url);
}
var lines = text.Split ('\n');
Func<string,string> first = s => s.Substring (0, s.IndexOf ('.'));
Func<string,string> last = s => s.Substring (s.LastIndexOf ('.') + 1);
Func<string,int> parse = s => int.Parse (s, NumberStyles.HexNumber);
foreach (var p in lines
.Select (x => x.Split ('#'))
.Select (arr => arr [0].Trim ())
.Where (x => x.Contains (';'))
.Select (x => x.Split (';'))) {
if (p [0].Contains ('.'))
ranges.Add (new RangeMap<char> () {Start = parse (first (p [0])), End = parse (last (p [0])), Value = p [1].Last ()});
else
eaw [parse (p [0])] = p [1].Last ();
}
// Apply mapping optimizer - for predefined optimizible list, fill '\0' (only if the mapping was correct)
foreach (var m in optmap) {
bool invalid = false;
for (int i = m.Start; i <= m.End; i++)
if (eaw [i] != m.Value) {
Console.Error.WriteLine ("Invalid optimization, at {0:X06}", i);
invalid = true;
}
if (!invalid)
for (int i = m.Start; i <= m.End; i++)
eaw [i] = '\0';
}
// calculate max index. After this, mapping is not generated.
int maxIndex = 0;
for (int i = eaw.Length - 1;;i--) {
if (eaw [i] != '\0') {
Console.Error.WriteLine ("max EAW index: {0:X06}", i);
maxIndex = i;
break;
}
}
using (var fs = File.CreateText (output)) {
for (int i = 0; i <= maxIndex; i++)
fs.Write (eaw [i]);
}
using (var fs = File.CreateText (mapoutput)) {
string allRanges = string.Join (";", ranges.Concat (optmap)
.OrderBy (m => m.Start)
.Select (m => string.Format ("{0:X06}-{1:X06}={2}", m.Start, m.End, m.Value))
);
fs.WriteLine (allRanges);
}
}
}
実のところ、もうひとつ最適化を施してあって、一部の文字範囲(optmap
フィールドの範囲)については、手動で範囲指定を追加してある。これは現状ではほぼ意味がないけど、後で配列の格納方法を最適化した時に有効になるだろう。
とりあえず、これでデータは生成できた。後は、EastAsianWidthを扱うクラスの中で、これをEmbeddedResourceから取り出して、使えるデータに戻すだけだ。
class Map
{
public int Start;
public int End;
public byte Value;
}
static readonly byte [] eaw;
static readonly Map [] map;
static EastAsianWidth ()
{
var stream = typeof (EastAsianWidth).Assembly.GetManifestResourceStream ("EastAsianWidth.dat");
eaw = new byte [stream.Length];
stream.Read (eaw, 0, eaw.Length);
stream = typeof (EastAsianWidth).Assembly.GetManifestResourceStream ("EastAsianWidth.opt");
Func<string,int> parse = s => int.Parse (s, NumberStyles.HexNumber);
string line = new StreamReader (stream).ReadToEnd ().Trim ();
map = line
.Split (';')
.Select (l => l.Split ('='))
.Select (arr => new {Range = arr [0].Split ('-'), Value = arr [1]})
.Select (m => new Map () {Start = parse (m.Range [0]), End = parse (m.Range [1]), Value = (byte) m.Value [0]})
.ToArray ();
}
いったんデータを復元したら、EastAsianWidth種別を取得するのはとても簡単だ。範囲指定のリストは考慮する必要がある。
static byte GetValue (int c)
{
foreach (var m in map)
if (m.Start <= c && c <= m.End)
return m.Value;
return eaw [c];
}
public static EastAsianWidthKind GetKind (char c)
{
return GetKind ((int) c);
}
public static EastAsianWidthKind GetKind (int c)
{
var ret = GetValue (c);
switch (ret) {
case (byte) 'F':
return EastAsianWidthKind.Full;
case (byte) 'H':
return EastAsianWidthKind.Half;
case (byte) 'W':
return EastAsianWidthKind.Wide;
case (byte) 'a':
return EastAsianWidthKind.Narrow;
case (byte) 'A':
return EastAsianWidthKind.Ambiguous;
case (byte) 'N':
return EastAsianWidthKind.Neutral;
}
throw new InvalidOperationException ();
}
この実装は以下のアーカイブにまとめておいた。
https://dl.dropboxusercontent.com/u/493047/tmp/nunicode.tar.gz
2016.03.23追記: ファイルが消えてしまっていたので、githubにrepoを作った: https://github.com/atsushieno/nunicode
そんなに真剣に最適化していないので、メモリ上に展開される配列はそれなりの大きさになっている。真面目に最適化しようと思ったら、monoでUnicodeサポートを実装するために作成した、空白部分をスキップするインデクサのクラスを活用できるだろう。 https://github.com/mono/mono/blob/master/mcs/class/corlib/Mono.Globalization.Unicode/CodePointIndexer.cs
ちなみに、monoでmscorlibの実装をしていた時は、GetManifestResourceStreamInternal()
という裏技を使って、メモリアドレスにダイレクトアクセスして、無駄なmanifest resource streamの生成を省くことができたけど、このコードはそうはいかないので、その部分は最適化出来ないと思って諦めた。
ともあれ、Unicode Character Databaseの値を素直に返すようなAPIを実装するのは、ちっとも難しくないので、みんな積極的にトライしてみても良いと思う。
ちなみにpostしてから書き忘れていたことに気付いたけど、タイトルにあるようなメソッドは作っていない。でもSystem.Charにextension methodを生やすだけだ。これだけならサルでも出来るよね?