decimal.TryParse很高兴接受格式错误的数字字符串

时间:2015-10-13 12:43:35

标签: c# parsing

有没有办法让C#TryParse()函数更严格?

现在,如果传入包含数字的字符串,则正确的小数和&千位分隔符,它通常似乎只是接受它们,即使格式没有意义,例如:123''345'678

如果数字格式不正确,我正在寻找一种让TryParse 不成功的方法。

所以,我的总部设在苏黎世,如果我这样做的话:

decimal exampleNumber = 1234567.89m;
Trace.WriteLine(string.Format("Value {0} gets formatted as: \"{1:N}\"", exampleNumber, exampleNumber));

...然后,根据我的区域设置,我得到了这个......

Value 1234567.89 gets formatted as: "1'234'567.89"

所以你可以看到,对于我的区域,小数位字符是一个句号,千位分隔符是一个撇号。

现在,让我们创建一个简单的函数来测试string是否可以解析为decimal

private void ParseTest(string str)
{
    decimal val = 0;
    if (decimal.TryParse(str, out val))
        Trace.WriteLine(string.Format("Parsed \"{0}\" as {1}", str, val));
    else
        Trace.WriteLine(string.Format("Couldn't parse: \"{0}\"", str));
}

好的,让我们用几个字符串调用这个函数。

以下哪些字符串您认为会被此函数成功解析?

以下是我得到的结果:

ParseTest("123345.67");         //  1. Parsed "123345.67" as 123345.67
ParseTest("123'345.67");        //  2. Parsed "123'345.67" as 123345.67
ParseTest("123'345'6.78");      //  3. Parsed "123'345'6.78" as 1233456.78
ParseTest("1''23'345'678");     //  4. Parsed "1''23'345'678" as 123345678
ParseTest("'1''23'345'678");    //  5. Couldn't parse: "'1''23'345'678"
ParseTest("123''345'678");      //  6. Parsed "123''345'678" as 123345678
ParseTest("123'4'5'6.7.89");    //  7. Couldn't parse: "123'4'5'6.7.89"
ParseTest("'12'3'45'678");      //  8. Couldn't parse: "'12'3'45'678"

我认为你可以看到我的观点。

对我来说,只有前两个字符串才能成功解析。其他人应该都失败了,因为他们在千分之后没有3位数,或者有两个撇号。

即使我将ParseTest更改为更具体一些,结果也完全相同。 (例如,它乐意接受“123''345'678”作为有效小数。)

private void ParseTest(string str)
{
    decimal val = 0;
    var styles = (NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands);

    if (decimal.TryParse(str, styles, CultureInfo.CurrentCulture, out val))
        Trace.WriteLine(string.Format("Parsed \"{0}\" as {1}", str, val));
    else
        Trace.WriteLine(string.Format("Couldn't parse: \"{0}\"", str));
}

那么,是否有一种直接的方法不允许格式错误的字符串被TryParse接受?

更新

感谢所有建议。

也许我应该澄清:我正在寻找的是前两个字符串有效,但第三个字符串被拒绝。

ParseTest("123345.67");
ParseTest("123'456.67");
ParseTest("12'345'6.7");

当然必须有一种方法可以使用“NumberStyles.AllowThousands”,因此它可以选择允许千位分隔符,但要确保数字格式 有意义吗?

现在,如果我使用它:

if (decimal.TryParse(str, styles, CultureInfo.CurrentCulture, out val))

我得到了这些结果:

Parsed "123345.67" as 123345.67
Parsed "123'456.67" as 123456.67
Parsed "12'345'6.7" as 123456.7

如果我使用它:

if (decimal.TryParse(str, styles, CultureInfo.InvariantCulture, out val))

我得到了这些结果:

Parsed "123345.67" as 123345.67
Couldn't parse: "123'456.67"
Couldn't parse: "12'345'6.7"

这是我的问题......无论CultureInfo设置如何,都应拒绝第三个字符串,并接受前两个字符串。

3 个答案:

答案 0 :(得分:2)

根据当前文化判断它是否格式正确的最简单方法是将格式化后的结果数与原始字符串进行比较。

//input = "123,456.56" -- true
//input = "123,4,56.56" -- false
//input = "123456.56" -- true
//input = "123,,456.56" -- false
string input = "123456.56";
decimal value;

if(!decimal.TryParse(input, out value))
{
    return false;
}

return (value.ToString("N") == input || value.ToString() == input);

对于完全省略千位分隔符和输入的输入,这将成功指定正确的千位分隔符。

如果您需要它来接受一系列小数位,那么您需要获取小数分隔符后面的字符数并将其附加到" N"格式字符串。

答案 1 :(得分:2)

在这里汇总所有有用的建议,这是我最终使用的内容。

它并不完美,但是,对于我的公司应用,它至少会拒绝那些“看起来不正确”的数字字符串。

在我提交代码之前,我的TryParseExact函数接受的内容与常规decimal.TryParse接受的内容之间存在差异:

enter image description here

这是我的代码。

我确信使用regex或其他方式可以更有效地完成某些操作,但这足以满足我的需求,我希望它可以帮助其他开发人员:

    public static bool TryParseExact(string str, out decimal result)
    {
        //  The regular decimal.TryParse() is a bit rubbish.  It'll happily accept strings which don't make sense, such as:
        //      123'345'6.78
        //      1''23'345'678
        //      123''345'678
        //
        //  This function does the same as TryParse(), but checks whether the number "makes sense", ie:
        //      - has exactly zero or one "decimal point" characters
        //      - if the string has thousand-separators, then are there exactly three digits inbetween them 
        // 
        //  Assumptions: if we're using thousand-separators, then there'll be just one "NumberGroupSizes" value.
        //
        //  Returns True if this is a valid number
        //          False if this isn't a valid number
        // 
        result = 0;

        if (str == null || string.IsNullOrWhiteSpace(str)) 
            return false;

        //  First, let's see if TryParse itself falls over, trying to parse the string.
        decimal val = 0;
        if (!decimal.TryParse(str, out val))
        {
            //  If the numeric string contains any letters, foreign characters, etc, the function will abort here.
            return false;
        }

        //  Note: we'll ONLY return TryParse's result *if* the rest of the validation succeeds.

        CultureInfo culture = CultureInfo.CurrentCulture;
        int[] expectedDigitLengths = culture.NumberFormat.NumberGroupSizes;         //  Usually a 1-element array:  { 3 }
        string decimalPoint = culture.NumberFormat.NumberDecimalSeparator;          //  Usually full-stop, but perhaps a comma in France.
        string thousands = culture.NumberFormat.NumberGroupSeparator;               //  Usually a comma, but can be apostrophe in European locations.

        int numberOfDecimalPoints = CountOccurrences(str, decimalPoint);
        if (numberOfDecimalPoints != 0 && numberOfDecimalPoints != 1)
        {
            //  You're only allowed either ONE or ZERO decimal point characters.  No more!
            return false;
        }

        int numberOfThousandDelimiters = CountOccurrences(str, thousands);
        if (numberOfThousandDelimiters == 0)
        {
            result = val;
            return true;
        }

        //  Okay, so this numeric-string DOES contain 1 or more thousand-seperator characters.
        //  Let's do some checks on the integer part of this numeric string  (eg "12,345,67.890" -> "12,345,67")
        if (numberOfDecimalPoints == 1)
        {
            int inx = str.IndexOf(decimalPoint);
            str = str.Substring(0, inx);
        }

        //  Split up our number-string into sections: "12,345,67" -> [ "12", "345", "67" ]
        string[] parts = str.Split(new string[] { thousands }, StringSplitOptions.None);

        if (parts.Length < 2)
        {
            //  If we're using thousand-separators, then we must have at least two parts (eg "1,234" contains two parts: "1" and "234")
            return false;
        }

        //  Note: the first section is allowed to be upto 3-chars long  (eg for "12,345,678", the "12" is perfectly valid)
        if (parts[0].Length == 0 || parts[0].Length > expectedDigitLengths[0])
        {
            //  This should catch errors like:
            //      ",234"
            //      "1234,567"
            //      "12345678,901"
            return false;
        }

        //  ... all subsequent sections MUST be 3-characters in length
        foreach (string oneSection in parts.Skip(1))
        {
            if (oneSection.Length != expectedDigitLengths[0])
                return false;
        }

        result = val;
        return true;
    }

    public static int CountOccurrences(string str, string chr)
    {
        //  How many times does a particular string appear in a string ?
        //
        int count = str.Length - str.Replace(chr, "").Length;
        return count;
    }

顺便说一下,我在Excel中创建了上面的表格图片,并注意到将这样的值粘贴到Excel中实际上很难:

1'234567.89

Excel是否会抱怨此值,或尝试将其存储为文本?不,它也乐意接受这个作为有效数字,并将其粘贴为&#34; 1234567.89&#34;。

无论如何,完成工作..感谢大家的帮助和帮助;建议。

答案 2 :(得分:1)

这是因为解析只是跳过NumberFormatInfo.NumberGroupSeparator字符串并完全忽略NumberFormatInfo.NumberGroupSizes属性。但是,您可以实现这样的验证:

static bool ValidateNumberGroups(string value, CultureInfo culture)
{
    string[] parts = value.Split(new string[] { culture.NumberFormat.NumberGroupSeparator }, StringSplitOptions.None);
    foreach (string part in parts)
    {
        int length = part.Length;
        if (culture.NumberFormat.NumberGroupSizes.Contains(length) == false)
        {
            return false;
        }
    }

    return true;
}

它仍然不完美,如MSDN says

  

数组的第一个元素定义NumberDecimalSeparator左侧最低有效数字组中的元素数。每个后续元素引用前一组左侧的下一个重要数字组。如果数组的最后一个元素不是0,则根据数组的最后一个元素对其余数字进行分组。如果最后一个元素为0,则不对其余数字进行分组。

     

例如,如果数组包含{3,4,5},则数字的分组类似于&#34; 55,55555,55555,55555,4444,333.00&#34;。如果数组包含{3,4,0},则数字的分组类似于&#34; 55555555555555555,4444,333.00&#34;。

但你现在可以看到这一点。