Skip to content

Parse("1.25", en-DE) returns 125 and Parse("1.5", fr-CH) throws, where decimal returns 1.25 and 1.5: under AllowCurrencySymbol (incl. default NumberStyles.Any) currency separators are only used after a symbol is read #195

Closed

Description

@matt-edmondson

What's wrong

PreciseNumber.Parse uses CurrencyDecimalSeparator / CurrencyGroupSeparator only once a currency symbol has actually been stripped. Otherwise it uses the number separators alone.

The BCL behaves differently whenever the style includes AllowCurrencySymbol:

  • it tries the currency separators first;
  • it also accepts the number separators as long as no currency symbol has been read ahead of the digits.

The two disagree for any culture whose currency separators differ from its number separators, such as fr-CH, de-AT, en-DE, en-BE, en-NL, en-SI and en-FI. Depending on the input, PreciseNumber either throws FormatException where decimal parses, or silently returns a value 10× or 100× too large.

The default overloads are affected. Parse(string, IFormatProvider) and TryParse(string, IFormatProvider, out) use NumberStyles.Any, which includes AllowCurrencySymbol.

Repro (HEAD f690afc, net10.0)

var fr = CultureInfo.GetCultureInfo("fr-CH");   // number "," / " "   currency "." / " "
var en = CultureInfo.GetCultureInfo("en-DE");   // number "," / "."   currency "." / ","

PreciseNumber.Parse("1.5", fr);           // FormatException   decimal.Parse("1.5", NumberStyles.Any, fr) = 1.5
PreciseNumber.TryParse("1.5", fr, out _); // false
PreciseNumber.Parse("1.25", en);          // 125 (!)           decimal.Parse("1.25", NumberStyles.Any, en) = 1.25

// ICU-independent formats
var mixed = new NumberFormatInfo { NumberDecimalSeparator = ",", NumberGroupSeparator = ".",
                                   CurrencyDecimalSeparator = ".", CurrencyGroupSeparator = ",", CurrencySymbol = "€" };
var swiss = new NumberFormatInfo { NumberDecimalSeparator = ",", NumberGroupSeparator = " ",
                                   CurrencyDecimalSeparator = ".", CurrencyGroupSeparator = " ", CurrencySymbol = "CHF" };
text, style format decimal / double PreciseNumber
"1.5", Any or Currency mixed 1.5 15
"1.25", Any or Currency mixed 1.25 125
"1,5 €", Any or Currency mixed 1.5 15
"1.5", Any or Currency swiss 1.5 FormatException
"1 234.5", Any or Currency swiss 1234.5 FormatException
"1 234,50 CHF", Any or Currency swiss 1234.50 FormatException
"1.234,5", Any de-AT 1234.5 FormatException

Why it matters

  • The default Parse(s, provider) overload can silently return a value off by a factor of 10 or 100.
  • Valid Swiss and Austrian amounts are rejected.
  • PreciseNumberParseAffixTests states that the expectation is the BCL's. Its hand-built Dollars and Euros formats use the same separators for numbers and currency, so this path is never tested.

Root cause

  • PreciseNumber/PreciseNumber.cs ~L1239-1241 picks a single separator pair: hasCurrencySymbol ? CurrencyDecimalSeparator : NumberDecimalSeparator, and the same for the group separator.
  • hasCurrencySymbol comes from StripAffixes, so it is true only when a symbol is present.
  • The digit loop compares against that one pair only.
  • The BCL's Number.Parsing.TryParseNumber does this instead when AllowCurrencySymbol is set:
    • it sets the separators to the currency ones;
    • it also accepts the number separators while parsingCurrency && (state & StateCurrency) == 0;
    • it checks the decimal separator before the group separator.
  • A trailing symbol is stripped before the digits are read, so "1 234,50 CHF" is read with the currency separators only. The BCL reads those digits before it reaches the symbol.

Suggested fix / acceptance criteria

  • When style has AllowCurrencySymbol, try the currency separator first at each position, then the number separator.
  • Stop accepting the number separators only when a symbol was read ahead of the digits.
  • Keep the existing rule that a group separator may only follow integral digits.
  • Add rows to TestParseAcceptsWhatDecimalAccepts using formats whose currency and number separators differ (mixed / swiss above). Cover "1.5", "1.25", "1 234.5", "1,5 €" and "1 234,50 CHF" under Any and Currency.
  • Parse("1.25", en-DE) equals decimal.Parse("1.25", NumberStyles.Any, en-DE).

Near-duplicates checked:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions