Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions SignificantNumber.Test/SignificantNumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>());
Assert.AreEqual(4, squared.SignificantDigits);
Assert.AreEqual(baseNumber * baseNumber * baseNumber, cubed);
Assert.AreEqual(1.879, cubed.To<double>());
}

[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<double>());
}

[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<double>());
}

[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<double>(), 1e-12);
}

[TestMethod]
public void Pow_NegativeOneAndOddExponent_ReturnsNegativeOne()
{
Expand Down
71 changes: 69 additions & 2 deletions SignificantNumber/SignificantNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>()) <= 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.
Expand All @@ -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<double>())
.ToSignificantNumber(significantDigits);
}

/// <summary>
/// The number of significant decimal digits a <see cref="double"/> always represents exactly.
/// </summary>
private const int DoubleSignificantDigits = 15;

/// <summary>
/// The largest integer power <see cref="Pow"/> computes exactly. Larger powers fall back to the
/// logarithm path, so that an extreme exponent cannot build an arbitrarily large intermediate.
/// </summary>
private const int MaxExactIntegerPower = 1024;

/// <summary>
/// Gets the value of an integer <see cref="PreciseNumber"/> as a <see cref="BigInteger"/>.
/// </summary>
/// <param name="value">A number that <see cref="PreciseNumber.IsInteger"/> reports as an integer.</param>
/// <returns>The integer value of <paramref name="value"/>.</returns>
private static BigInteger IntegerValue(PreciseNumber value) =>
value.Exponent >= 0
? value.Significand * BigInteger.Pow(10, value.Exponent)
: value.Significand / BigInteger.Pow(10, -value.Exponent);

/// <summary>
/// Raises a number to a non-negative integer power by exact repeated squaring.
/// </summary>
/// <param name="value">The base.</param>
/// <param name="exponent">The non-negative power.</param>
/// <returns><paramref name="value"/> raised to <paramref name="exponent"/>, with no rounding.</returns>
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;
}

/// <summary>
/// Compares the current instance with another <see cref="SignificantNumber"/> at the lower of their significant digit counts.
/// </summary>
Expand Down
Loading