Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions PreciseNumber.Test/PreciseNumberParseCultureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ public class PreciseNumberParseCultureTests
NegativeSign = "-",
};

// Shaped like ru-RU, pl-PL or sv-SE, which group with U+00A0, and like de-CH, which groups with U+2019.
private static readonly NumberFormatInfo NoBreakSpaceGroups = new()
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = "\u00A0",
NegativeSign = "-",
};

private static readonly NumberFormatInfo ApostropheGroups = new()
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = "\u2019",
NegativeSign = "-",
};

private static readonly NumberFormatInfo NoBreakSpaceDecimal = new()
{
NumberDecimalSeparator = "\u00A0",
NumberGroupSeparator = ".",
NegativeSign = "-",
};

private static readonly NumberFormatInfo MultiCharacterSeparator = new()
{
NumberDecimalSeparator = "<>",
Expand Down Expand Up @@ -172,4 +194,51 @@ public void Parse_ReadsACulturesNegativeSignOnlyWhenTheStyleAllowsIt()
Assert.IsFalse(PreciseNumber.TryParse("−2", NumberStyles.None, CommaDecimal, out _));
Assert.IsFalse(PreciseNumber.TryParse("-2", NumberStyles.None, CommaDecimal, out _));
}

// A keyboard types U+0020, and every BCL numeric type lets it stand in for a U+00A0 or U+202F
// separator, so generic INumberBase<T>.Parse code failed only for PreciseNumber.
[TestMethod]
[DataRow("1 234,5", "1234.5")]
[DataRow("-1 234 567", "-1234567")]
[DataRow("1 234 567,25", "1234567.25")]
public void Parse_AcceptsATypedSpaceForANoBreakSpaceGroupSeparator(string input, string expected)
{
foreach (NumberFormatInfo format in new[] { NarrowSpaceGroups, NoBreakSpaceGroups })
{
Assert.AreEqual(P(expected), PreciseNumber.Parse(input, format));
Assert.AreEqual(P(expected), PreciseNumber.Parse(input, NumberStyles.Number, format));
Assert.AreEqual(P(expected), PreciseNumber.Parse(input, NumberStyles.Any, format));
Assert.AreEqual(decimal.Parse(input, NumberStyles.Number, format), PreciseNumber.Parse(input, NumberStyles.Number, format).To<decimal>());
}
}

[TestMethod]
public void Parse_AcceptsATypedSpaceForANoBreakSpaceDecimalSeparator()
{
Assert.AreEqual(P("1.5"), PreciseNumber.Parse("1 5", NumberStyles.Number, NoBreakSpaceDecimal));
Assert.AreEqual(decimal.Parse("1 5", NumberStyles.Number, NoBreakSpaceDecimal), PreciseNumber.Parse("1 5", NumberStyles.Number, NoBreakSpaceDecimal).To<decimal>());
}

[TestMethod]
public void Parse_StillAcceptsTheRealNoBreakSpace()
{
Assert.AreEqual(P("1234.5"), PreciseNumber.Parse("1\u202F234,5", NarrowSpaceGroups));
Assert.AreEqual(P("1234.5"), PreciseNumber.Parse("1\u00A0234,5", NoBreakSpaceGroups));
}

[TestMethod]
public void Parse_DoesNotLetASpaceStandInForOtherSeparators()
{
Assert.IsFalse(PreciseNumber.TryParse("1 234,5", NumberStyles.Number, ApostropheGroups, out _));
Assert.IsFalse(PreciseNumber.TryParse("1 234,5", NumberStyles.Number, CommaDecimal, out _));
Assert.IsFalse(PreciseNumber.TryParse("1 234", NumberStyles.Number, CultureInfo.InvariantCulture, out _));
Assert.IsFalse(PreciseNumber.TryParse("1\u00A0234", NumberStyles.Number, NarrowSpaceGroups, out _));
}

[TestMethod]
public void Parse_RejectsATypedSpaceWhenTheStyleDoesNotAllowThousands()
{
Assert.IsFalse(PreciseNumber.TryParse("1 234", NumberStyles.Float, NarrowSpaceGroups, out _));
Assert.IsFalse(PreciseNumber.TryParse("1 234", NumberStyles.Float, NoBreakSpaceGroups, out _));
}
}
31 changes: 29 additions & 2 deletions PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@
/// A fixed precision such as <c>E15</c> rounds values that need 17 digits, which turns
/// <see cref="double.MaxValue"/> into a number that converts back to infinity.
/// </remarks>
internal static string GetStringFormatForFloatType<TFloat>()

Check warning on line 747 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 747 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.
where TFloat : INumber<TFloat>
=> "R";

Expand Down Expand Up @@ -1205,7 +1205,7 @@
public static PreciseNumber MinMagnitudeNumber(PreciseNumber x, PreciseNumber y) => MinMagnitude(x, y);

/// <inheritdoc/>
public static PreciseNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider)

Check warning on line 1208 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 28 to the 15 allowed.

Check warning on line 1208 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.

Check warning on line 1208 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 28 to the 15 allowed.

Check warning on line 1208 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.
{
// Read the separator and signs from the same culture TryFormat writes them with, so that
// Parse(x.ToString(p), p) round-trips for every provider.
Expand Down Expand Up @@ -1255,7 +1255,7 @@
for (int i = startIndex; i < s.Length; i++)
{
char c = s[i];
if (allowDecimalPoint && s[i..].StartsWith(decimalSeparator, StringComparison.Ordinal))
if (allowDecimalPoint && StartsWithSeparator(s[i..], decimalSeparator))
{
if (hasDecimal)
{
Expand All @@ -1263,14 +1263,14 @@
}

hasDecimal = true;
i += decimalSeparator.Length - 1;

Check warning on line 1266 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 1266 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

// Group separators may only sit between integral digits.
if (allowThousands && !hasDecimal && digitCount > 0 && s[i..].StartsWith(groupSeparator, StringComparison.Ordinal))
if (allowThousands && !hasDecimal && digitCount > 0 && StartsWithSeparator(s[i..], groupSeparator))
{
i += groupSeparator.Length - 1;

Check warning on line 1273 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 1273 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

Expand Down Expand Up @@ -1310,6 +1310,33 @@
}
}

/// <summary>
/// Determines whether <paramref name="s"/> starts with <paramref name="separator"/>, letting a U+0020
/// space in <paramref name="s"/> stand in for a U+00A0 or U+202F no-break space in the separator.
/// </summary>
/// <remarks>
/// Cultures such as fr-FR, ru-RU and sv-SE group digits with a no-break space, but a keyboard types
/// an ordinary one. The BCL numeric parsers accept it in their place, so <c>decimal.Parse("1 234,5", fr)</c>
/// succeeds, and this matches them. Every other character is compared ordinally.
/// </remarks>
private static bool StartsWithSeparator(ReadOnlySpan<char> s, ReadOnlySpan<char> separator)
{
if (s.Length < separator.Length)
{
return false;
}

for (int i = 0; i < separator.Length; i++)
{
if (s[i] != separator[i] && !(s[i] == ' ' && separator[i] is '\u00A0' or '\u202F'))
{
return false;
}
}

return true;
}

/// <summary>
/// Builds the value <see cref="Parse(ReadOnlySpan{char}, NumberStyles, IFormatProvider?)"/> read: the
/// written <paramref name="digits"/>, of which the last <paramref name="decimalDigits"/> follow the
Expand Down
Loading