Skip to content

Log, Log2, Log10, Exp, Exp10, Pow, the hyperbolics, Acos and SinPi return about 50 wrong trailing digits when asked for more than ~140 digits, while Sin and Cos refuse the same request #143

Description

@matt-edmondson

What's wrong

PiTo, Ln2To and Ln10To return at most ConstantPrecision (150) digits. That cap is deliberate and is pinned by TestConstantAccessorsReturnTheWholeConstantWhenAskedForMore. #96 reported that callers assume they get the full width they asked for.

PR #97 (6f79d8c) fixed #96 for SinCos only, by adding RequireReducibleArgument. Its commit message ruled out the rest of the API: "Log is unaffected … correct to 130 digits at 180 total working digits. Exp tops out around ConstantPrecision - 1 … so it is documented rather than guarded."

That measurement never asked for more than 150 result digits. When significantDigits is above about 140, the capped constant limits the answer to roughly 148 correct digits, yet the function still reports every digit that was requested. Neither the XML docs nor the README mention the limit.

Affected call sites

These read a constant wider than it can be served, with no guard:

  • PreciseNumber/PreciseNumber.Exponentials.cs
    • :163: Log, via Ln10To(working + exponentDigits), whenever the decimal exponent is non-zero
    • :227, :273, :357, :389: Log2, Log10, Log2P1, Log10P1
    • :442, :448: Exp, and through it Pow (fractional), Sinh, Cosh and ExpM1 for |x| > 0.5
    • :532, :563, :608, :641: Exp2, Exp2M1, Exp10, Exp10M1
  • PreciseNumber/PreciseNumber.Trigonometry.cs
    • :337: Acos
    • :539: SinPi, CosPi, TanPi
    • :445: Atan2 with x < 0
    • :602, :639, :667, :695, :723: the …Pi inverses and the degree/radian conversions

Reproduction

Run against da99f93 and checked against Python decimal at 260 digits:

Call Correct digits in the result
Log(12345, 200) 148 (ends …988588036379…, correct is …988587987647…)
Exp(5, 200) about 147
Log2(3, 200) 148
Exp10(0.5, 200) 148
Sinh(3, 200) 148
Acos(0.5, 200) 148
SinPi(0.25, 200) 150

Controls:

  • Log(2, 200) reads no constant because its exponent is zero, and it is correct to all 200 digits.
  • Log(12345, 145) is correct.
  • Sin(0.5, 200) throws ArgumentOutOfRangeException naming the 150-digit ceiling.

So one function clearly refuses a request that its neighbours answer with silently wrong digits.

The one-argument overloads are affected too. They default to the argument's own SignificantDigits, so Log(x) or Exp(x) on any value wider than about 140 digits hits this, for example a parsed 200-digit literal.

Suggested fix

Apply one rule to every function that reads a constant. Either:

  • (a) Refuse. Throw when significantDigits plus the digits the argument consumes exceeds ConstantPrecision, as RequireReducibleArgument does.
  • (b) Report only correct digits. Round the result to min(significantDigits, ConstantPrecision - consumedDigits - 1).

(b) matches how the constant accessors already behave, and it would also settle #124 for the default-precision overloads. Whichever is chosen, update the Log/SinPi paragraph in CLAUDE.md, which currently says those functions have no ceiling.

Acceptance criteria

  • Each of Log(12345, 200), Exp(5, 200), Log2(3, 200), Exp10(0.5, 200), Sinh(3, 200), Acos(0.5, 200) and SinPi(0.25, 200) either throws a documented exception or returns a result whose reported digits all match an independent reference.
  • Log(2, 200), which reads no constant, stays exact to 200 digits.
  • The chosen ceiling is documented on the affected methods.

This follows up #96 / #97.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions