What's wrong
FractionalPow (PreciseNumber/PreciseNumber.Exponentials.cs:683) ends with:
return Exp(product, significantDigits + consumed).ReduceSignificance(significantDigits);
Exp rounds half away from zero to significantDigits + consumed digits, which is often only one digit more than the caller asked for. ReduceSignificance then rounds half away from zero again. When the true value continues …49… just past the requested width, the first rounding turns the 4 into a 5 and the second rounding then carries it up. The last digit comes out one unit too high.
Every other function that finishes with ReduceSignificance(significantDigits) first computes at working = significantDigits + ExponentialGuardDigits. This path is the only one that skips the guard digits.
Repro (default 50 significant digits; reference values from Python decimal at 80 digits)
| Call |
True value (tail) |
Expected |
Observed |
Pow(Parse("19"), Parse("1.5")) |
…5794164538… |
…579416 |
…579417 |
Pow(Parse("30"), Parse("1.5")) |
…0849939394976… |
…849939 |
…84994 |
Pow(Parse("2957"), Parse("1.5")) |
|
…814757 |
…814758 |
A sweep of 400 random integer bases raised to 1.5 found two more misrounded results besides 19. About 3,000 cases for Exp, Log*, ExpM1, LogP1, the hyperbolic functions, Sin, Cos, Atan and Sqrt at 3–20 digits all matched the correctly rounded values, so the problem is specific to Pow.
This is not #121 (Divide's exact path) or #123 (Exp2's ln 2 widening).
Suggested fix / acceptance criteria
Round once, from a guarded width:
return Exp(product, working + consumed).ReduceSignificance(significantDigits);
With this change applied locally, all three repros return the expected digits and the existing 411 tests pass. Add pinning tests for Pow(19, 1.5) and Pow(30, 1.5).
What's wrong
FractionalPow(PreciseNumber/PreciseNumber.Exponentials.cs:683) ends with:Exprounds half away from zero tosignificantDigits + consumeddigits, which is often only one digit more than the caller asked for.ReduceSignificancethen rounds half away from zero again. When the true value continues…49…just past the requested width, the first rounding turns the 4 into a 5 and the second rounding then carries it up. The last digit comes out one unit too high.Every other function that finishes with
ReduceSignificance(significantDigits)first computes atworking = significantDigits + ExponentialGuardDigits. This path is the only one that skips the guard digits.Repro (default 50 significant digits; reference values from Python
decimalat 80 digits)Pow(Parse("19"), Parse("1.5"))Pow(Parse("30"), Parse("1.5"))Pow(Parse("2957"), Parse("1.5"))A sweep of 400 random integer bases raised to 1.5 found two more misrounded results besides 19. About 3,000 cases for
Exp,Log*,ExpM1,LogP1, the hyperbolic functions,Sin,Cos,AtanandSqrtat 3–20 digits all matched the correctly rounded values, so the problem is specific toPow.This is not #121 (Divide's exact path) or #123 (Exp2's ln 2 widening).
Suggested fix / acceptance criteria
Round once, from a guarded width:
With this change applied locally, all three repros return the expected digits and the existing 411 tests pass. Add pinning tests for
Pow(19, 1.5)andPow(30, 1.5).