diff --git a/PreciseNumber.Test/PreciseNumberParseExponentTests.cs b/PreciseNumber.Test/PreciseNumberParseExponentTests.cs
new file mode 100644
index 0000000..1a77354
--- /dev/null
+++ b/PreciseNumber.Test/PreciseNumberParseExponentTests.cs
@@ -0,0 +1,84 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.PreciseNumber.Test;
+
+using System.Globalization;
+using System.Numerics;
+
+///
+/// Pins how Parse applies a written exponent at the edges of the exponent range:
+/// padding zeros must not overflow a value that fits, and a zero is zero whatever its exponent.
+///
+[TestClass]
+public sealed class PreciseNumberParseExponentTests
+{
+ [TestMethod]
+ [DataRow("1e-2147483646", -2147483646, 1)]
+ [DataRow("1.000000E-2147483646", -2147483646, 1)]
+ [DataRow("2.500E-2147483647", -2147483648, 25)]
+ [DataRow("1.0e-2147483648", -2147483648, 1)]
+ [DataRow("-1.50e-2147483647", -2147483648, -15)]
+ [DataRow("10e2147483646", 2147483647, 1)]
+ [DataRow("1000e2147483644", 2147483647, 1)]
+ public void Parse_AppliesTheExponentAfterDroppingPaddingZeros(string text, int exponent, int significand)
+ {
+ PreciseNumber expected = new(exponent, new BigInteger(significand));
+
+ Assert.AreEqual(expected, PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture));
+ Assert.IsTrue(PreciseNumber.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out PreciseNumber parsed));
+ Assert.AreEqual(expected, parsed);
+ }
+
+ [TestMethod]
+ [DataRow("0e99999999999")]
+ [DataRow("0e-99999999999")]
+ [DataRow("0.000e99999999999999999999999")]
+ [DataRow("-0e2147483648")]
+ [DataRow("000e5")]
+ public void Parse_ReadsZeroWhateverItsWrittenExponent(string text)
+ {
+ Assert.AreEqual(0m, decimal.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture));
+ Assert.AreEqual(PreciseNumber.Zero, PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture));
+ }
+
+ [TestMethod]
+ [DataRow("1e2147483648")]
+ [DataRow("1e-2147483649")]
+ [DataRow("0.1e-2147483648")]
+ [DataRow("1e99999999999999999999")]
+ public void Parse_StillOverflowsForAValueOutsideTheExponentRange(string text)
+ {
+ Assert.ThrowsExactly(() => PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture));
+ Assert.IsFalse(PreciseNumber.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out _));
+ }
+
+ [TestMethod]
+ [DataRow("0e")]
+ [DataRow("0ex")]
+ [DataRow("0e 5")]
+ public void Parse_RejectsAMalformedExponentEvenOnZero(string text) =>
+ Assert.ThrowsExactly(() => PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture));
+
+ [TestMethod]
+ public void Parse_RoundTripsScientificTextAtTheEdgesOfTheExponentRange()
+ {
+ // At most seven digits, which is all the default "E" format writes.
+ BigInteger[] significands = [BigInteger.One, new BigInteger(25), new BigInteger(-1234567)];
+ for (int offset = 0; offset <= 10; offset++)
+ {
+ foreach (BigInteger significand in significands)
+ {
+ foreach (int exponent in new[] { int.MinValue + offset, int.MaxValue - offset })
+ {
+ PreciseNumber value = new(exponent, significand);
+ string text = value.ToString("E", CultureInfo.InvariantCulture);
+
+ Assert.IsTrue(
+ PreciseNumber.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out PreciseNumber parsed),
+ $"\"{text}\" did not parse.");
+ Assert.AreEqual(value, parsed, $"\"{text}\" parsed to a different value.");
+ }
+ }
+ }
+ }
+}
diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs
index 40355c7..2da3467 100644
--- a/PreciseNumber/PreciseNumber.cs
+++ b/PreciseNumber/PreciseNumber.cs
@@ -1232,9 +1232,10 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor
try
{
int digitCount = 0;
- int exponent = 0;
bool hasDecimal = false;
int decimalDigits = 0;
+ ReadOnlySpan exponentText = [];
+ bool hasExponent = false;
for (int i = startIndex; i < s.Length; i++)
{
@@ -1260,10 +1261,8 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor
if (allowExponent && c is 'e' or 'E')
{
- // An exponent outside the range of int throws OverflowException, which TryParse reports as failure.
- // 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);
+ exponentText = s[(i + 1)..];
+ hasExponent = true;
break;
}
@@ -1285,16 +1284,7 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor
throw new FormatException(InvalidFormatMessage);
}
- BigInteger significand = BigInteger.Parse(digits[..digitCount], NumberStyles.None, InvariantCulture);
-
- exponent = checked(exponent - decimalDigits);
-
- if (isNegative)
- {
- significand = -significand;
- }
-
- return new(exponent, significand);
+ return FromParsedParts(digits[..digitCount], decimalDigits, hasExponent, exponentText, isNegative, numberFormat);
}
finally
{
@@ -1305,6 +1295,60 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor
}
}
+ ///
+ /// Builds the value read: the
+ /// written , of which the last follow the
+ /// decimal separator, scaled by when is set.
+ ///
+ private static PreciseNumber FromParsedParts(ReadOnlySpan digits, int decimalDigits, bool hasExponent, ReadOnlySpan exponentText, bool isNegative, NumberFormatInfo numberFormat)
+ {
+ // Drop trailing zeros before applying the exponent, so that padding such as the zeros in
+ // "2.500E-2147483647" can't push the arithmetic past int when the value itself fits.
+ int significantDigitCount = digits.Length;
+ while (significantDigitCount > 0 && digits[significantDigitCount - 1] == '0')
+ {
+ significantDigitCount--;
+ }
+
+ bool isZero = significantDigitCount == 0;
+ long exponent = 0;
+ if (hasExponent)
+ {
+ // 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. A zero is zero whatever its written
+ // exponent, as for decimal and double, so only a nonzero value can overflow here.
+ try
+ {
+ exponent = long.Parse(exponentText, NumberStyles.AllowLeadingSign, numberFormat);
+ }
+ catch (OverflowException) when (isZero)
+ {
+ return Zero;
+ }
+ }
+
+ if (isZero)
+ {
+ return Zero;
+ }
+
+ // An exponent outside the range of int throws OverflowException, which TryParse reports as failure.
+ exponent = checked(exponent - decimalDigits + (digits.Length - significantDigitCount));
+ if (exponent is < int.MinValue or > int.MaxValue)
+ {
+ throw new OverflowException("Value was either too large or too small for a PreciseNumber.");
+ }
+
+ BigInteger significand = BigInteger.Parse(digits[..significantDigitCount], NumberStyles.None, InvariantCulture);
+
+ if (isNegative)
+ {
+ significand = -significand;
+ }
+
+ return new((int)exponent, significand);
+ }
+
/// Trims the leading and trailing white space that allows.
private static ReadOnlySpan TrimAllowedWhite(ReadOnlySpan s, NumberStyles style)
{