Skip to content

fix: do the formatting length arithmetic in long - #77

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/fix-extreme-exponent-formatting
Sep 15, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/fix-extreme-exponent-formatting

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #75

Problem

ToString and TryFormat sized their buffers with int arithmetic over the exponent, so a value whose exponent sat near either end of the int range threw instead of formatting. Parse accepts any exponent an int holds, so a value could parse successfully and then fail to format.

Value ToString() before ToString() after TryFormat into 64 chars before after
1E-2147483648 OverflowException (from int.Abs) OverflowException naming the exponent ArgumentOutOfRangeException false
1E2147483647 ArgumentOutOfRangeException OverflowException naming the exponent ArgumentOutOfRangeException false

Changes

  • ToString sizes its buffer in long, and throws an OverflowException naming the exponent when the fixed point text could not exist as a string at all, rather than surfacing the failure as a negation or allocation error.
  • TryWriteDigits (behind TryFormat) does every length in long, so the comparison against the destination's length is what answers. It now returns false for any destination too small, whatever the exponent, instead of wrapping a length negative and reaching Span.Slice.
  • CountDecimalDigits returns long. An exponent of int.MinValue implies 2,147,483,648 decimal digits, one more than an int can count, and int.Abs(int.MinValue) throws. LowestDecimalDigits follows it.
  • Round follows the widened count, and stops raising ten to a power wider than the significand: dropping one digit more than the significand holds always leaves zero, so a value far below the requested place now rounds to zero instead of throwing.

This is the same treatment #72 gave the conversion paths, applied to formatting.

Deliberately not changed

Formatting stays fixed point, so 1E-1000000 still renders as 1,000,002 characters. Switching to scientific notation past some exponent, or supporting the "E" format string, is a separate API decision — "G" is the only format accepted today and changing what it means would change existing output. Left to #75 to decide.

Testing

Six tests added in PreciseNumberTests.cs, covering int.MinValue and int.MaxValue exponents across ToString, TryFormat, CountDecimalDigits and Round, plus the parse-then-format path from the issue's repro and a regression guard at large-but-representable exponents (1E±1000).

  • Five of the six fail against main and pass with the fix — verified by reverting PreciseNumber.cs alone and re-running. The sixth is the regression guard, which passes either way.
  • Full suite: 266 passed, 0 failed.
  • Solution builds clean across net7.0/8.0/9.0/10.0 with no new warnings.
  • TextBenchmarks run before and after (--job short, Digits 8/30/200): means unchanged within noise, allocations byte for byte identical.

No public API signature changes; the behaviour changes are all in cases that previously threw.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XbHTU3pZh6egvtowTyuqNM


Generated by Claude Code

ToString and TryFormat sized their buffers with int arithmetic over the
exponent, so a value whose exponent sat near either end of the int range
threw rather than formatted: int.Abs(int.MinValue) overflows, and the sums
near int.MaxValue wrapped negative and reached ArrayPool.Rent and
Span.Slice as invalid lengths. Parse accepts any exponent an int holds, so
a value could parse and then fail to format.

Widen the lengths to long, the way #72 did for the conversion paths.
TryFormat now answers false for a destination that cannot hold the
rendering, whatever the exponent, and ToString throws an OverflowException
naming the exponent when the text could not exist as a string at all.

CountDecimalDigits returns long for the same reason: an exponent of
int.MinValue implies one more decimal digit than an int can count. Round
follows it, and stops raising ten to a power wider than the significand,
since dropping one digit more than it holds always leaves zero.

Formatting stays fixed point. Scientific notation for extreme exponents is
a separate API decision and is left to the issue.

Text benchmarks are unchanged within noise across all three digit counts,
with allocations byte for byte identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XbHTU3pZh6egvtowTyuqNM
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 6a22be0 into main Sep 15, 2026
13 checks passed
@matt-edmondson
matt-edmondson deleted the claude/fix-extreme-exponent-formatting branch September 15, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formatting a value with an extreme exponent throws or produces a huge string

2 participants