From 310faf9549cce2443ee0d50b2745545dbbade766 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 23:27:15 +0000 Subject: [PATCH 1/2] Round exact quotients of approximations to the requested digits [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Divide returns a terminating quotient exactly. Asin(±1), Atan2(y, 0), DegreesToRadians and RootN(x, -n) divided a value already rounded to a wider working precision, so when the quotient terminated they returned the guard digits too, and those digits were wrong: Asin(1, 5) gave 1.570796326794895 and RootN(1.0486, -2, 4) gave 0.9765625. Add DivideApproximation, which divides at working precision and then rounds to significantDigits, and use it on those paths and on the other approx / approx paths (Tan, TanPi, AsinPi, AcosPi, AtanPi, RadiansToDegrees). RootN(x, -n) now takes the inner root with guard digits. Fixes ktsu-dev/PreciseNumber#121 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jNTPLxThrQnZyafw1CZ1p --- PreciseNumber.Test/PreciseNumberRootTests.cs | 8 +++++ .../PreciseNumberTrigonometryTests.cs | 13 +++++++ PreciseNumber/PreciseNumber.Roots.cs | 2 +- PreciseNumber/PreciseNumber.Trigonometry.cs | 35 ++++++++++++++----- 4 files changed, 48 insertions(+), 10 deletions(-) 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..f2afa57 100644 --- a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs +++ b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs @@ -352,4 +352,17 @@ 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"); + } } 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. /// From 1f54a487384879b2b7609b4e21cfa67e76caedf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 23:39:02 +0000 Subject: [PATCH 2/2] Cover AsinPi, AcosPi and TanPi on their rounding paths The general paths of AsinPi and AcosPi and all of TanPi had no test, which left the new DivideApproximation call sites there uncovered. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jNTPLxThrQnZyafw1CZ1p --- PreciseNumber.Test/PreciseNumberTrigonometryTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs index f2afa57..ecc8df1 100644 --- a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs +++ b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs @@ -365,4 +365,13 @@ public void TestExactQuotientsOfApproximatePiStillRoundToTheRequestedDigits() 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"); + } }