diff --git a/src/libraries/Common/src/System/Number.Parsing.Common.cs b/src/libraries/Common/src/System/Number.Parsing.Common.cs index e43cbe14c29226..96661652f59ef7 100644 --- a/src/libraries/Common/src/System/Number.Parsing.Common.cs +++ b/src/libraries/Common/src/System/Number.Parsing.Common.cs @@ -319,6 +319,9 @@ internal enum ParsingStatus private static bool IsSpaceReplacingChar(uint c) => (c == '\u00a0') || (c == '\u202f'); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint NormalizeSpaceReplacingChar(uint c) => IsSpaceReplacingChar(c) ? '\u0020' : c; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe TChar* MatchNegativeSignChars(TChar* p, TChar* pEnd, NumberFormatInfo info) where TChar : unmanaged, IUtfChar @@ -345,14 +348,16 @@ internal enum ParsingStatus if (TChar.CastToUInt32(*str) != '\0') { // We only hurt the failure case - // This fix is for French or Kazakh cultures. Since a user cannot type 0xA0 or 0x202F as a - // space character we use 0x20 space character instead to mean the same. + // This fix is for cultures that use NBSP (U+00A0) or narrow NBSP (U+202F) as group/decimal separators + // (e.g., French, Kazakh, Ukrainian). Since a user cannot easily type these characters, + // we accept regular space (U+0020) as equivalent. + // We also need to handle the reverse case where the input has NBSP and the format string has space. while (true) { uint cp = (p < pEnd) ? TChar.CastToUInt32(*p) : '\0'; uint val = TChar.CastToUInt32(*str); - if ((cp != val) && !(IsSpaceReplacingChar(val) && (cp == '\u0020'))) + if (cp != val && NormalizeSpaceReplacingChar(cp) != NormalizeSpaceReplacingChar(val)) { break; } diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs index bdfde88395a326..4f682c5216e7e9 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs @@ -1331,6 +1331,31 @@ private static void Eval(BigInteger x, string expected) } Assert.Equal(expected, actual); } + + [Fact] + public static void ParseWithNBSPAsGroupSeparator() + { + // Culture has NBSP as group separator; input has regular spaces. + // Exercises MatchChars path: cp=='\u0020' && IsSpaceReplacingChar(val=='\u00A0') + CultureInfo nbspCulture = new CultureInfo("en-US"); + nbspCulture.NumberFormat.NumberGroupSeparator = "\u00A0"; + + BigInteger result = BigInteger.Parse("1 234 567", NumberStyles.AllowThousands, nbspCulture); + Assert.Equal((BigInteger)1234567, result); + + // Culture has regular space as group separator; input has NBSP. + // Exercises MatchChars path: val=='\u0020' && IsSpaceReplacingChar(cp=='\u00A0') + CultureInfo spaceCulture = new CultureInfo("en-US"); + spaceCulture.NumberFormat.NumberGroupSeparator = " "; + + result = BigInteger.Parse("1\u00A0234\u00A0567", NumberStyles.AllowThousands, spaceCulture); + Assert.Equal((BigInteger)1234567, result); + + // Culture has regular space as group separator; input has narrow NBSP (U+202F). + // Exercises IsSpaceReplacingChar matching U+202F against U+0020 + result = BigInteger.Parse("1\u202F234\u202F567", NumberStyles.AllowThousands, spaceCulture); + Assert.Equal((BigInteger)1234567, result); + } } [Collection(nameof(DisableParallelization))]