C# Decimal.Parse issue with commas
Asked Answered
L

4

23

Here's my problem (for en-US):

Decimal.Parse("1,2,3,4") returns 1234, instead of throwing an InvalidFormatException.

Most Windows applications (Excel en-US) do not drop the thousand separators and do not consider that value a decimal number. The same issue happens for other languages (although with different characters).

Are there any other decimal parsing libraries out there that solve this issue?

Thanks!

Lapith answered 6/5, 2009 at 21:1 Comment(2)
Are you trying to disallow any thousands separator, or just the extraneous ones?Drogin
I should have been more specific (I'm new here, sorry!). I disallow extraneous thousand separators in the string. "1,234.00" should be valid, while "12,34.00" should be incorrect.Lapith
L
10

I ended up having to write the code to verify the currency manually. Personally, for a framework that prides itself for having all the globalization stuff built in, it's amazing .NET doesn't have anything to handle this.

My solution is below. It works for all the locales in the framework. It doesn't support Negative numbers, as Orion pointed out below, though. What do you guys think?

    public static bool TryParseCurrency(string value, out decimal result)
    {
        result = 0;
        const int maxCount = 100;
        if (String.IsNullOrEmpty(value))
            return false;

        const string decimalNumberPattern = @"^\-?[0-9]{{1,{4}}}(\{0}[0-9]{{{2}}})*(\{0}[0-9]{{{3}}})*(\{1}[0-9]+)*$";

        NumberFormatInfo format = CultureInfo.CurrentCulture.NumberFormat;

        int secondaryGroupSize = format.CurrencyGroupSizes.Length > 1
                ? format.CurrencyGroupSizes[1]
                : format.CurrencyGroupSizes[0];

        var r = new Regex(String.Format(decimalNumberPattern
                                       , format.CurrencyGroupSeparator==" " ? "s" : format.CurrencyGroupSeparator
                                       , format.CurrencyDecimalSeparator
                                       , secondaryGroupSize
                                       , format.CurrencyGroupSizes[0]
                                       , maxCount), RegexOptions.Compiled | RegexOptions.CultureInvariant);
        return !r.IsMatch(value.Trim()) ? false : Decimal.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out result);
    }

And here's one test to show it working (nUnit):

    [Test]
    public void TestCurrencyStrictParsingInAllLocales()
    {
        var originalCulture = CultureInfo.CurrentCulture;
        var cultures = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
        const decimal originalNumber = 12345678.98m;
        foreach(var culture in cultures)
        {
            var stringValue = originalNumber.ToCurrencyWithoutSymbolFormat();
            decimal resultNumber = 0;
            Assert.IsTrue(DecimalUtils.TryParseCurrency(stringValue, out resultNumber));
            Assert.AreEqual(originalNumber, resultNumber);
        }
        System.Threading.Thread.CurrentThread.CurrentCulture = originalCulture;

    }
Lapith answered 8/5, 2009 at 2:38 Comment(4)
There's a couple of issues in terms of overall usage. A lot of cultures don't use - to represent the negative sign so including it in your regex makes it not culture-neutral. Also some numbering systems use the negative sign after the number instead of in front (e.g. '1234-' for '-1234' in en-US). Often you also have to allow for spaces before or after the '-' for some cultures. Also some cultures use parentheses for negative numbers which means paired usage. Either way, if you're only using this for en-US then you should be fine.Drogin
Also .Net does support currency parsing. You just have to pass in the currency rules for the NumberStyles option (i.e. NumberStyles.Currency). It just also have a more liberal interpretation of grouping separators.Drogin
Hi Oron, thanks for the follow up. You're right about the negative sign, this would simply not work. This is going to be used by users across the globe, but the application doesn't support the input of negative values, so it works for me.Lapith
@bolu yes, performance of the code as is is not great, but you can always cache the regex objects, which is what I did in my solution (years ago).Lapith
U
14

It's allowing thousands, because the default NumberStyles value used by Decimal.Parse (NumberStyles.Number) includes NumberStyles.AllowThousands.

If you want to disallow the thousands separators, you can just remove that flag, like this:

Decimal.Parse("1,2,3,4", NumberStyles.Number ^ NumberStyles.AllowThousands)

(the above code will throw an InvalidFormatException, which is what you want, right?)

Unhinge answered 6/5, 2009 at 21:8 Comment(6)
I don't think that's what he wants. I think he wants it to consider "1,200" valid, but not "1,2,0,0"... could be wrong though.Intercontinental
You're right, Max. I still need the thousand separator, when it separates thousands only.Lapith
I would still consider the parser to be flawed. The separator should only be allowed to appear every three digits and only to the left of the decimal pointGoldfinch
I don't see the parser skipping decimal separators as a bug, as those characters have no meaning in math. But it would be nice to have a more strict version of that method that would catch this..Lapith
It's taking into account globalization. 123,40 is actually 123.40 in Danish. Perhaps you should move to regex to try and match the string.Kantos
Not really, Joshua. When in en-US locale, Decimal.Parse converts "123,40" to 12340.00, instead of 123.40. The behavior is consistent with other locales..Lapith
L
10

I ended up having to write the code to verify the currency manually. Personally, for a framework that prides itself for having all the globalization stuff built in, it's amazing .NET doesn't have anything to handle this.

My solution is below. It works for all the locales in the framework. It doesn't support Negative numbers, as Orion pointed out below, though. What do you guys think?

    public static bool TryParseCurrency(string value, out decimal result)
    {
        result = 0;
        const int maxCount = 100;
        if (String.IsNullOrEmpty(value))
            return false;

        const string decimalNumberPattern = @"^\-?[0-9]{{1,{4}}}(\{0}[0-9]{{{2}}})*(\{0}[0-9]{{{3}}})*(\{1}[0-9]+)*$";

        NumberFormatInfo format = CultureInfo.CurrentCulture.NumberFormat;

        int secondaryGroupSize = format.CurrencyGroupSizes.Length > 1
                ? format.CurrencyGroupSizes[1]
                : format.CurrencyGroupSizes[0];

        var r = new Regex(String.Format(decimalNumberPattern
                                       , format.CurrencyGroupSeparator==" " ? "s" : format.CurrencyGroupSeparator
                                       , format.CurrencyDecimalSeparator
                                       , secondaryGroupSize
                                       , format.CurrencyGroupSizes[0]
                                       , maxCount), RegexOptions.Compiled | RegexOptions.CultureInvariant);
        return !r.IsMatch(value.Trim()) ? false : Decimal.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out result);
    }

And here's one test to show it working (nUnit):

    [Test]
    public void TestCurrencyStrictParsingInAllLocales()
    {
        var originalCulture = CultureInfo.CurrentCulture;
        var cultures = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
        const decimal originalNumber = 12345678.98m;
        foreach(var culture in cultures)
        {
            var stringValue = originalNumber.ToCurrencyWithoutSymbolFormat();
            decimal resultNumber = 0;
            Assert.IsTrue(DecimalUtils.TryParseCurrency(stringValue, out resultNumber));
            Assert.AreEqual(originalNumber, resultNumber);
        }
        System.Threading.Thread.CurrentThread.CurrentCulture = originalCulture;

    }
Lapith answered 8/5, 2009 at 2:38 Comment(4)
There's a couple of issues in terms of overall usage. A lot of cultures don't use - to represent the negative sign so including it in your regex makes it not culture-neutral. Also some numbering systems use the negative sign after the number instead of in front (e.g. '1234-' for '-1234' in en-US). Often you also have to allow for spaces before or after the '-' for some cultures. Also some cultures use parentheses for negative numbers which means paired usage. Either way, if you're only using this for en-US then you should be fine.Drogin
Also .Net does support currency parsing. You just have to pass in the currency rules for the NumberStyles option (i.e. NumberStyles.Currency). It just also have a more liberal interpretation of grouping separators.Drogin
Hi Oron, thanks for the follow up. You're right about the negative sign, this would simply not work. This is going to be used by users across the globe, but the application doesn't support the input of negative values, so it works for me.Lapith
@bolu yes, performance of the code as is is not great, but you can always cache the regex objects, which is what I did in my solution (years ago).Lapith
D
1

You might be able to do this in a two-phase process. First you could verify the thousands separator using the information in the CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator and CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes throwing an exception if it doesn't pass and then pass the number into the Decimal.Parse();

Drogin answered 6/5, 2009 at 21:20 Comment(2)
Thanks, Orion, I'll give this idea a try.Lapith
Hi Orion, I ended up using the items you mentioned, plus a few more. Take a look at the solution on my answer below, what do you think?Lapith
F
0

It is a common issue never solved by microsoft. So, I don't understand why 1,2,3.00 (english culture for example) is valid! You need to build an algorith to examine group size and return false/exception(like a failed double.parse) if the test is not passed. I had a similar problem in a mvc application, which build in validator doesn't accept thousands..so i've overwrite it with a custom, using double/decimal/float.parse, but adding a logic to validate group size.

If you want read my solution (it is used for my mvc custom validator, but you can use it to have a better double/decimal/float.parse generic validator) go here https://mcmap.net/q/541271/-decimal-values-with-thousand-separator-in-asp-net-mvc

Finkelstein answered 29/1, 2017 at 9:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.