4

I want to parse a string from an Text input to decimal. The value represents a currency value.

Currently i got this solution:

private Decimal CastToDecimal(string value) 
{
    Decimal result;
    var valid = Decimal.TryParse(value, NumberStyles.Currency, null, out result);
    return valid ? result : -1;
}

This works pretty well so far, except for possible culture-differences. I'm german and i expect most users to enter german-style puctuation. But it is possible that someone uses "." instead of "," and the conversion will fail.

"123,45€" => 123.45

"123.456,78€" => 123456.78

"123.45€" => 12345 <- I want the result to be 123.45 here

Is there a way to automatically detect the used culture for a decimal value? Such that it does not matter if you use german or english punctuation, you still get the same result?

Update:

Thanks to your help, i created a method which does what i want (i think).

private static Decimal CastToDecimal(string value)
{
    Decimal resultDe;
    Decimal resultEn;
    var style = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands;
    var cultureDe = CultureInfo.CreateSpecificCulture("de-DE");
    var cultureEn = CultureInfo.CreateSpecificCulture("en-GB");
    var deValid = Decimal.TryParse(value, style, cultureDe, out resultDe);
    var enValid = Decimal.TryParse(value, style, cultureEn, out resultEn);
    var minVal = Math.Min(resultDe, resultEn);
    var maxVal = Math.Max(resultDe, resultEn);
    if (!deValid)
        return resultEn;
    if (!enValid)
        return resultDe;
    return BitConverter.GetBytes(decimal.GetBits(minVal)[3])[2] > 2 ? maxVal : minVal;
}

This code...

    Console.WriteLine(CastToDecimal("123,45"));
    Console.WriteLine(CastToDecimal("123.45"));
    Console.WriteLine(CastToDecimal("123,450"));
    Console.WriteLine(CastToDecimal("123.450"));
    Console.WriteLine(CastToDecimal("123.123,45"));
    Console.WriteLine(CastToDecimal("123,123.45"));

returns this:

123,45
123,45
123450
123450
123123,45
123123,45
Alexander Mills
  • 277
  • 3
  • 18
  • 2
    There is none. You need to know which culture was used to format it. Or use `InvariantCulture` everywhere. Here's [my answer](http://stackoverflow.com/a/26862464/2530848) to very related question. – Sriram Sakthivel Dec 12 '14 at 12:16
  • But `123.456` means 123.456 with UK punctuation and it means 123456 with German! So there's no clear way to disambiguate. – chiastic-security Dec 12 '14 at 12:16
  • You can't automatically detect, if you had 123,456 then how do you now if it is 123456 with a thousand separator or 123.456 with , for the decimal. – Ben Robinson Dec 12 '14 at 12:17
  • 1
    Since the field is for currency only, 123.456 would be 123456, since there are no half cents. It's only a seperator if there are less than 3 digits on the right side of the seperator – Alexander Mills Dec 12 '14 at 12:18
  • 1
    @AlexanderMills That might be true for your wallet, but not for some applications which calculate the average price for a product for example. – Dirk Dec 12 '14 at 12:19
  • 1
    Yes but decimal.Parse does not understand the context it just sees a string. You should be able to get the user's culture and then pass in the appropriate `CultureInfo` so that the parse will work and fail correctly. – Ben Robinson Dec 12 '14 at 12:21
  • The number of digits between separators is also culture-dependent. "123.45" could be 12345. – Rawling Dec 12 '14 at 13:21
  • Just noticed update on your post what if user enters 123.000 where . is used for decimal separator. – Mubashar Dec 12 '14 at 13:31
  • Then it's assumed the user ment 123000... Since the user should input a "real-life"-currency value, he should not put 3 decimal places there and expect it to be 123 – Alexander Mills Dec 12 '14 at 13:41

4 Answers4

2

I encountered the same problem some time ago. My solution was writing my own parser in Java. The algorithm first cleans up the string. Brief description follows:

  1. Scan string from left to right
  2. If char = '.' then dotFound=true ; lastSeparatorPosition = index ; dots++
  3. If char = ',' then commaFound=true ; lastSeparatorPosition = index ; commas++
  4. If dots == 0 && commas == 0 then its an integer => done
  5. If dots > 0 && commas > 0 then the one at lastSeparatorPosition is the decimal separator. Remove the others from the string => done
  6. /* only one separator type */ if ( dots + commas ) > 1 then remove them // because must be thousands separator => done
  7. /* separator occurs once */ if numberOfDigits right of separator == 3 then you have to decide :-) either integer or decimal with 3 digits in fraction

7 is the only remaining problem like chiastic-security already stated. Here you can only decide taken the conceptual environment into account. All other cases are safe.

Have fun

Jemolah
  • 1,962
  • 3
  • 24
  • 41
2

the solution at http://msdn.microsoft.com/en-us/library/3s27fasw%28v=vs.110%29.aspx which includes setting the NumberStyle may be helpful.

...
value = "1.345,978";
style = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands;
culture = CultureInfo.CreateSpecificCulture("es-ES");
if (Double.TryParse(value, style, culture, out number))
   Console.WriteLine("Converted '{0}' to {1}.", value, number);
else
   Console.WriteLine("Unable to convert '{0}'.", value);
// Displays:  
//       Converted '1.345,978' to 1345.978. 

value = "1 345,978";
if (Double.TryParse(value, style, culture, out number))
   Console.WriteLine("Converted '{0}' to {1}.", value, number);
else
   Console.WriteLine("Unable to convert '{0}'.", value);
...
shelbypereira
  • 2,097
  • 3
  • 27
  • 50
1

This can't be done, simply because there are strings that are meaningful in two different cultures, but mean different things. For instance:

123.456

123,456

The first is a bit over 123 in the UK, but 123456 in Germany; the second is 123456 in the UK but a bit over 123 in France.

chiastic-security
  • 20,430
  • 4
  • 39
  • 67
0

The only solution is to add validation on input and give user and example by the way if it is a webpage then find a way to get input according to user's culture. I suggests you to not to try to do what you are trying because there are some culture which contradict each others for example in currency;

US/Australia/Many others uses following format

45,999.95

where , is thousand separator and . is decimal separator

whereas in some European countries

45.999,95  

means the same as above but thousands separator is . and , is used as decimal separator.

Now issue is there is no guarantee that user use both separator and your system may assume thousand separator as decimal and so on.

If you really don't want to bother user then make separate input fields for major and minor currencies.

So its better to not to go there. I believe this may help. Happy Coding :)

Update:

Same case is with date format e.g. in US format month comes first and then day whereas in Australia day comes first and then month now 02/01/2015 input will mean differently system can't tell the intention of user.

Mubashar
  • 12,300
  • 11
  • 66
  • 95