Skip to content

Do not round exact values up under :up rounding#239

Open
b-erdem wants to merge 1 commit into
ericmj:mainfrom
b-erdem:fix-up-exact-rounding
Open

Do not round exact values up under :up rounding#239
b-erdem wants to merge 1 commit into
ericmj:mainfrom
b-erdem:fix-up-exact-rounding

Conversation

@b-erdem

@b-erdem b-erdem commented Jul 5, 2026

Copy link
Copy Markdown

Follows up on the :up bug I noted in #236 — independent of #237 and #238.

Problem

:up (round away from zero) increments the coefficient whenever the discarded part is non-empty, even when all discarded digits are zero. An exact value with trailing zeros beyond the precision is rounded up:

Context.with(%Context{precision: 1, rounding: :up}, fn ->
  Decimal.add(Decimal.new(0), Decimal.new("9.0"))   #=> Decimal.new("10")   # value is exactly 9
end)

Decimal.round(Decimal.new("9.0"), 0, :up)           #=> Decimal.new("10")   # should be 9

Per the General Decimal Arithmetic spec, round-up leaves the value unchanged when every discarded digit is zero. Python's decimal returns 9 for both. :ceiling and :floor already guard on any_nonzero?/2 — so today, for a positive value, :up and :ceiling disagree even though both round away from zero (Decimal.add(0, ~d"9.0") at precision 1 gives 10 under :up but 9 under :ceiling). They must agree.

Fix

increment?(:up, …) now checks any_nonzero?(remain, sticky?) instead of returning true unconditionally — the same check :ceiling/:floor use. One clause.

Verification

Differential fuzz against Python's decimal (the GDA reference), comparing numeric values: 0 mismatches across ~3,800 :up cases spanning add/sub/mult/div and round/3, at precisions 1–5. Genuine round-ups still round up (add(0, 3.1) at precision 1 → 4; round(9.01, 0, :up)10).

Tests

Extends the existing with_context/2: up test with the exact-value cases (and a nonzero-discarded case that still rounds up). Full suite passes (101 doctests, 27 properties, 101 tests).

Notes

Round-up (`:up`, away from zero) incremented the coefficient whenever the
discarded part was non-empty, even when every discarded digit was zero.
An exact value with trailing zeros beyond the precision was therefore
rounded up: at precision 1, `Decimal.add(0, Decimal.new("9.0"))` returned
`10` and `Decimal.round(Decimal.new("9.0"), 0, :up)` returned `10`, where
the value is exactly 9.

Per the General Decimal Arithmetic spec, round-up leaves the value
unchanged when all discarded digits are zero. `:ceiling` and `:floor`
already check `any_nonzero?/2`; `:up` now does too, so for a positive
value `:up` and `:ceiling` agree (as they must). Python's decimal returns
9 for these cases.

The clause is shared by every context operation and by round/3.
Verified against Python's decimal: 0 value mismatches across ~3,800
:up cases spanning add/sub/mult/div and round/3.
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.

1 participant