LoginSignup
13
13

More than 5 years have passed since last update.

System.Char.GetEastAsianWidthKind()

Last updated at Posted at 2014-04-22

UnicodeのEastAsianWidthが何か話題になっているらしい。
http://tech.albert2005.co.jp/blog/2014/04/21/mco-eaw/

というのをコレで知った。
https://twitter.com/ishisaka/status/458165828165578753

.NETに無い? じゃあ実装してみましょう。

EastAsianWidthの、特にAmbiguousの挙動がこわいこわいって言われているみたいだけど、文字の全角・半角の判別処理を実装するのは、ちっとも難しくない。やることは2つだけだ。

  • 全てのUnicode文字について、EastAsianWidthの値のリストを構築する。
  • 文字(char ch)と対象言語が東アジアかどうか(bool isEastAsian)から、文字種に基づいて全角か半角かをboolで返す

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を生やすだけだ。これだけならサルでも出来るよね?

13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13