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
50 changes: 50 additions & 0 deletions PreciseNumber.Test/PreciseNumberParseCultureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,54 @@ public void Parse_ReadsTheExponentSignWithTheProvider()
{
Assert.AreEqual(P("0.015"), PreciseNumber.Parse("1,5e−2", CommaDecimal));
}

[TestMethod]
[DataRow("1.5", NumberStyles.Integer)]
[DataRow("1e5", NumberStyles.Integer)]
[DataRow("-5", NumberStyles.None)]
[DataRow("1e 5", NumberStyles.Float)]
[DataRow("1e5", NumberStyles.Number)]
[DataRow("1.5", NumberStyles.None)]
[DataRow("1.5", NumberStyles.Float)]
[DataRow("-1.5e-3", NumberStyles.Float)]
[DataRow("-1.5e-3", NumberStyles.Any)]
[DataRow("1,234.5", NumberStyles.Any)]
[DataRow("-5", NumberStyles.Integer)]
[DataRow("1e+5", NumberStyles.Float)]
public void TryParse_AcceptsWhatDecimalAccepts(string text, NumberStyles style)
{
bool decimalParsed = decimal.TryParse(text, style, CultureInfo.InvariantCulture, out decimal expected);
bool parsed = PreciseNumber.TryParse(text, style, CultureInfo.InvariantCulture, out PreciseNumber actual);

Assert.AreEqual(decimalParsed, parsed, $"PreciseNumber and decimal disagree on whether \"{text}\" parses under {style}.");
if (parsed)
{
Assert.AreEqual(P(expected.ToString(CultureInfo.InvariantCulture)), actual);
}
}

[TestMethod]
public void Parse_ThrowsForWhatTheStyleDoesNotAllow()
{
Assert.ThrowsExactly<FormatException>(() => PreciseNumber.Parse("1.5", NumberStyles.Integer, CultureInfo.InvariantCulture));
Assert.ThrowsExactly<FormatException>(() => PreciseNumber.Parse("1e5", NumberStyles.Integer, CultureInfo.InvariantCulture));
Assert.ThrowsExactly<FormatException>(() => PreciseNumber.Parse("-5", NumberStyles.None, CultureInfo.InvariantCulture));
Assert.ThrowsExactly<FormatException>(() => PreciseNumber.Parse("1e 5", NumberStyles.Float, CultureInfo.InvariantCulture));
}

[TestMethod]
public void Parse_WithoutAStyleStillReadsFractionsExponentsAndSigns()
{
Assert.AreEqual(P("1.5"), PreciseNumber.Parse("1.5", CultureInfo.InvariantCulture));
Assert.AreEqual(P("-0.0015"), PreciseNumber.Parse("-1.5e-3", CultureInfo.InvariantCulture));
Assert.AreEqual(P("1234.5"), PreciseNumber.Parse("1,234.5", CultureInfo.InvariantCulture));
}

[TestMethod]
public void Parse_ReadsACulturesNegativeSignOnlyWhenTheStyleAllowsIt()
{
Assert.AreEqual(P("-2.5"), PreciseNumber.Parse("−2,5", NumberStyles.Float, CommaDecimal));
Assert.IsFalse(PreciseNumber.TryParse("−2", NumberStyles.None, CommaDecimal, out _));
Assert.IsFalse(PreciseNumber.TryParse("-2", NumberStyles.None, CommaDecimal, out _));
}
}
21 changes: 16 additions & 5 deletions PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,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 734 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 734 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 @@ -1189,7 +1189,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 1192 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 1192 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 1192 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 1192 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 All @@ -1210,6 +1210,8 @@
ReadOnlySpan<char> decimalSeparator = numberFormat.NumberDecimalSeparator;
ReadOnlySpan<char> groupSeparator = numberFormat.NumberGroupSeparator;
bool allowThousands = style.HasFlag(NumberStyles.AllowThousands) && !groupSeparator.IsEmpty;
bool allowDecimalPoint = style.HasFlag(NumberStyles.AllowDecimalPoint);
bool allowExponent = style.HasFlag(NumberStyles.AllowExponent);

int startIndex = ReadLeadingSign(s, style, numberFormat, out bool isNegative);

Expand All @@ -1229,7 +1231,7 @@
for (int i = startIndex; i < s.Length; i++)
{
char c = s[i];
if (s[i..].StartsWith(decimalSeparator, StringComparison.Ordinal))
if (allowDecimalPoint && s[i..].StartsWith(decimalSeparator, StringComparison.Ordinal))
{
if (hasDecimal)
{
Expand All @@ -1237,21 +1239,23 @@
}

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

Check warning on line 1242 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 1242 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))
{
i += groupSeparator.Length - 1;

Check warning on line 1249 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 1249 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;
}

if (c is 'e' or 'E')
if (allowExponent && c is 'e' or 'E')
{
// An exponent outside the range of int throws OverflowException, which TryParse reports as failure.
exponent = int.Parse(s[(i + 1)..], NumberStyles.Integer, numberFormat);
// Only a sign may precede its digits: NumberStyles.Integer would also let white space through,
// accepting "1e 5" where every BCL numeric type rejects it.
exponent = int.Parse(s[(i + 1)..], NumberStyles.AllowLeadingSign, numberFormat);
break;
}

Expand Down Expand Up @@ -1313,10 +1317,17 @@
/// Reads a leading sign spelled as <paramref name="numberFormat"/> spells it, returning the index
/// of the first character after it. The ASCII hyphen stays accepted whatever the culture spells
/// its negative sign with, as it always has been here and as the BCL does for cultures that use
/// U+2212. A positive sign is only read when <paramref name="style"/> allows a leading sign.
/// U+2212. Neither sign is read unless <paramref name="style"/> allows a leading sign, so under
/// <see cref="NumberStyles.None"/> a sign is left in place and rejected as a non-digit.
/// </summary>
private static int ReadLeadingSign(ReadOnlySpan<char> s, NumberStyles style, NumberFormatInfo numberFormat, out bool isNegative)
{
isNegative = false;
if (!style.HasFlag(NumberStyles.AllowLeadingSign))
{
return 0;
}

isNegative = true;
if (s.StartsWith(numberFormat.NegativeSign, StringComparison.Ordinal))
{
Expand All @@ -1329,7 +1340,7 @@
}

isNegative = false;
return style.HasFlag(NumberStyles.AllowLeadingSign) && s.StartsWith(numberFormat.PositiveSign, StringComparison.Ordinal)
return s.StartsWith(numberFormat.PositiveSign, StringComparison.Ordinal)
? numberFormat.PositiveSign.Length
: 0;
}
Expand Down
Loading