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); }