diff --git a/PreciseNumber.Test/PreciseNumberRootTests.cs b/PreciseNumber.Test/PreciseNumberRootTests.cs index 0c71cad..1b4c002 100644 --- a/PreciseNumber.Test/PreciseNumberRootTests.cs +++ b/PreciseNumber.Test/PreciseNumberRootTests.cs @@ -299,4 +299,12 @@ public void TestRootsOfLongSignificandsStayCorrect() Assert.AreEqual(200, root.SignificantDigits, "A 200 digit input produced a shorter root"); AssertAgreesTo(value, root.Squared(), 195, "The root of a 200 digit value does not square back"); } + + [TestMethod] + public void TestNegativeDegreeRootRoundsTheReciprocalOfAnUnroundedRoot() + { + // The square root of 1.0486 is 1.02401…, which rounds to 1.024 at four digits, and 1/1.024 is + // exactly 0.9765625. The true reciprocal is 0.976551…, so it must come from a wider root. + Assert.AreEqual(Parse("0.9766"), PreciseNumber.RootN(Parse("1.0486"), -2, 4), "RootN(1.0486, -2) is wrong"); + } } diff --git a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs index cdba4d8..ecc8df1 100644 --- a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs +++ b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs @@ -352,4 +352,26 @@ public void TestInverseHalfTurnFamilyReturnsHalfTurns() Assert.AreEqual(PreciseNumber.One, PreciseNumber.AcosPi(PreciseNumber.NegativeOne, 50)); AssertAgreesTo(Parse("0.25"), PreciseNumber.AtanPi(PreciseNumber.One, 50), 49, "AtanPi(1) is a quarter turn"); } + + [TestMethod] + public void TestExactQuotientsOfApproximatePiStillRoundToTheRequestedDigits() + { + // π at working precision halves, or divides by 180, without a remainder. That exact quotient + // must still be rounded to the digits asked for, not handed back with the guard digits on it. + Assert.AreEqual(Parse("1.5708"), PreciseNumber.Asin(PreciseNumber.One, 5), "Asin(1) is wrong"); + Assert.AreEqual(Parse("-1.5708"), PreciseNumber.Asin(-PreciseNumber.One, 5), "Asin(-1) is wrong"); + Assert.AreEqual(Parse("1.5708"), PreciseNumber.Atan2(PreciseNumber.One, PreciseNumber.Zero, 5), "Atan2(1, 0) is wrong"); + Assert.AreEqual(Parse("-1.5708"), PreciseNumber.Atan2(-PreciseNumber.One, PreciseNumber.Zero, 5), "Atan2(-1, 0) is wrong"); + Assert.AreEqual(Parse("3.1416"), PreciseNumber.DegreesToRadians(Parse("180"), 5), "DegreesToRadians(180) is wrong"); + Assert.AreEqual(Parse("1.5708"), PreciseNumber.DegreesToRadians(Parse("90"), 5), "DegreesToRadians(90) is wrong"); + } + + [TestMethod] + public void TestHalfTurnFunctionsRoundToTheRequestedDigits() + { + // asin(1/2) = π/6 and acos(1/2) = π/3, so in half turns they are 1/6 and 1/3. tan(π/8) = √2 − 1. + Assert.AreEqual(Parse("0.1666666667"), PreciseNumber.AsinPi(Parse("0.5"), 10), "AsinPi(0.5) is wrong"); + Assert.AreEqual(Parse("0.3333333333"), PreciseNumber.AcosPi(Parse("0.5"), 10), "AcosPi(0.5) is wrong"); + Assert.AreEqual(Parse("0.4142135624"), PreciseNumber.TanPi(Parse("0.125"), 10), "TanPi(0.125) is wrong"); + } } diff --git a/PreciseNumber/PreciseNumber.Roots.cs b/PreciseNumber/PreciseNumber.Roots.cs index aeb897c..465b923 100644 --- a/PreciseNumber/PreciseNumber.Roots.cs +++ b/PreciseNumber/PreciseNumber.Roots.cs @@ -158,7 +158,7 @@ public static PreciseNumber RootN(PreciseNumber x, int n, int significantDigits) { return x.Significand.IsZero ? throw new DivideByZeroException() - : Divide(One, RootN(x, -n, significantDigits), significantDigits); + : DivideApproximation(One, RootN(x, -n, significantDigits + RootGuardDigits), significantDigits + RootGuardDigits, significantDigits); } if (x.Significand.IsZero || n == 1) diff --git a/PreciseNumber/PreciseNumber.Trigonometry.cs b/PreciseNumber/PreciseNumber.Trigonometry.cs index db996e6..3c6e87a 100644 --- a/PreciseNumber/PreciseNumber.Trigonometry.cs +++ b/PreciseNumber/PreciseNumber.Trigonometry.cs @@ -236,7 +236,7 @@ public static PreciseNumber Tan(PreciseNumber x, int significantDigits) RequireSignificantDigits(significantDigits); int working = significantDigits + TrigonometricGuardDigits; (PreciseNumber sin, PreciseNumber cos) = SinCos(x, working); - return Divide(sin, cos, significantDigits); + return DivideApproximation(sin, cos, working, significantDigits); } /// @@ -281,7 +281,7 @@ public static PreciseNumber Asin(PreciseNumber x, int significantDigits) if (magnitude == One) { - PreciseNumber halfPi = Divide(PiTo(working), Two, significantDigits); + PreciseNumber halfPi = DivideApproximation(PiTo(working), Two, working, significantDigits); return x.Significand.Sign > 0 ? halfPi : -halfPi; } @@ -431,7 +431,7 @@ public static PreciseNumber Atan2(PreciseNumber y, PreciseNumber x, int signific return Zero; } - PreciseNumber halfPi = Divide(PiTo(working), Two, significantDigits); + PreciseNumber halfPi = DivideApproximation(PiTo(working), Two, working, significantDigits); return y.Significand.Sign > 0 ? halfPi : -halfPi; } @@ -562,7 +562,7 @@ public static PreciseNumber TanPi(PreciseNumber x, int significantDigits) RequireSignificantDigits(significantDigits); int working = significantDigits + TrigonometricGuardDigits; (PreciseNumber sin, PreciseNumber cos) = SinCosPi(x, working); - return Divide(sin, cos, significantDigits); + return DivideApproximation(sin, cos, working, significantDigits); } /// @@ -599,7 +599,7 @@ public static PreciseNumber AsinPi(PreciseNumber x, int significantDigits) } int working = significantDigits + TrigonometricGuardDigits; - return Divide(Asin(x, working), PiTo(working), significantDigits); + return DivideApproximation(Asin(x, working), PiTo(working), working, significantDigits); } /// @@ -636,7 +636,7 @@ public static PreciseNumber AcosPi(PreciseNumber x, int significantDigits) } int working = significantDigits + TrigonometricGuardDigits; - return Divide(Acos(x, working), PiTo(working), significantDigits); + return DivideApproximation(Acos(x, working), PiTo(working), working, significantDigits); } /// @@ -664,7 +664,7 @@ public static PreciseNumber AtanPi(PreciseNumber x, int significantDigits) } int working = significantDigits + TrigonometricGuardDigits; - return Divide(Atan(x, working), PiTo(working), significantDigits); + return DivideApproximation(Atan(x, working), PiTo(working), working, significantDigits); } /// @@ -692,7 +692,7 @@ public static PreciseNumber DegreesToRadians(PreciseNumber degrees, int signific { RequireSignificantDigits(significantDigits); int working = significantDigits + TrigonometricGuardDigits; - return Divide(Multiply(degrees, PiTo(working)), OneEighty, significantDigits); + return DivideApproximation(Multiply(degrees, PiTo(working)), OneEighty, working, significantDigits); } /// @@ -720,7 +720,7 @@ public static PreciseNumber RadiansToDegrees(PreciseNumber radians, int signific { RequireSignificantDigits(significantDigits); int working = significantDigits + TrigonometricGuardDigits; - return Divide(Multiply(radians, OneEighty), PiTo(working), significantDigits); + return DivideApproximation(Multiply(radians, OneEighty), PiTo(working), working, significantDigits); } /// @@ -853,6 +853,23 @@ private static (PreciseNumber Sin, PreciseNumber Cos) SinCosSeries(PreciseNumber $"The trigonometric series did not converge to {workingDigits.ToString(InvariantCulture)} significant digits."); } + /// + /// Divides a value that is already an approximation, then rounds to the digits the caller asked for. + /// + /// The dividend, carried at . + /// The divisor. + /// The significant digits the operands were computed to. + /// The number of significant digits to produce. + /// The quotient, rounded to . + /// + /// returns a terminating quotient exactly, + /// without rounding it. When an operand is a rounded approximation, such as π at working precision, + /// that exact quotient still carries the operand's guard digits, and they are wrong. Rounding + /// afterwards keeps the result to the digits requested whether or not the quotient terminates. + /// + private static PreciseNumber DivideApproximation(PreciseNumber numerator, PreciseNumber denominator, int workingDigits, int significantDigits) => + Divide(numerator, denominator, workingDigits).ReduceSignificance(significantDigits); + /// /// Sums the arc tangent series for a small argument. ///