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
54 changes: 54 additions & 0 deletions PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.PreciseNumber.Test;

using System.Globalization;

/// <summary>
/// Covers the run of trailing <c>'\0'</c> characters the BCL parsers ignore, which text read out of a
/// fixed-size or NUL-padded buffer ends with. Each case is checked against <see cref="decimal"/>
/// parsing the same text with the same style, so the expectation is the BCL's rather than ours.
/// </summary>
[TestClass]
public class PreciseNumberParseTrailingNulTests
{
[TestMethod]
[DataRow("5\0", NumberStyles.None)]
[DataRow("5\0", NumberStyles.Float)]
[DataRow("1.5\0\0\0", NumberStyles.Float)]
[DataRow("1e3 \0", NumberStyles.Float)]
[DataRow("12345\0\0\0\0\0\0\0\0", NumberStyles.Integer)]
[DataRow("-0.25\0", NumberStyles.Number)]
public void ParseIgnoresTrailingNulsAsDecimalDoes(string text, NumberStyles style)
{
CultureInfo culture = CultureInfo.InvariantCulture;
Assert.IsTrue(decimal.TryParse(text, style, culture, out decimal expected), "The case must be one decimal accepts.");
PreciseNumber expectedNumber = expected.ToPreciseNumber();

Assert.AreEqual(expectedNumber, PreciseNumber.Parse(text, style, culture));
Assert.AreEqual(expectedNumber, PreciseNumber.Parse(text.AsSpan(), style, culture));

Assert.IsTrue(PreciseNumber.TryParse(text, style, culture, out PreciseNumber fromString));
Assert.AreEqual(expectedNumber, fromString);

Assert.IsTrue(PreciseNumber.TryParse(text.AsSpan(), style, culture, out PreciseNumber fromSpan));
Assert.AreEqual(expectedNumber, fromSpan);
}

[TestMethod]
[DataRow("1.5\0 ", NumberStyles.Float)]
[DataRow("\0 5", NumberStyles.Float)]
[DataRow("\0", NumberStyles.Float)]
[DataRow("\0\0\0", NumberStyles.Float)]
[DataRow("5 \0", NumberStyles.None)]
[DataRow("5\0" + "1", NumberStyles.Float)]
public void ParseStillRejectsNulsThatAreNotTheLastCharacters(string text, NumberStyles style)
{
CultureInfo culture = CultureInfo.InvariantCulture;
Assert.IsFalse(decimal.TryParse(text, style, culture, out _), "The case must be one decimal rejects.");

Assert.IsFalse(PreciseNumber.TryParse(text, style, culture, out _));
Assert.IsFalse(PreciseNumber.TryParse(text.AsSpan(), style, culture, out _));
Assert.ThrowsExactly<FormatException>(() => PreciseNumber.Parse(text, style, culture));
}
}
16 changes: 16 additions & 0 deletions PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,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 751 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 751 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 @@ -1209,7 +1209,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 1212 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 30 to the 15 allowed.

Check warning on line 1212 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 1212 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 30 to the 15 allowed.

Check warning on line 1212 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 @@ -1222,6 +1222,7 @@
throw new ArgumentException(UnsupportedParseStylesMessage, nameof(style));
}

s = TrimTrailingNuls(s);
s = TrimAllowedWhite(s, style);

if (s.IsEmpty)
Expand Down Expand Up @@ -1268,14 +1269,14 @@
}

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

Check warning on line 1272 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 1272 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 && StartsWithSeparator(s[i..], groupSeparator))
{
i += groupSeparator.Length - 1;

Check warning on line 1279 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 1279 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 @@ -1403,6 +1404,21 @@
/// </summary>
private const string AllowedWhite = " \t\n\v\f\r";

/// <summary>
/// Removes the run of <c>'\0'</c> characters that ends <paramref name="s"/>, as the BCL parsers
/// ignore it, so that text from a NUL-padded buffer parses the way <see cref="decimal"/> reads it.
/// </summary>
/// <remarks>
/// Under every style, white space included: the NULs are dropped only when they are the very last
/// characters, and only when something else comes before them, so text that is nothing but NULs
/// is still an error.
/// </remarks>
private static ReadOnlySpan<char> TrimTrailingNuls(ReadOnlySpan<char> s)
{
ReadOnlySpan<char> trimmed = s.TrimEnd('\0');
return trimmed.IsEmpty ? s : trimmed;
}

/// <summary>Trims the leading and trailing white space that <paramref name="style"/> allows.</summary>
private static ReadOnlySpan<char> TrimAllowedWhite(ReadOnlySpan<char> s, NumberStyles style)
{
Expand Down
Loading