Skip to content

Commit 21582d1

Browse files
Merge pull request #104 from ktsu-dev/fix/103-integer-pow-precision
2 parents 37bd2a8 + 254b0fe commit 21582d1

2 files changed

Lines changed: 119 additions & 2 deletions

File tree

‎SignificantNumber.Test/SignificantNumberTests.cs‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,56 @@ public void Pow_NegativeBaseAndOddExponent_ReturnsNegativeResult()
716716
Assert.AreEqual(SignificantNumber.CreateFromComponents(0, new BigInteger(-8)), result);
717717
}
718718

719+
[TestMethod]
720+
public void Pow_IntegerExponent_MatchesRepeatedMultiplication()
721+
{
722+
SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-3, new BigInteger(1234));
723+
SignificantNumber two = SignificantNumber.CreateFromComponents(0, new BigInteger(2));
724+
SignificantNumber three = SignificantNumber.CreateFromComponents(0, new BigInteger(3));
725+
726+
SignificantNumber squared = baseNumber.Pow(two);
727+
SignificantNumber cubed = baseNumber.Pow(three);
728+
729+
// 1.234^2 = 1.522756 and 1.234^3 = 1.879080904, each kept to the base's 4 significant digits
730+
Assert.AreEqual(baseNumber * baseNumber, squared);
731+
Assert.AreEqual(1.523, squared.To<double>());
732+
Assert.AreEqual(4, squared.SignificantDigits);
733+
Assert.AreEqual(baseNumber * baseNumber * baseNumber, cubed);
734+
Assert.AreEqual(1.879, cubed.To<double>());
735+
}
736+
737+
[TestMethod]
738+
public void Pow_NegativeBaseAndOddExponent_KeepsBasePrecision()
739+
{
740+
SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-1, new BigInteger(-25));
741+
SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(3));
742+
SignificantNumber result = baseNumber.Pow(power);
743+
744+
// (-2.5)^3 = -15.625, kept to the base's 2 significant digits
745+
Assert.AreEqual(-16.0, result.To<double>());
746+
}
747+
748+
[TestMethod]
749+
public void Pow_NegativeIntegerExponent_MatchesReciprocalOfPower()
750+
{
751+
SignificantNumber baseNumber = SignificantNumber.CreateFromComponents(-3, new BigInteger(1234));
752+
SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(-2));
753+
SignificantNumber result = baseNumber.Pow(power);
754+
755+
// 1.234^-2 = 0.65670..., kept to the base's 4 significant digits
756+
Assert.AreEqual(0.6567, result.To<double>());
757+
}
758+
759+
[TestMethod]
760+
public void Exp_IntegerExponent_KeepsMoreThanOneSignificantDigit()
761+
{
762+
SignificantNumber power = SignificantNumber.CreateFromComponents(0, new BigInteger(2));
763+
SignificantNumber result = SignificantNumber.Exp(power);
764+
765+
Assert.IsGreaterThan(1, result.SignificantDigits);
766+
Assert.AreEqual(Math.Exp(2), result.To<double>(), 1e-12);
767+
}
768+
719769
[TestMethod]
720770
public void Pow_NegativeOneAndOddExponent_ReturnsNegativeOne()
721771
{

‎SignificantNumber/SignificantNumber.cs‎

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,22 @@ public SignificantNumber Pow(PreciseNumber power)
586586
throw new ArgumentException("Cannot raise a negative number to a non-integer power.", nameof(power));
587587
}
588588

589-
int significantDigits = LowestSignificantDigits(this, power);
589+
if (PreciseNumber.IsInteger(power) && Math.Abs(power.To<double>()) <= MaxExactIntegerPower)
590+
{
591+
// An integer power is an exact count, like the counting numbers in a multiplication, so the
592+
// result keeps the base's significant digits. Computing it by exact repeated multiplication
593+
// makes x.Pow(2) agree with x * x and keeps the sign of a negative base.
594+
BigInteger exponent = IntegerValue(power);
595+
PreciseNumber positivePower = ExactIntegerPower(Value, BigInteger.Abs(exponent));
596+
PreciseNumber exact = exponent.Sign < 0 ? PreciseNumber.Divide(PreciseNumber.One, positivePower) : positivePower;
597+
return exact.ToSignificantNumber(Value.SignificantDigits);
598+
}
599+
600+
// An integer power too large to compute exactly is still exact, so it keeps the base's
601+
// precision, bounded by the double the result is computed in.
602+
int significantDigits = PreciseNumber.IsInteger(power)
603+
? int.Min(Value.SignificantDigits, DoubleSignificantDigits)
604+
: LowestSignificantDigits(this, power);
590605

591606
// Use logarithm and exponential to support decimal powers. This computes |x|^p, so the
592607
// sign of a negative base is restored for odd integer powers.
@@ -612,12 +627,64 @@ public static SignificantNumber Exp(PreciseNumber power)
612627
return E;
613628
}
614629

615-
int significantDigits = LowestSignificantDigits(E, power);
630+
// An integer power is exact, so the precision is bounded only by the double the result is
631+
// computed in, not by the single significant digit an integer like 2 carries.
632+
int significantDigits = PreciseNumber.IsInteger(power)
633+
? int.Min(E.SignificantDigits, DoubleSignificantDigits)
634+
: LowestSignificantDigits(E, power);
616635

617636
return Math.Exp(power.To<double>())
618637
.ToSignificantNumber(significantDigits);
619638
}
620639

640+
/// <summary>
641+
/// The number of significant decimal digits a <see cref="double"/> always represents exactly.
642+
/// </summary>
643+
private const int DoubleSignificantDigits = 15;
644+
645+
/// <summary>
646+
/// The largest integer power <see cref="Pow"/> computes exactly. Larger powers fall back to the
647+
/// logarithm path, so that an extreme exponent cannot build an arbitrarily large intermediate.
648+
/// </summary>
649+
private const int MaxExactIntegerPower = 1024;
650+
651+
/// <summary>
652+
/// Gets the value of an integer <see cref="PreciseNumber"/> as a <see cref="BigInteger"/>.
653+
/// </summary>
654+
/// <param name="value">A number that <see cref="PreciseNumber.IsInteger"/> reports as an integer.</param>
655+
/// <returns>The integer value of <paramref name="value"/>.</returns>
656+
private static BigInteger IntegerValue(PreciseNumber value) =>
657+
value.Exponent >= 0
658+
? value.Significand * BigInteger.Pow(10, value.Exponent)
659+
: value.Significand / BigInteger.Pow(10, -value.Exponent);
660+
661+
/// <summary>
662+
/// Raises a number to a non-negative integer power by exact repeated squaring.
663+
/// </summary>
664+
/// <param name="value">The base.</param>
665+
/// <param name="exponent">The non-negative power.</param>
666+
/// <returns><paramref name="value"/> raised to <paramref name="exponent"/>, with no rounding.</returns>
667+
private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger exponent)
668+
{
669+
PreciseNumber result = PreciseNumber.One;
670+
PreciseNumber square = value;
671+
while (!exponent.IsZero)
672+
{
673+
if (!exponent.IsEven)
674+
{
675+
result = PreciseNumber.Multiply(result, square);
676+
}
677+
678+
exponent >>= 1;
679+
if (!exponent.IsZero)
680+
{
681+
square = PreciseNumber.Multiply(square, square);
682+
}
683+
}
684+
685+
return result;
686+
}
687+
621688
/// <summary>
622689
/// Compares the current instance with another <see cref="SignificantNumber"/> at the lower of their significant digit counts.
623690
/// </summary>

0 commit comments

Comments
 (0)