Skip to content

Commit 310faf9

Browse files
committed
Round exact quotients of approximations to the requested digits [patch]
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 #121 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jNTPLxThrQnZyafw1CZ1p
1 parent e664fcb commit 310faf9

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

‎PreciseNumber.Test/PreciseNumberRootTests.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,12 @@ public void TestRootsOfLongSignificandsStayCorrect()
299299
Assert.AreEqual(200, root.SignificantDigits, "A 200 digit input produced a shorter root");
300300
AssertAgreesTo(value, root.Squared(), 195, "The root of a 200 digit value does not square back");
301301
}
302+
303+
[TestMethod]
304+
public void TestNegativeDegreeRootRoundsTheReciprocalOfAnUnroundedRoot()
305+
{
306+
// The square root of 1.0486 is 1.02401…, which rounds to 1.024 at four digits, and 1/1.024 is
307+
// exactly 0.9765625. The true reciprocal is 0.976551…, so it must come from a wider root.
308+
Assert.AreEqual(Parse("0.9766"), PreciseNumber.RootN(Parse("1.0486"), -2, 4), "RootN(1.0486, -2) is wrong");
309+
}
302310
}

‎PreciseNumber.Test/PreciseNumberTrigonometryTests.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,4 +352,17 @@ public void TestInverseHalfTurnFamilyReturnsHalfTurns()
352352
Assert.AreEqual(PreciseNumber.One, PreciseNumber.AcosPi(PreciseNumber.NegativeOne, 50));
353353
AssertAgreesTo(Parse("0.25"), PreciseNumber.AtanPi(PreciseNumber.One, 50), 49, "AtanPi(1) is a quarter turn");
354354
}
355+
356+
[TestMethod]
357+
public void TestExactQuotientsOfApproximatePiStillRoundToTheRequestedDigits()
358+
{
359+
// π at working precision halves, or divides by 180, without a remainder. That exact quotient
360+
// must still be rounded to the digits asked for, not handed back with the guard digits on it.
361+
Assert.AreEqual(Parse("1.5708"), PreciseNumber.Asin(PreciseNumber.One, 5), "Asin(1) is wrong");
362+
Assert.AreEqual(Parse("-1.5708"), PreciseNumber.Asin(-PreciseNumber.One, 5), "Asin(-1) is wrong");
363+
Assert.AreEqual(Parse("1.5708"), PreciseNumber.Atan2(PreciseNumber.One, PreciseNumber.Zero, 5), "Atan2(1, 0) is wrong");
364+
Assert.AreEqual(Parse("-1.5708"), PreciseNumber.Atan2(-PreciseNumber.One, PreciseNumber.Zero, 5), "Atan2(-1, 0) is wrong");
365+
Assert.AreEqual(Parse("3.1416"), PreciseNumber.DegreesToRadians(Parse("180"), 5), "DegreesToRadians(180) is wrong");
366+
Assert.AreEqual(Parse("1.5708"), PreciseNumber.DegreesToRadians(Parse("90"), 5), "DegreesToRadians(90) is wrong");
367+
}
355368
}

‎PreciseNumber/PreciseNumber.Roots.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public static PreciseNumber RootN(PreciseNumber x, int n, int significantDigits)
158158
{
159159
return x.Significand.IsZero
160160
? throw new DivideByZeroException()
161-
: Divide(One, RootN(x, -n, significantDigits), significantDigits);
161+
: DivideApproximation(One, RootN(x, -n, significantDigits + RootGuardDigits), significantDigits + RootGuardDigits, significantDigits);
162162
}
163163

164164
if (x.Significand.IsZero || n == 1)

‎PreciseNumber/PreciseNumber.Trigonometry.cs‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public static PreciseNumber Tan(PreciseNumber x, int significantDigits)
236236
RequireSignificantDigits(significantDigits);
237237
int working = significantDigits + TrigonometricGuardDigits;
238238
(PreciseNumber sin, PreciseNumber cos) = SinCos(x, working);
239-
return Divide(sin, cos, significantDigits);
239+
return DivideApproximation(sin, cos, working, significantDigits);
240240
}
241241

242242
/// <summary>
@@ -281,7 +281,7 @@ public static PreciseNumber Asin(PreciseNumber x, int significantDigits)
281281

282282
if (magnitude == One)
283283
{
284-
PreciseNumber halfPi = Divide(PiTo(working), Two, significantDigits);
284+
PreciseNumber halfPi = DivideApproximation(PiTo(working), Two, working, significantDigits);
285285
return x.Significand.Sign > 0 ? halfPi : -halfPi;
286286
}
287287

@@ -431,7 +431,7 @@ public static PreciseNumber Atan2(PreciseNumber y, PreciseNumber x, int signific
431431
return Zero;
432432
}
433433

434-
PreciseNumber halfPi = Divide(PiTo(working), Two, significantDigits);
434+
PreciseNumber halfPi = DivideApproximation(PiTo(working), Two, working, significantDigits);
435435
return y.Significand.Sign > 0 ? halfPi : -halfPi;
436436
}
437437

@@ -562,7 +562,7 @@ public static PreciseNumber TanPi(PreciseNumber x, int significantDigits)
562562
RequireSignificantDigits(significantDigits);
563563
int working = significantDigits + TrigonometricGuardDigits;
564564
(PreciseNumber sin, PreciseNumber cos) = SinCosPi(x, working);
565-
return Divide(sin, cos, significantDigits);
565+
return DivideApproximation(sin, cos, working, significantDigits);
566566
}
567567

568568
/// <summary>
@@ -599,7 +599,7 @@ public static PreciseNumber AsinPi(PreciseNumber x, int significantDigits)
599599
}
600600

601601
int working = significantDigits + TrigonometricGuardDigits;
602-
return Divide(Asin(x, working), PiTo(working), significantDigits);
602+
return DivideApproximation(Asin(x, working), PiTo(working), working, significantDigits);
603603
}
604604

605605
/// <summary>
@@ -636,7 +636,7 @@ public static PreciseNumber AcosPi(PreciseNumber x, int significantDigits)
636636
}
637637

638638
int working = significantDigits + TrigonometricGuardDigits;
639-
return Divide(Acos(x, working), PiTo(working), significantDigits);
639+
return DivideApproximation(Acos(x, working), PiTo(working), working, significantDigits);
640640
}
641641

642642
/// <summary>
@@ -664,7 +664,7 @@ public static PreciseNumber AtanPi(PreciseNumber x, int significantDigits)
664664
}
665665

666666
int working = significantDigits + TrigonometricGuardDigits;
667-
return Divide(Atan(x, working), PiTo(working), significantDigits);
667+
return DivideApproximation(Atan(x, working), PiTo(working), working, significantDigits);
668668
}
669669

670670
/// <summary>
@@ -692,7 +692,7 @@ public static PreciseNumber DegreesToRadians(PreciseNumber degrees, int signific
692692
{
693693
RequireSignificantDigits(significantDigits);
694694
int working = significantDigits + TrigonometricGuardDigits;
695-
return Divide(Multiply(degrees, PiTo(working)), OneEighty, significantDigits);
695+
return DivideApproximation(Multiply(degrees, PiTo(working)), OneEighty, working, significantDigits);
696696
}
697697

698698
/// <summary>
@@ -720,7 +720,7 @@ public static PreciseNumber RadiansToDegrees(PreciseNumber radians, int signific
720720
{
721721
RequireSignificantDigits(significantDigits);
722722
int working = significantDigits + TrigonometricGuardDigits;
723-
return Divide(Multiply(radians, OneEighty), PiTo(working), significantDigits);
723+
return DivideApproximation(Multiply(radians, OneEighty), PiTo(working), working, significantDigits);
724724
}
725725

726726
/// <summary>
@@ -853,6 +853,23 @@ private static (PreciseNumber Sin, PreciseNumber Cos) SinCosSeries(PreciseNumber
853853
$"The trigonometric series did not converge to {workingDigits.ToString(InvariantCulture)} significant digits.");
854854
}
855855

856+
/// <summary>
857+
/// Divides a value that is already an approximation, then rounds to the digits the caller asked for.
858+
/// </summary>
859+
/// <param name="numerator">The dividend, carried at <paramref name="workingDigits"/>.</param>
860+
/// <param name="denominator">The divisor.</param>
861+
/// <param name="workingDigits">The significant digits the operands were computed to.</param>
862+
/// <param name="significantDigits">The number of significant digits to produce.</param>
863+
/// <returns>The quotient, rounded to <paramref name="significantDigits"/>.</returns>
864+
/// <remarks>
865+
/// <see cref="Divide(PreciseNumber, PreciseNumber, int)"/> returns a terminating quotient exactly,
866+
/// without rounding it. When an operand is a rounded approximation, such as π at working precision,
867+
/// that exact quotient still carries the operand's guard digits, and they are wrong. Rounding
868+
/// afterwards keeps the result to the digits requested whether or not the quotient terminates.
869+
/// </remarks>
870+
private static PreciseNumber DivideApproximation(PreciseNumber numerator, PreciseNumber denominator, int workingDigits, int significantDigits) =>
871+
Divide(numerator, denominator, workingDigits).ReduceSignificance(significantDigits);
872+
856873
/// <summary>
857874
/// Sums the arc tangent series for a small argument.
858875
/// </summary>

0 commit comments

Comments
 (0)