Repository navigation
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
Description
What's wrong
Many cultures use a no-break space as the group separator:
- U+202F narrow no-break space:
fr-FR - U+00A0 no-break space:
ru-RU,pl-PL,sv-SE,cs-CZ,nb-NO,fr-CA, and others
Parse matches the group separator by ordinal comparison only:
PreciseNumber/PreciseNumber/PreciseNumber.cs
Lines 1253 to 1258 in c13cd7f
| // Group separators may only sit between integral digits. | |
| if (allowThousands && !hasDecimal && digitCount > 0 && s[i..].StartsWith(groupSeparator, StringComparison.Ordinal)) | |
| { | |
| i += groupSeparator.Length - 1; | |
| continue; |
if (allowThousands && !hasDecimal && digitCount > 0 && s[i..].StartsWith(groupSeparator, StringComparison.Ordinal))That means a number typed with an ordinary space (U+0020), which is what a keyboard produces, is rejected.
Every BCL numeric type accepts it. The runtime's number parser deliberately treats U+0020 as matching a U+00A0 or U+202F separator (IsSpaceReplacingChar / MatchChars in Number.Parsing.cs), so decimal, double, and int all parse these strings. Generic code calling INumberBase<T>.Parse(s, provider) therefore works for every built-in type and fails only for PreciseNumber. That includes the provider-only overloads, which use NumberStyles.Any.
PreciseNumberParseCultureTests covers a U+202F separator (NarrowSpaceGroups), but only to round-trip the library's own output. It has no case for typed input.
Reproduction (net10.0, NumberStyles.Number; Any and AllowThousands behave the same)
All inputs below use a plain U+0020 space between digit groups.
| Culture (group separator) | Input | decimal | PreciseNumber |
|---|---|---|---|
| fr-FR (U+202F) | "1 234,5" |
1234.5 | FormatException |
| fr-FR | "-1 234 567" |
-1234567 | FormatException |
| ru-RU / pl-PL / cs-CZ / fr-CA (U+00A0) | "1 234,5" |
1234.5 | FormatException |
| sv-SE / nb-NO (U+00A0) | "-1 234 567" |
-1234567 | FormatException |
| fr-FR, input with a real U+202F | "1 234,5" |
1234.5 | 1234.5 (works) |
| de-CH (U+2019) | "1 234,5" |
fails | fails (agree) |
var fr = new CultureInfo("fr-FR");
decimal.Parse("1 234,5", fr); // 1234.5
PreciseNumber.Parse("1 234,5", fr); // FormatExceptionSuggested fix / acceptance criteria
- When matching the group separator, and the decimal separator for parity with the BCL, let a U+0020 in the input match a U+00A0 or U+202F in the culture's separator, character by character, the way the runtime's
MatchCharsdoes. - Everything else stays ordinal. For example, de-CH's U+2019 still requires U+2019, and a culture whose separator is not a no-break space does not start accepting spaces.
- If 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 (currency separators) lands first, apply the same rule to
CurrencyGroupSeparatorandCurrencyDecimalSeparator. - Tests:
- Every failing row above parses to the
decimalresult. - Existing round-trips such as
Parse(x.ToString("N2", fr), fr)are unchanged. "1 234"still fails underNumberStyles.Float(noAllowThousands).
- Every failing row above parses to the
Activity
matt-edmondson commentedon Oct 20, 2026
Triage
Parsedisagrees withdecimal/doublefor normal typed input in many cultures (fr-FR, ru-RU, pl-PL, sv-SE and others), and genericINumberBase<T>.Parsecode fails only forPreciseNumber. There is a workaround (type the real no-break space), and nothing is corrupted silently: the call throws.PreciseNumber.Parseseparator matching (PreciseNumber.csaround L1253) andPreciseNumberParseCultureTests. Owner: @matt-edmondsonMatchChars-style helper that lets U+0020 match U+00A0/U+202F, and use it for both the group and decimal separators. Add the table's rows as tests, and keep theNumberStyles.Floatnegative case.Generated by Claude Code