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 . ///