From e1516236e6d9ac8fd21153db53283c1b46a50ba7 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Wed, 2 Sep 2026 19:15:55 +0300 Subject: [PATCH] Respect rounding mode when quantizing values smaller than one ULP Quantize truncated any value with fewer digits than the target exponent straight to zero, ignoring the context's Rounding mode. RoundCeiling of a positive sub-ULP value (e.g. 0.002 to exponent -1) returned 0.0 instead of 0.1, and RoundFloor of a negative one lost the rounding entirely. Round the discarded coefficient through the configured Rounder instead. --- context.go | 6 ++++++ decimal_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/context.go b/context.go index 9d9e2d8..5d5c284 100644 --- a/context.go +++ b/context.go @@ -1182,7 +1182,13 @@ func (c *Context) quantize(d, v *Decimal, exp int32) Condition { p := int32(d.NumDigits()) - diff if p < 0 { if !d.IsZero() { + var discard Decimal + discard.Coeff.Set(&d.Coeff) + discard.Exponent = -diff d.Coeff.SetInt64(0) + if c.Rounding.ShouldAddOne(&d.Coeff, d.Negative, discard.Cmp(decimalHalf)) { + d.Coeff.SetInt64(1) + } res = Inexact | Rounded } } else { diff --git a/decimal_test.go b/decimal_test.go index cda8926..d8dfbde 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -702,6 +702,41 @@ func TestQuantize(t *testing.T) { } } +func TestQuantizeRounding(t *testing.T) { + tests := []struct { + s string + e int32 + rnd Rounder + expect string + }{ + {s: "0.002", e: -1, rnd: RoundCeiling, expect: "0.1"}, + {s: "1.002", e: -1, rnd: RoundCeiling, expect: "1.1"}, + {s: "-0.002", e: -1, rnd: RoundCeiling, expect: "-0.0"}, + {s: "0.002", e: -1, rnd: RoundFloor, expect: "0.0"}, + {s: "-0.002", e: -1, rnd: RoundFloor, expect: "-0.1"}, + {s: "0.002", e: -1, rnd: RoundUp, expect: "0.1"}, + {s: "0.002", e: -1, rnd: RoundDown, expect: "0.0"}, + {s: "0.04", e: -1, rnd: RoundHalfUp, expect: "0.0"}, + {s: "0.05", e: -1, rnd: RoundHalfUp, expect: "0.1"}, + {s: "0.05", e: -1, rnd: RoundHalfEven, expect: "0.0"}, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("%s: %d %s", tc.s, tc.e, tc.rnd), func(t *testing.T) { + c := Context{Precision: 100, Rounding: tc.rnd, MaxExponent: 1000, MinExponent: -1000, Traps: DefaultTraps} + d, _, err := NewFromString(tc.s) + if err != nil { + t.Fatal(err) + } + if _, err := c.Quantize(d, d, tc.e); err != nil { + t.Fatal(err) + } + if s := d.String(); s != tc.expect { + t.Fatalf("expected: %s, got: %s", tc.expect, s) + } + }) + } +} + func TestCmpOrder(t *testing.T) { tests := []struct { s string