Render normalized scientific notation when converting to double, float,
Half, and decimal. The .NET 7 and 8 parsers clamp an exponent above 1000
and still offset it by every digit before the point, so a value with more
than 1000 fraction digits converted to zero on those runtimes. Every digit
is still rendered, so rounding stays correct.
Format double and float input with the shortest round-trippable text
instead of 16 and 8 significant digits. double.MaxValue and MinValue no
longer convert back to infinity, and 0.3048 stays exactly 0.3048.
Handle an exponent of int.MinValue in every conversion. The fast paths
called int.Abs, which throws, and truncation negated the exponent, which
wraps. Such values now convert to zero.
Return false from TryParse for an exponent outside the range of int, and
throw OverflowException from Parse for it consistently, including when the
decimal point or stripped trailing zeros push the exponent out of range.
Round on the exact dropped digits in Round and ReduceSignificance. Adding
a run of fives rounded 123.456 to 124 at three significant digits and
1.2345 to 1.24 at two decimal places. This predates 2.0. Divide already
rounded exactly and is unchanged.
Narrow the allocation claim in the README, CLAUDE.md, and the migration
guide to operations whose operands and intermediate and final significands
fit in an int, and document the conversion and TryParse changes.
Claude-Session: https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD
Summary
ktsu.PreciseNumber 2.0.0 and 2.0.1 shipped with these bugs, and this PR fixes them:
PreciseNumberwith more than 1000 fraction digits todouble,float,Half, ordecimalreturned 0.PreciseNumber.Parse("1." + new string('0', 1000) + "1")converted to 0 instead of 1.double.MaxValueanddouble.MinValueconverted toPreciseNumberand back came out as positive and negative infinity.int.MinValuethrewOverflowException(todoubleandfloat) orArgumentOutOfRangeException(to integer types) instead of returning 0.TryParse("1e99999999999", null, out _)threwOverflowExceptioninstead of returningfalse.RoundandReduceSignificancerounded wrong whenever they dropped two or more digits.123.456.ToPreciseNumber().ReduceSignificance(3)gave 124, and1.2345rounded to two decimal places gave 1.24. This one predates 2.0 and turned up during the SignificantNumber migration.The docs also claimed that arithmetic on values with small significands allocates nothing. That holds only when the operands and every intermediate and final significand fit in an
int, so the README, CLAUDE.md, and the migration guide now say that.Behavior changes to note
ToPreciseNumber()andCreateCheckedfrom adoubleorfloatkeep the shortest digits that round-trip instead of 16 or 8 significant digits.(0.1 + 0.2).ToPreciseNumber()is now 0.30000000000000004, and a result that routes throughdouble, such asExp(-1), carries a 17th digit when thedoubleneeds one.ParsethrowsOverflowExceptionfor an exponent outside the range ofint. That includes1.5E-2147483648and10E2147483647, which used to wrap silently to a wrong value.RoundandReduceSignificancereturn different results for inputs that drop two or more digits below half.How
decimalrender normalized scientific notation (d.ddd…E±n), so any value within range of the destination has a small exponent. Every digit is still rendered. Truncating to the destination's precision was considered and rejected, because correct rounding can hinge on a nonzero digit far past the cut."R".int.Abs(Exponent), and truncation widens the exponent tolongbefore negating it.TryParsecatchesOverflowExceptionas well asFormatException.Parseuses checked arithmetic when it adjusts the exponent for the decimal point, and so does the constructor when it strips trailing zeros.RoundandReduceSignificancedivide by the power of ten and compare the exact remainder with half the divisor, rounding half away from zero.Dividealready rounded that way and wasn't affected.Risk and testing
The rounding fix and the 17-digit conversions change results that callers may have pinned. Three existing tests encoded the old behavior, and each now expects the exact value:
TestStaticRoundexpected 1.2345 to round to 1.24 and now expects 1.23.TestReduceSignificanceexpected 12345 at three digits to have significand 124 and now expects 123.TestExpWithNegativePowerexpected 1/e rebuilt at the result's precision and now expects 0.36787944117144233, the exact round trip ofMath.Exp(-1).New tests cover the following:
MaxValue,MinValue, andEpsilonfordouble,float, andHalf.int.MinValueexponent converted to floating point,decimal, integer, andBigIntegerdestinations.ParseandTryParse.All 260 tests pass on net10.0, and the library builds for net7.0, net8.0, net9.0, and net10.0 with no warnings. The long fraction bug only reproduces in the .NET 7 and 8 parsers, which the net10.0 test project can't reach. A file-based app built for net8.0 on runtime 8.0.31 returned 0 for
1.(1000 zeros)1before the fix and 1 after, for all four destination types.Known limitation
Formatting a value whose exponent is
int.MinValuecan still throw, becauseCountDecimalDigitsandTryFormatnegate the exponent. That's outside the conversion paths this PR fixes and is left for a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD