From 3fa2e57d20b86750172c7f77f23dc6701188173a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 22:26:07 +0000 Subject: [PATCH] Check Tan's precision ceiling against the requested digits [patch] Tan called the public SinCos at its widened working precision, so its ten guard digits were counted against the pi reduction ceiling and it refused precisions Sin and Cos deliver. SinCos is now a checked public wrapper over an unchecked SinCosCore; Tan checks the ceiling against the digits it reports and computes sin and cos through the core. Fixes #125 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VL7qukbkUpkVRArT45bBFb --- .../PreciseNumberTrigonometryTests.cs | 31 +++++++++++++++++++ PreciseNumber/PreciseNumber.Trigonometry.cs | 31 ++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs index ecc8df1..e2c0c6d 100644 --- a/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs +++ b/PreciseNumber.Test/PreciseNumberTrigonometryTests.cs @@ -151,6 +151,37 @@ public void TestSinDeliversEveryDigitUpToTheReductionCeiling() AssertAgreesTo(Parse(SinOneMillionDigits), sine, 120, "Sin at the reduction ceiling did not match its wide reference"); } + [TestMethod] + public void TestTanDeliversEveryDigitUpToTheReductionCeiling() + { + // Tan widens by its guard digits before computing sin and cos. Those are margin, so they must + // not be counted against the ceiling: Tan has to reach the same precision Sin and Cos do. + int ceiling = PreciseNumber.ConstantPrecision - 7; + PreciseNumber million = Parse("1000000"); + PreciseNumber tangent = PreciseNumber.Tan(million, ceiling); + (PreciseNumber sin, PreciseNumber cos) = PreciseNumber.SinCos(million, ceiling); + + Assert.AreEqual(ceiling, tangent.SignificantDigits); + AssertAgreesTo(PreciseNumber.Divide(sin, cos, ceiling), tangent, ceiling - 2, "Tan at the reduction ceiling was not sin/cos"); + + // One digit past the ceiling is still refused, as it is for Sin. + Assert.ThrowsExactly(() => PreciseNumber.Tan(million, ceiling + 1)); + } + + [TestMethod] + public void TestTanAcceptsThePrecisionsSinAndCosAccept() + { + // The issue's reproduction: each of these is within the ceiling for Sin and Cos, and Tan + // refused them because its guard digits were checked as if they were part of the answer. + PreciseNumber one = PreciseNumber.One; + PreciseNumber tangent = PreciseNumber.Tan(one, 145); + + Assert.AreEqual(145, tangent.SignificantDigits); + StringAssert.StartsWith(Digits(tangent), Tan1Digits, StringComparison.Ordinal, "Tan(1, 145) is wrong"); + Assert.AreEqual(145, PreciseNumber.Tan(Parse("0.3"), 145).SignificantDigits); + Assert.AreEqual(PreciseNumber.ConstantPrecision, PreciseNumber.Tan(Parse("0.3"), PreciseNumber.ConstantPrecision).SignificantDigits); + } + [TestMethod] public void TestSinRefusesAnArgumentTooLargeToReduceAtAll() { diff --git a/PreciseNumber/PreciseNumber.Trigonometry.cs b/PreciseNumber/PreciseNumber.Trigonometry.cs index 3c6e87a..17f2e03 100644 --- a/PreciseNumber/PreciseNumber.Trigonometry.cs +++ b/PreciseNumber/PreciseNumber.Trigonometry.cs @@ -183,7 +183,25 @@ public static (PreciseNumber Sin, PreciseNumber Cos) SinCos(PreciseNumber x) => public static (PreciseNumber Sin, PreciseNumber Cos) SinCos(PreciseNumber x, int significantDigits) { RequireSignificantDigits(significantDigits); + RequireReducibleArgument(significantDigits, IntegerDigitCount(x)); + return SinCosCore(x, significantDigits); + } + /// + /// Computes the sine and cosine of an angle in radians without checking the precision against + /// the reduction ceiling. + /// + /// The angle, in radians. + /// The number of significant digits to produce, at least one. + /// A tuple of the sine and cosine of . + /// + /// The body of , for a caller that has already checked + /// the digits it will report and wants these computed wider as guard margin. Margin is allowed to + /// be unavailable (see ), so checking the widened + /// precision would refuse requests whose every reported digit is correct. + /// + private static (PreciseNumber Sin, PreciseNumber Cos) SinCosCore(PreciseNumber x, int significantDigits) + { if (x.Significand.IsZero) { return (Zero, One); @@ -196,8 +214,6 @@ public static (PreciseNumber Sin, PreciseNumber Cos) SinCos(PreciseNumber x, int int argumentDigits = IntegerDigitCount(x); int reductionDigits = working + argumentDigits + TrigonometricGuardDigits; - RequireReducibleArgument(significantDigits, argumentDigits); - PreciseNumber piOverTwo = Divide(PiTo(reductionDigits), Two, reductionDigits); BigInteger quadrant = RoundToNearestInteger(Divide(x, piOverTwo, reductionDigits)); PreciseNumber remainder = Subtract(x, Multiply(new(0, quadrant), piOverTwo)) @@ -226,16 +242,23 @@ public static PreciseNumber Tan(PreciseNumber x) => /// The angle, in radians. /// The number of significant digits to produce. /// The tangent of . - /// Thrown when is less than one. + /// + /// Thrown when is less than one, or when it and the integer + /// digits of together exceed , exactly as for + /// . + /// /// Thrown when the cosine of is zero. /// /// sin / cos from one reduction and a single division, rather than two independent series. + /// The ceiling is checked against the requested digits only; the sine and cosine are then + /// computed wider as guard margin, which is not counted against it. /// public static PreciseNumber Tan(PreciseNumber x, int significantDigits) { RequireSignificantDigits(significantDigits); + RequireReducibleArgument(significantDigits, IntegerDigitCount(x)); int working = significantDigits + TrigonometricGuardDigits; - (PreciseNumber sin, PreciseNumber cos) = SinCos(x, working); + (PreciseNumber sin, PreciseNumber cos) = SinCosCore(x, working); return DivideApproximation(sin, cos, working, significantDigits); }