From 5b49fee8fa6404abacaf9a0e9f71a3139f7c4502 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 00:29:07 +0000 Subject: [PATCH 1/3] Compute Pow and Exp beyond the range of a double [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pow and Exp computed through double whenever they left the exact integer path, so a result above double's range threw "Infinite values are not supported" and one below it silently became an exact zero. When the double result isn't a normal number, work in base-10 logarithms built from the base's significand and exponent instead, and rebuild the result with its power of ten kept as an integer. The result is rounded to the digits the logarithm can determine, and a power of ten beyond ±1e9 throws OverflowException rather than returning infinity or zero. Fixes ktsu-dev/SignificantNumber#106 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QBrEwaLGeo2SvjVK3AnLrp --- .../SignificantNumberRangeTests.cs | 72 ++++++++++++++++++ SignificantNumber/SignificantNumber.cs | 76 ++++++++++++++++++- 2 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 SignificantNumber.Test/SignificantNumberRangeTests.cs diff --git a/SignificantNumber.Test/SignificantNumberRangeTests.cs b/SignificantNumber.Test/SignificantNumberRangeTests.cs new file mode 100644 index 0000000..f2864f9 --- /dev/null +++ b/SignificantNumber.Test/SignificantNumberRangeTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace SignificantNumber.Test; + +using System.Globalization; +using ktsu.PreciseNumber; +using ktsu.SignificantNumber; + +/// +/// Covers and for results outside the range +/// of a . +/// +[TestClass] +public class SignificantNumberRangeTests +{ + private static SignificantNumber Parse(string text) => + SignificantNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + private static PreciseNumber Precise(string text) => + PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + [TestMethod] + [DataRow("10", "2000", "1E2000")] + [DataRow("1.5", "2000", "1.5E352")] + [DataRow("1E400", "0.5", "1E200")] + [DataRow("-10", "2001", "-1E2001")] + public void Pow_ResultAboveDoubleRange_ReturnsMagnitude(string baseNumber, string power, string expected) => + Assert.AreEqual(Parse(expected), Parse(baseNumber).Pow(Precise(power))); + + [TestMethod] + [DataRow("2", "-1100", "7E-332")] + [DataRow("1E-400", "0.5", "1E-200")] + [DataRow("1E400", "-0.5", "1E-200")] + [DataRow("-2", "-1101", "-4E-332")] + public void Pow_ResultBelowDoubleRange_ReturnsNonzeroMagnitude(string baseNumber, string power, string expected) => + Assert.AreEqual(Parse(expected), Parse(baseNumber).Pow(Precise(power))); + + [TestMethod] + public void Exp_ResultAboveDoubleRange_ReturnsMagnitude() + { + // e^710 = 2.2339...E308 and e^1000 = 1.9700711140170469...E434 + SignificantNumber exp710 = SignificantNumber.Exp(Precise("710")); + SignificantNumber exp1000 = SignificantNumber.Exp(Precise("1000")); + + Assert.AreEqual(308, exp710.Exponent + exp710.SignificantDigits - 1); + Assert.AreEqual(434, exp1000.Exponent + exp1000.SignificantDigits - 1); + Assert.AreEqual(Parse("1.97007111402E434"), exp1000); + } + + [TestMethod] + public void Exp_ResultBelowDoubleRange_ReturnsNonzeroMagnitude() + { + // e^-1000 = 5.0759588975494567...E-435 + SignificantNumber result = SignificantNumber.Exp(Precise("-1000")); + + Assert.AreEqual(Parse("5.07595889755E-435"), result); + } + + [TestMethod] + public void Pow_PowerOfTenBeyondExponentRange_ThrowsOverflowException() + { + Assert.ThrowsExactly(() => Parse("10").Pow(Precise("1E300"))); + Assert.ThrowsExactly(() => Parse("10").Pow(Precise("-1E300"))); + } + + [TestMethod] + public void Exp_PowerBeyondExponentRange_ThrowsOverflowException() + { + Assert.ThrowsExactly(() => SignificantNumber.Exp(Precise("1E300"))); + Assert.ThrowsExactly(() => SignificantNumber.Exp(Precise("-1E300"))); + } +} diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 5ca3bf0..83a4623 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -563,6 +563,7 @@ internal static bool DoesImplementGenericInterface(Type type, Type genericInterf /// The current number raised to , rounded to the fewest significant digits of the two. /// Thrown when the current number is zero and is negative. /// Thrown when the current number is negative and is not an integer, as the result is not a real number. + /// Thrown when the result's power of ten is too large or too small to represent. public SignificantNumber Pow(PreciseNumber power) { if (Equal(power, Zero)) @@ -605,10 +606,19 @@ public SignificantNumber Pow(PreciseNumber power) // Use logarithm and exponential to support decimal powers. This computes |x|^p, so the // sign of a negative base is restored for odd integer powers. + bool isNegativeResult = isNegativeBase && PreciseNumber.IsOddInteger(power); double logValue = Math.Log(Math.Abs(Value.To())); double magnitude = Math.Exp(logValue * power.To()); - double result = isNegativeBase && PreciseNumber.IsOddInteger(power) ? -magnitude : magnitude; - return result.ToSignificantNumber(significantDigits); + if (double.IsNormal(magnitude)) + { + double result = isNegativeResult ? -magnitude : magnitude; + return result.ToSignificantNumber(significantDigits); + } + + // The base or the result is outside the range of a double, so work in base-10 logarithms + // built from the base's significand and exponent, which never pass through a double. + double log10Magnitude = Log10OfMagnitude(Value) * power.To(); + return FromLog10OfMagnitude(log10Magnitude, isNegativeResult, significantDigits); } /// @@ -616,6 +626,7 @@ public SignificantNumber Pow(PreciseNumber power) /// /// The power to raise e to. /// e raised to , rounded to the fewest significant digits of the two. + /// Thrown when the result's power of ten is too large or too small to represent. public static SignificantNumber Exp(PreciseNumber power) { if (Equal(power, Zero)) @@ -633,8 +644,59 @@ public static SignificantNumber Exp(PreciseNumber power) ? int.Min(E.SignificantDigits, DoubleSignificantDigits) : LowestSignificantDigits(E, power); - return Math.Exp(power.To()) - .ToSignificantNumber(significantDigits); + double result = Math.Exp(power.To()); + if (double.IsNormal(result)) + { + return result.ToSignificantNumber(significantDigits); + } + + // The result is outside the range of a double: e^p = 10^(p × log10(e)). + return FromLog10OfMagnitude(power.To() * Math.Log10(Math.E), isNegative: false, significantDigits); + } + + /// + /// Computes the base-10 logarithm of the magnitude of a nonzero number from its significand and exponent, so + /// that it works for magnitudes outside the range of a . + /// + /// A nonzero number. + /// log10(||). + private static double Log10OfMagnitude(PreciseNumber value) => + BigInteger.Log10(BigInteger.Abs(value.Significand)) + value.Exponent; + + /// + /// Builds a number from the base-10 logarithm of its magnitude, keeping the power of ten as an integer so that + /// the result can lie outside the range of a . + /// + /// log10 of the magnitude of the result. + /// Whether the result is negative. + /// The number of significant digits the result should have. + /// + /// The result, rounded to , or to fewer digits when the logarithm cannot + /// determine that many: a double carries about 15 significant digits, and the integer part of the logarithm + /// uses some of them. + /// + /// Thrown when the result's power of ten is too large or too small to represent. + private static SignificantNumber FromLog10OfMagnitude(double log10Magnitude, bool isNegative, int significantDigits) + { + double wholePart = Math.Floor(log10Magnitude); + if (!double.IsFinite(wholePart) || Math.Abs(wholePart) > MaxLog10Magnitude) + { + throw new OverflowException(log10Magnitude > 0 + ? "The result is too large to represent." + : "The result is too small to represent."); + } + + // 10^fraction is in [1, 10), so scaling it by 10^16 gives a 17-digit significand that a double holds exactly. + double mantissa = Math.Pow(10, log10Magnitude - wholePart); + BigInteger significand = new(Math.Round(mantissa * 1e16)); + int exponent = (int)wholePart - 16; + + int integerDigits = wholePart == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(wholePart))) + 1; + int determinedDigits = int.Max(1, DoubleSignificantDigits - integerDigits); + + return CreateFromComponents(exponent, isNegative ? -significand : significand) + .Value + .ToSignificantNumber(int.Min(significantDigits, determinedDigits)); } /// @@ -648,6 +710,12 @@ public static SignificantNumber Exp(PreciseNumber power) /// private const int MaxExactIntegerPower = 1024; + /// + /// The largest power of ten, in either direction, that and produce. It keeps the + /// exponent of the result well inside the range of an . + /// + private const int MaxLog10Magnitude = 1_000_000_000; + /// /// Gets the value of an integer as a . /// From ed5c9a08c3279f84e8642e85dc9eed7b8ef02adf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 00:32:02 +0000 Subject: [PATCH 2/3] Count the result's integer digits without a floating-point comparison Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QBrEwaLGeo2SvjVK3AnLrp --- SignificantNumber/SignificantNumber.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 83a4623..9e46f98 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -689,9 +689,10 @@ private static SignificantNumber FromLog10OfMagnitude(double log10Magnitude, boo // 10^fraction is in [1, 10), so scaling it by 10^16 gives a 17-digit significand that a double holds exactly. double mantissa = Math.Pow(10, log10Magnitude - wholePart); BigInteger significand = new(Math.Round(mantissa * 1e16)); - int exponent = (int)wholePart - 16; + int powerOfTen = (int)wholePart; + int exponent = powerOfTen - 16; - int integerDigits = wholePart == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(wholePart))) + 1; + int integerDigits = int.Abs(powerOfTen).ToString(CultureInfo.InvariantCulture).Length; int determinedDigits = int.Max(1, DoubleSignificantDigits - integerDigits); return CreateFromComponents(exponent, isNegative ? -significand : significand) From e6910049771943d6ec2622bc313f2be641bc52a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 00:41:42 +0000 Subject: [PATCH 3/3] Move Pow's logarithm path into a helper to keep its complexity within bounds Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QBrEwaLGeo2SvjVK3AnLrp --- SignificantNumber/SignificantNumber.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 9e46f98..e7f43ea 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -604,21 +604,33 @@ public SignificantNumber Pow(PreciseNumber power) ? int.Min(Value.SignificantDigits, DoubleSignificantDigits) : LowestSignificantDigits(this, power); - // Use logarithm and exponential to support decimal powers. This computes |x|^p, so the - // sign of a negative base is restored for odd integer powers. + // This computes |x|^p, so the sign of a negative base is restored for odd integer powers. bool isNegativeResult = isNegativeBase && PreciseNumber.IsOddInteger(power); - double logValue = Math.Log(Math.Abs(Value.To())); + return PowByLogarithm(Value, power, isNegativeResult, significantDigits); + } + + /// + /// Raises the magnitude of a nonzero number to a power by logarithm and exponential, which supports decimal powers. + /// + /// The nonzero base. + /// The power to raise the magnitude of to. + /// Whether the result is negative. + /// The number of significant digits the result should have. + /// ||^, negated when is set. + private static SignificantNumber PowByLogarithm(PreciseNumber value, PreciseNumber power, bool isNegative, int significantDigits) + { + double logValue = Math.Log(Math.Abs(value.To())); double magnitude = Math.Exp(logValue * power.To()); if (double.IsNormal(magnitude)) { - double result = isNegativeResult ? -magnitude : magnitude; + double result = isNegative ? -magnitude : magnitude; return result.ToSignificantNumber(significantDigits); } // The base or the result is outside the range of a double, so work in base-10 logarithms // built from the base's significand and exponent, which never pass through a double. - double log10Magnitude = Log10OfMagnitude(Value) * power.To(); - return FromLog10OfMagnitude(log10Magnitude, isNegativeResult, significantDigits); + double log10Magnitude = Log10OfMagnitude(value) * power.To(); + return FromLog10OfMagnitude(log10Magnitude, isNegative, significantDigits); } ///