2014年3月27日

char.IsDigit() / 正規表現"\d" の注意事項

char.IsDigit()正規表現の"\d" は、半角数字にマッチする。
が、それだけでなく、UNICODEカテゴリ"Nd"(Number, Decimal Digit) にもマッチする。

上記を意識して使わないと、高確率でバグを仕込んでしまう。
例えば、日本語システムだと全角数字("0" ~ "9")の入力が想定できるため、半角数字と区別する必要がある場合に、これらを使用するとバグる。
他に、これらを使ってチェックし、int.Parse() に投げるなども NG。
※普通は、int.TryParse() 推奨

UNICODEカテゴリ"Nd"にマッチすることの検証

[検証コード]

using System;
using System.Globalization;
using System.Text.RegularExpressions;

static class Program
{
    static void Main()
    {
        foreach( var point in DigitZeroPoints ){
            for( var c = point; c < point + 10; c++ ){
                var char_isdigit = char.IsDigit(c);
                var regex_isdigit = Regex.IsMatch(c.ToString(), @"\d");
                if( !char_isdigit || !regex_isdigit ){
                    // どちらかのマッチ失敗時のみコンソール出力
                    Console.WriteLine(
                        "{0:X4} - char={1} regex={2} category={3}",
                        (int)c, char_isdigit, regex_isdigit,
                        CharUnicodeInfo.GetUnicodeCategory(c));
                }
            }
        }
    }

    // UNICODEカテゴリ"Nd"の ZERO のコードポイント一覧
    // ※FileFormat.Info から 2014/03/26 に取得
    // ※4Byte 文字は char で表現できないため、除外
    private static readonly char[] DigitZeroPoints = new[] {
        '\u0030',  // DIGIT ZERO
        '\u0660',  // ARABIC-INDIC DIGIT ZERO
        '\u06F0',  // EXTENDED ARABIC-INDIC DIGIT ZERO
        '\u07C0',  // NKO DIGIT ZERO
        '\u0966',  // DEVANAGARI DIGIT ZERO
        '\u09E6',  // BENGALI DIGIT ZERO
        '\u0A66',  // GURMUKHI DIGIT ZERO
        '\u0AE6',  // GUJARATI DIGIT ZERO
        '\u0B66',  // ORIYA DIGIT ZERO
        '\u0BE6',  // TAMIL DIGIT ZERO
        '\u0C66',  // TELUGU DIGIT ZERO
        '\u0CE6',  // KANNADA DIGIT ZERO
        '\u0D66',  // MALAYALAM DIGIT ZERO
        '\u0E50',  // THAI DIGIT ZERO
        '\u0ED0',  // LAO DIGIT ZERO
        '\u0F20',  // TIBETAN DIGIT ZERO
        '\u1040',  // MYANMAR DIGIT ZERO
        '\u1090',  // MYANMAR SHAN DIGIT ZERO
        '\u17E0',  // KHMER DIGIT ZERO
        '\u1810',  // MONGOLIAN DIGIT ZERO
        '\u1946',  // LIMBU DIGIT ZERO
        '\u19D0',  // NEW TAI LUE DIGIT ZERO
        '\u1A80',  // TAI THAM HORA DIGIT ZERO
        '\u1A90',  // TAI THAM THAM DIGIT ZERO
        '\u1B50',  // BALINESE DIGIT ZERO
        '\u1BB0',  // SUNDANESE DIGIT ZERO
        '\u1C40',  // LEPCHA DIGIT ZERO
        '\u1C50',  // OL CHIKI DIGIT ZERO
        '\uA620',  // VAI DIGIT ZERO
        '\uA8D0',  // SAURASHTRA DIGIT ZERO
        '\uA900',  // KAYAH LI DIGIT ZERO
        '\uA9D0',  // JAVANESE DIGIT ZERO
        '\uAA50',  // CHAM DIGIT ZERO
        '\uABF0',  // MEETEI MAYEK DIGIT ZERO
        '\uFF10',  // FULLWIDTH DIGIT ZERO
    };
}

[結果]
1A80 - char=False regex=False category=OtherNotAssigned
:
1A89 - char=False regex=False category=OtherNotAssigned
1A90 - char=False regex=False category=OtherNotAssigned
:
1A99 - char=False regex=False category=OtherNotAssigned
A9D0 - char=False regex=False category=OtherNotAssigned
:
A9D9 - char=False regex=False category=OtherNotAssigned
ABF0 - char=False regex=False category=OtherNotAssigned
:
ABF9 - char=False regex=False category=OtherNotAssigned

TAI THAM HORA DIGIT (1A80~9)、TAI THAM THAM DIGIT (1A90~9)、JAVANESE DIGIT (A9D0~9)、MEETEI MAYEK DIGIT (ABF0~9) が数字文字として判定されず、UNICODEカテゴリが"Cn"(Other, Not Assigned) となった。

原因は、判定されなかった文字の UNICODE バージョンは 5.2.0 で、Windows7/.NET 4.0 の UNICODE バージョンは 5.1 だからである。(参考:.NET の UNICODE バージョン
環境がないので検証できないが、Windows 8/.NET 4.5 なら数字文字として判定されるはず。

おまけ

半角数字のみを判定したい場合は、下記を使っている。

public static bool IsAsciiDigit(this char c)
{
    return '0' <= c && c <= '9';
}

正規表現の場合

// \d を使わず素直に書く
var regex_isdigit = Regex.IsMatch(c.ToString(), "[0-9]");

// または

// ECMAScript 準拠の正規表現ならば、半角数字のみにマッチする
var regex_isdigit = Regex.IsMatch(c.ToString(), @"\d", RegexOptions.ECMAScript);

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

0 件のコメント:

コメントを投稿