Skip to content

Round to negative decimal places correctly for integers - #108

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/round-negative-digits
Sep 27, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/round-negative-digits

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #105

What was wrong

Round(int decimalDigits) worked out how many digits to drop from CountDecimalDigits(), then moved the exponent by CopySign(dropped, Exponent). That is only correct when the stored exponent is negative. Integers keep their trailing zeros in a positive exponent: 1234 is stored at exponent 0, and 1230 as 123 at exponent 1. For those, a negative decimalDigits dropped the wrong number of digits and also moved the exponent the wrong way:

Call Before After
1234.5.Round(-2) 1200 1200
1234.Round(-2) 0.12 1200
1230.Round(-2) 0.1 1200
1500.Round(-3) 0 2000

Decision: support negative digits, don't throw

The triage note left the choice open. I went with support because 1234.5.Round(-2) already returns 1200, so negative digits already work for any value with a fractional part. Throwing would break that existing behaviour. This change makes the result independent of how the value happens to be stored. If you'd rather reject negative digits the way Math.Round does, that is a small follow-up.

Change

This is the rewrite suggested in the issue. It computes targetExponent = -decimalDigits, drops targetExponent - Exponent digits when the value has any below that place (still capped at SignificantDigits + 1), and sets the new exponent to checked(Exponent + dropped). For negative exponents this is the same arithmetic as before, and every existing rounding test passes unchanged. The static Round(PreciseNumber, int) forwards to this method, so it is fixed too.

Tests

TestRoundToNegativeDecimalsDoesNotDependOnHowTheValueIsStored checks the instance and static overloads on:

  • the four inputs from the issue
  • half away from zero on both signs (1250, ±1500)
  • rounding down (1499) and rounding to zero (400)
  • a value already at the requested place
  • non-negative digits on integers, which must be no-ops

Verification

🤖 Generated with Claude Code

https://claude.ai/code/session_018AEYBNCRr6chFLiZDv1MVj


Generated by Claude Code

Round derived the digits to drop from the decimal-place count and moved
the exponent by CopySign(dropped, Exponent). That only holds when the
stored exponent is negative. An integer is stored with its trailing zeros
in a positive exponent (1234 at exponent 0, 1230 as 123 at exponent 1),
so Round(-2) dropped the wrong number of digits and moved the exponent
the wrong way: 1234 gave 0.12 and 1500.Round(-3) gave 0. Work from the
exponent of the requested place instead, which is right for every sign.

Fixes #105

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

Copy link
Copy Markdown

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.

Round with negative decimalDigits returns 0.12 for 1234 (and 0 for 1500) when the value has no fractional part

2 participants