Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
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.
PreciseNumberParseAffixTestsstates that the expectation is the BCL's. Its hand-builtDollarsandEurosformats 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.hasCurrencySymbolcomes fromStripAffixes, so it is true only when a symbol is present.- The digit loop compares against that one pair only.
- The BCL's
Number.Parsing.TryParseNumberdoes this instead whenAllowCurrencySymbolis 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
stylehasAllowCurrencySymbol, 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
TestParseAcceptsWhatDecimalAcceptsusing formats whose currency and number separators differ (mixed/swissabove). Cover"1.5","1.25","1 234.5","1,5 €"and"1 234,50 CHF"underAnyandCurrency. Parse("1.25", en-DE)equalsdecimal.Parse("1.25", NumberStyles.Any, en-DE).
Near-duplicates checked:
- Parse("(1.5)"), Parse("1.5-") and Parse("$1,234.50", NumberStyles.Currency, en-US) throw FormatException: AllowParentheses, AllowTrailingSign and AllowCurrencySymbol are never honoured #152 and Honour AllowParentheses, AllowTrailingSign and AllowCurrencySymbol in Parse [patch] #184: Honour AllowParentheses, AllowTrailingSign and AllowCurrencySymbol in Parse [patch] #184 says that once a symbol has been read, the currency separators replace the number separators; it does not address input without a symbol.
- Read white space around a sign or currency symbol as the BCL does [patch] #191, Parse("1 234,5", fr-FR) throws where decimal returns 1234.5: a typed space doesn't match a no-break-space group separator #174 / Let a typed space match a no-break-space separator in Parse [patch] #176, Parse can't read its own N output for NumberNegativePattern 2 and 4: "1,234.50 -" and "5 -" fail, though decimal, double and int parse them #189, Parse/TryParse ignore the IFormatProvider, so a value formatted with ToString(culture) cannot be parsed back with the same culture #106.
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity