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..e7f43ea 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))
@@ -603,12 +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.
- double logValue = Math.Log(Math.Abs(Value.To()));
+ // This computes |x|^p, so the sign of a negative base is restored for odd integer powers.
+ bool isNegativeResult = isNegativeBase && PreciseNumber.IsOddInteger(power);
+ 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());
- double result = isNegativeBase && PreciseNumber.IsOddInteger(power) ? -magnitude : magnitude;
- return result.ToSignificantNumber(significantDigits);
+ if (double.IsNormal(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, isNegative, significantDigits);
}
///
@@ -616,6 +638,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 +656,60 @@ 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 powerOfTen = (int)wholePart;
+ int exponent = powerOfTen - 16;
+
+ int integerDigits = int.Abs(powerOfTen).ToString(CultureInfo.InvariantCulture).Length;
+ int determinedDigits = int.Max(1, DoubleSignificantDigits - integerDigits);
+
+ return CreateFromComponents(exponent, isNegative ? -significand : significand)
+ .Value
+ .ToSignificantNumber(int.Min(significantDigits, determinedDigits));
}
///
@@ -648,6 +723,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 .
///