Skip to content

Commit e56bafe

Browse files
committed
Clamp default trig precision to what the reduction can deliver [patch]
Sin(x), Cos(x), SinCos(x) and Tan(x) asked for every digit of a constant carrying ConstantPrecision digits, and an argument with an integer digit then needs pi one digit wider than it is stored, so Sin(Pi) threw. The default-precision overloads now cap at ConstantPrecision minus the argument's integer digits, with a floor of one. The explicit overloads keep refusing as before. Fixes #124 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL7qukbkUpkVRArT45bBFb
1 parent 09efd30 commit e56bafe

2 files changed

Lines changed: 67 additions & 11 deletions

File tree

‎PreciseNumber.Test/PreciseNumberTrigonometryTests.cs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,39 @@ public void TestSinRefusesAnArgumentTooLargeToReduceAtAll()
164164
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Tan(enormous, 10));
165165
}
166166

167+
[TestMethod]
168+
public void TestDefaultPrecisionOverloadsAcceptTheLibrarysOwnConstants()
169+
{
170+
// The constants carry ConstantPrecision digits, so the default used to ask for every one of
171+
// them and then refuse, because an argument with an integer digit needs π one digit wider.
172+
// The overloads that choose their own precision must choose one they can deliver.
173+
int ceiling = PreciseNumber.ConstantPrecision - 1;
174+
175+
PreciseNumber cosPi = PreciseNumber.Cos(PreciseNumber.Pi);
176+
AssertAgreesTo(PreciseNumber.NegativeOne, cosPi, ceiling, "Cos(Pi) was not -1");
177+
AssertAgreesTo(PreciseNumber.One, PreciseNumber.Cos(PreciseNumber.Tau), ceiling, "Cos(Tau) was not 1");
178+
179+
(PreciseNumber sin, PreciseNumber cos) = PreciseNumber.SinCos(PreciseNumber.Pi);
180+
Assert.AreEqual(PreciseNumber.Sin(PreciseNumber.Pi), sin, "SinCos(Pi).Sin disagreed with Sin(Pi)");
181+
Assert.AreEqual(cosPi, cos, "SinCos(Pi).Cos disagreed with Cos(Pi)");
182+
183+
// E is not a multiple of π/2, so the sine has digits of its own to compare against the
184+
// explicit overload at the same ceiling.
185+
Assert.AreEqual(PreciseNumber.Sin(PreciseNumber.E, ceiling), PreciseNumber.Sin(PreciseNumber.E));
186+
}
187+
188+
[TestMethod]
189+
public void TestDefaultPrecisionOverloadsStillRefuseAnArgumentTooLargeToReduce()
190+
{
191+
// Clamping the default must not hide the one case with no answer at any precision.
192+
PreciseNumber enormous = Parse("1E200");
193+
194+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Sin(enormous));
195+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Cos(enormous));
196+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.SinCos(enormous));
197+
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => PreciseNumber.Tan(enormous));
198+
}
199+
167200
[TestMethod]
168201
public void TestOrdinaryAnglesAreUnaffectedByTheReductionCeiling()
169202
{

‎PreciseNumber/PreciseNumber.Trigonometry.cs‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ public readonly partial record struct PreciseNumber
9595
/// <returns>The sine of <paramref name="x"/>.</returns>
9696
/// <remarks>
9797
/// Produced to the significant digits of <paramref name="x"/>, and never fewer than
98-
/// <see cref="MinimumDivisionPrecision"/>. Use <see cref="Sin(PreciseNumber, int)"/> to choose
99-
/// that precision.
98+
/// <see cref="MinimumDivisionPrecision"/>, but no more than the reduction can deliver at the
99+
/// magnitude of <paramref name="x"/>; see <see cref="DefaultReducedPrecision(PreciseNumber)"/>.
100+
/// Use <see cref="Sin(PreciseNumber, int)"/> to choose that precision.
100101
/// </remarks>
101102
public static PreciseNumber Sin(PreciseNumber x) =>
102-
Sin(x, DefaultTrigonometricPrecision(x));
103+
Sin(x, DefaultReducedPrecision(x));
103104

104105
/// <summary>
105106
/// Returns the sine of an angle in radians, to a chosen number of significant digits.
@@ -123,11 +124,12 @@ public static PreciseNumber Sin(PreciseNumber x, int significantDigits) =>
123124
/// <returns>The cosine of <paramref name="x"/>.</returns>
124125
/// <remarks>
125126
/// Produced to the significant digits of <paramref name="x"/>, and never fewer than
126-
/// <see cref="MinimumDivisionPrecision"/>. Use <see cref="Cos(PreciseNumber, int)"/> to choose
127-
/// that precision.
127+
/// <see cref="MinimumDivisionPrecision"/>, but no more than the reduction can deliver at the
128+
/// magnitude of <paramref name="x"/>; see <see cref="DefaultReducedPrecision(PreciseNumber)"/>.
129+
/// Use <see cref="Cos(PreciseNumber, int)"/> to choose that precision.
128130
/// </remarks>
129131
public static PreciseNumber Cos(PreciseNumber x) =>
130-
Cos(x, DefaultTrigonometricPrecision(x));
132+
Cos(x, DefaultReducedPrecision(x));
131133

132134
/// <summary>
133135
/// Returns the cosine of an angle in radians, to a chosen number of significant digits.
@@ -152,10 +154,10 @@ public static PreciseNumber Cos(PreciseNumber x, int significantDigits) =>
152154
/// <remarks>
153155
/// Both come from one argument reduction and one kernel, which is the reason to prefer this over a
154156
/// separate <see cref="Sin(PreciseNumber)"/> and <see cref="Cos(PreciseNumber)"/> when both are
155-
/// wanted.
157+
/// wanted. The precision follows <see cref="DefaultReducedPrecision(PreciseNumber)"/>.
156158
/// </remarks>
157159
public static (PreciseNumber Sin, PreciseNumber Cos) SinCos(PreciseNumber x) =>
158-
SinCos(x, DefaultTrigonometricPrecision(x));
160+
SinCos(x, DefaultReducedPrecision(x));
159161

160162
/// <summary>
161163
/// Returns the sine and cosine of an angle in radians, to a chosen number of significant digits.
@@ -214,11 +216,12 @@ public static (PreciseNumber Sin, PreciseNumber Cos) SinCos(PreciseNumber x, int
214216
/// <returns>The tangent of <paramref name="x"/>.</returns>
215217
/// <remarks>
216218
/// Produced to the significant digits of <paramref name="x"/>, and never fewer than
217-
/// <see cref="MinimumDivisionPrecision"/>. Use <see cref="Tan(PreciseNumber, int)"/> to choose
218-
/// that precision.
219+
/// <see cref="MinimumDivisionPrecision"/>, but no more than the reduction can deliver at the
220+
/// magnitude of <paramref name="x"/>; see <see cref="DefaultReducedPrecision(PreciseNumber)"/>.
221+
/// Use <see cref="Tan(PreciseNumber, int)"/> to choose that precision.
219222
/// </remarks>
220223
public static PreciseNumber Tan(PreciseNumber x) =>
221-
Tan(x, DefaultTrigonometricPrecision(x));
224+
Tan(x, DefaultReducedPrecision(x));
222225

223226
/// <summary>
224227
/// Returns the tangent of an angle in radians, to a chosen number of significant digits.
@@ -736,6 +739,26 @@ public static PreciseNumber RadiansToDegrees(PreciseNumber radians, int signific
736739
private static int DefaultTrigonometricPrecision(PreciseNumber value) =>
737740
Math.Max(value.SignificantDigits, MinimumDivisionPrecision);
738741

742+
/// <summary>
743+
/// Gets the significant digits the circular functions produce when the caller does not choose.
744+
/// </summary>
745+
/// <param name="value">The angle being reduced.</param>
746+
/// <returns>
747+
/// <see cref="DefaultTrigonometricPrecision(PreciseNumber)"/>, capped at what reducing
748+
/// <paramref name="value"/> against <see cref="ConstantPrecision"/> digits of π can deliver, and
749+
/// never less than one.
750+
/// </returns>
751+
/// <remarks>
752+
/// The built-in constants carry <see cref="ConstantPrecision"/> digits, so the uncapped default
753+
/// asks for exactly as many digits as π holds, and a constant with an integer digit then needs
754+
/// one more than that. <see cref="RequireReducibleArgument(int, int)"/> rightly refuses such a
755+
/// request when a caller makes it, but an overload that picks its own precision must not pick one
756+
/// it will refuse, or <c>Sin(Pi)</c> throws. Only an argument too large to reduce at any
757+
/// precision, where even one digit is out of reach, still throws.
758+
/// </remarks>
759+
private static int DefaultReducedPrecision(PreciseNumber value) =>
760+
Math.Max(1, Math.Min(DefaultTrigonometricPrecision(value), ConstantPrecision - IntegerDigitCount(value)));
761+
739762
/// <summary>
740763
/// Selects the sine and cosine of an angle from those of its reduced remainder and the quadrant
741764
/// the reduction removed.

0 commit comments

Comments
 (0)