diff --git a/SignificantNumber.Test/SignificantNumberTests.cs b/SignificantNumber.Test/SignificantNumberTests.cs index 42ce997..4cebeee 100644 --- a/SignificantNumber.Test/SignificantNumberTests.cs +++ b/SignificantNumber.Test/SignificantNumberTests.cs @@ -716,6 +716,56 @@ public void Pow_NegativeBaseAndOddExponent_ReturnsNegativeResult() Assert.AreEqual(SignificantNumber.CreateFromComponents(0, new BigInteger(-8)), result); } + [TestMethod] + public void Pow_IntegerExponent_MatchesRepeatedMultiplication() + { + SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-3, new BigInteger(1234)); + SignificantNumber two = SignificantNumber.CreateFromComponents(0, new BigInteger(2)); + SignificantNumber three = SignificantNumber.CreateFromComponents(0, new BigInteger(3)); + + SignificantNumber squared = baseNumber.Pow(two); + SignificantNumber cubed = baseNumber.Pow(three); + + // 1.234^2 = 1.522756 and 1.234^3 = 1.879080904, each kept to the base's 4 significant digits + Assert.AreEqual(baseNumber * baseNumber, squared); + Assert.AreEqual(1.523, squared.To()); + Assert.AreEqual(4, squared.SignificantDigits); + Assert.AreEqual(baseNumber * baseNumber * baseNumber, cubed); + Assert.AreEqual(1.879, cubed.To()); + } + + [TestMethod] + public void Pow_NegativeBaseAndOddExponent_KeepsBasePrecision() + { + SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-1, new BigInteger(-25)); + SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(3)); + SignificantNumber result = baseNumber.Pow(power); + + // (-2.5)^3 = -15.625, kept to the base's 2 significant digits + Assert.AreEqual(-16.0, result.To()); + } + + [TestMethod] + public void Pow_NegativeIntegerExponent_MatchesReciprocalOfPower() + { + SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-3, new BigInteger(1234)); + SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(-2)); + SignificantNumber result = baseNumber.Pow(power); + + // 1.234^-2 = 0.65670..., kept to the base's 4 significant digits + Assert.AreEqual(0.6567, result.To()); + } + + [TestMethod] + public void Exp_IntegerExponent_KeepsMoreThanOneSignificantDigit() + { + SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(2)); + SignificantNumber result = SignificantNumber.Exp(power); + + Assert.IsGreaterThan(1, result.SignificantDigits); + Assert.AreEqual(Math.Exp(2), result.To(), 1e-12); + } + [TestMethod] public void Pow_NegativeOneAndOddExponent_ReturnsNegativeOne() { diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 522f7d0..5ca3bf0 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -586,7 +586,22 @@ public SignificantNumber Pow(PreciseNumber power) throw new ArgumentException("Cannot raise a negative number to a non-integer power.", nameof(power)); } - int significantDigits = LowestSignificantDigits(this, power); + if (PreciseNumber.IsInteger(power) && Math.Abs(power.To()) <= MaxExactIntegerPower) + { + // An integer power is an exact count, like the counting numbers in a multiplication, so the + // result keeps the base's significant digits. Computing it by exact repeated multiplication + // makes x.Pow(2) agree with x * x and keeps the sign of a negative base. + BigInteger exponent = IntegerValue(power); + PreciseNumber positivePower = ExactIntegerPower(Value, BigInteger.Abs(exponent)); + PreciseNumber exact = exponent.Sign < 0 ? PreciseNumber.Divide(PreciseNumber.One, positivePower) : positivePower; + return exact.ToSignificantNumber(Value.SignificantDigits); + } + + // An integer power too large to compute exactly is still exact, so it keeps the base's + // precision, bounded by the double the result is computed in. + int significantDigits = PreciseNumber.IsInteger(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. @@ -612,12 +627,64 @@ public static SignificantNumber Exp(PreciseNumber power) return E; } - int significantDigits = LowestSignificantDigits(E, power); + // An integer power is exact, so the precision is bounded only by the double the result is + // computed in, not by the single significant digit an integer like 2 carries. + int significantDigits = PreciseNumber.IsInteger(power) + ? int.Min(E.SignificantDigits, DoubleSignificantDigits) + : LowestSignificantDigits(E, power); return Math.Exp(power.To()) .ToSignificantNumber(significantDigits); } + /// + /// The number of significant decimal digits a always represents exactly. + /// + private const int DoubleSignificantDigits = 15; + + /// + /// The largest integer power computes exactly. Larger powers fall back to the + /// logarithm path, so that an extreme exponent cannot build an arbitrarily large intermediate. + /// + private const int MaxExactIntegerPower = 1024; + + /// + /// Gets the value of an integer as a . + /// + /// A number that reports as an integer. + /// The integer value of . + private static BigInteger IntegerValue(PreciseNumber value) => + value.Exponent >= 0 + ? value.Significand * BigInteger.Pow(10, value.Exponent) + : value.Significand / BigInteger.Pow(10, -value.Exponent); + + /// + /// Raises a number to a non-negative integer power by exact repeated squaring. + /// + /// The base. + /// The non-negative power. + /// raised to , with no rounding. + private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger exponent) + { + PreciseNumber result = PreciseNumber.One; + PreciseNumber square = value; + while (!exponent.IsZero) + { + if (!exponent.IsEven) + { + result = PreciseNumber.Multiply(result, square); + } + + exponent >>= 1; + if (!exponent.IsZero) + { + square = PreciseNumber.Multiply(square, square); + } + } + + return result; + } + /// /// Compares the current instance with another at the lower of their significant digit counts. ///