【问题标题】:decimal.TryParse is happily accepting badly formatted number stringsdecimal.TryParse 乐于接受格式错误的数字字符串
【发布时间】:2017-01-29 16:55:58
【问题描述】:

有没有办法让 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 设置如何,都应该拒绝第三个字符串,而接受前两个。

【问题讨论】:

  • 您需要接受有效千位分隔符吗?如果没有,请不要使用 AllowThousands...
  • 看起来您需要单独的正则表达式验证。
  • 老实说,当您在 123''345'678 示例中分配正确的属性时,我很惊讶相邻的数千个分隔符被成功解析甚至。但在这种情况下,也许您只需将NumberGroupSizes 属性分配为{3, 3, 0}。我不知道。
  • 所以你的文化是de-CH,不是吗?然后我们可以通过添加System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("de-CH"); 来重现它
  • @PaulKienitz:想到正则表达式用于此目的,我不寒而栗

标签: c# parsing


【解决方案1】:

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

//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”格式字符串。

【讨论】:

  • 美丽。正是我想要的。很抱歉在这个问题上浪费了大家的时间,但我只是想,TryParse 已经存在了这么多年,它必须有一种方法来识别格式错误的字符串。我不可能是第一个寻找“常识”解析函数的人……!
  • 我也是这么想的。但是像"12'345.666""12'345.6" 这样的字符串呢?他们觉得自己很无辜,但是因为句点. 之后的小数位数不完全是两位,所以他们被您的解决方案拒绝了。这可以用.ToString("#,0.#############################") 或类似的丑陋的东西来解决。
  • 我已经在答案末尾的小数点后包含了不同长度的解决方案。 N0、N1、N8 等将是有效的格式字符串
  • NumberGroupSizes 背后的逻辑实际上非常复杂,因此很难提供一个解决方案,将每个组大小与当前文化允许的组大小进行比较。至少我已经放弃了,因为它变得很麻烦。
  • 啊,我没有仔细阅读你答案的最后一部分。
【解决方案2】:

把所有有用的建议放在一起,这就是我最终使用的。

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

在我展示我的代码之前,这里是我的 TryParseExact 函数将接受的内容与常规 decimal.TryParse 将接受的内容之间的区别:

这是我的代码。

我确信有一种更有效的方法可以做到这一点,使用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 是否抱怨高于此值,或者尝试将其存储为文本?不,它也很乐意将其视为有效数字,并将其粘贴为“1234567.89”。

不管怎样,工作完成了。感谢大家的帮助和建议。

【讨论】:

  • Regex 确实会将其恢复到大约 1 或 2 行 ;-),无论如何,加上一个用于发布您的解决方案
【解决方案3】:

这是因为解析只是跳过了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 },则数字的分组类似于“55,55555,55555,55555,4444,333.00”。如果数组包含 { 3, 4, 0 },则数字分组类似于“55555555555555555,4444,333.00”。

但你现在明白了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-27
    • 1970-01-01
    • 1970-01-01
    • 2014-04-24
    • 2016-09-06
    相关资源
    最近更新 更多