diff --git a/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs b/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs
new file mode 100644
index 0000000..7fce253
--- /dev/null
+++ b/PreciseNumber.Test/PreciseNumberParseTrailingNulTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.PreciseNumber.Test;
+
+using System.Globalization;
+
+///
+/// Covers the run of trailing '\0' characters the BCL parsers ignore, which text read out of a
+/// fixed-size or NUL-padded buffer ends with. Each case is checked against
+/// parsing the same text with the same style, so the expectation is the BCL's rather than ours.
+///
+[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(() => PreciseNumber.Parse(text, style, culture));
+ }
+}
diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs
index 602eeaf..8a5c81c 100644
--- a/PreciseNumber/PreciseNumber.cs
+++ b/PreciseNumber/PreciseNumber.cs
@@ -1222,6 +1222,7 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor
throw new ArgumentException(UnsupportedParseStylesMessage, nameof(style));
}
+ s = TrimTrailingNuls(s);
s = TrimAllowedWhite(s, style);
if (s.IsEmpty)
@@ -1403,6 +1404,21 @@ private static PreciseNumber FromParsedParts(ReadOnlySpan digits, int deci
///
private const string AllowedWhite = " \t\n\v\f\r";
+ ///
+ /// Removes the run of '\0' characters that ends , as the BCL parsers
+ /// ignore it, so that text from a NUL-padded buffer parses the way reads it.
+ ///
+ ///
+ /// 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.
+ ///
+ private static ReadOnlySpan TrimTrailingNuls(ReadOnlySpan s)
+ {
+ ReadOnlySpan trimmed = s.TrimEnd('\0');
+ return trimmed.IsEmpty ? s : trimmed;
+ }
+
/// Trims the leading and trailing white space that allows.
private static ReadOnlySpan TrimAllowedWhite(ReadOnlySpan s, NumberStyles style)
{