From 06d21033f3a54f3711f8d8fbf59b57d9d4851aa8 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Fri, 11 Sep 2026 15:28:43 +0900 Subject: [PATCH] Floor and clamp the exponent of a root of zero rootSpecials divided the operand exponent by factor with Go's /, which truncates toward zero, so every odd negative exponent came out one too high. It also never brought the result into the context's exponent range and never raised Clamped, unlike quoSpecials. TestGDA compares finite results with Cmp, which treats every zero as equal regardless of exponent, so the vendored vectors never caught it. --- context.go | 19 ++++++++++++-- gda_test.go | 9 +------ zeroroot_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 zeroroot_test.go diff --git a/context.go b/context.go index 9d9e2d8..55c1f86 100644 --- a/context.go +++ b/context.go @@ -461,8 +461,23 @@ func (c *Context) rootSpecials(d, x *Decimal, factor int32) (bool, Condition, er } case 0: d.Set(x) - d.Exponent /= factor - return true, 0, nil + // The ideal exponent is the operand's exponent divided by factor, + // rounded towards negative infinity; Go's / truncates towards zero. + e := x.Exponent / factor + if x.Exponent%factor != 0 && x.Exponent < 0 { + e-- + } + var res Condition + if etiny := c.etiny(); e < etiny { + e = etiny + res = Clamped + } else if e > c.MaxExponent { + e = c.MaxExponent + res = Clamped + } + d.Exponent = e + res, err := c.goError(res) + return true, res, err } return false, 0, nil } diff --git a/gda_test.go b/gda_test.go index 8bd366e..7701098 100644 --- a/gda_test.go +++ b/gda_test.go @@ -846,13 +846,6 @@ var GDAignore = map[string]bool{ } var GDAignoreFlags = map[string]bool{ - // unflagged clamped - "sqtx9024": true, - "sqtx9025": true, - "sqtx9026": true, - "sqtx9027": true, - "sqtx9038": true, - "sqtx9039": true, - "sqtx9040": true, + // apd does not implement the clamp context directive. "sqtx9045": true, } diff --git a/zeroroot_test.go b/zeroroot_test.go new file mode 100644 index 0000000..9a9b05e --- /dev/null +++ b/zeroroot_test.go @@ -0,0 +1,66 @@ +package apd + +import "testing" + +// TestRootSpecialsZeroExponent checks the exponent and conditions of a +// square root or cube root of zero. TestGDA compares finite results with +// Cmp, which considers every zero equal regardless of exponent, so it +// cannot check these cases even though testdata carries them. +func TestRootSpecialsZeroExponent(t *testing.T) { + tests := []struct { + id string + prec uint32 + emax int32 + emin int32 + cbrt bool + x string + exp int32 + neg bool + cond Condition + }{ + // testdata/squareroot.decTest + {id: "sqtx006", prec: 9, emax: 384, emin: -383, x: "00.0", exp: -1}, + {id: "sqtx009", prec: 9, emax: 384, emin: -383, x: "00.000", exp: -2}, + {id: "sqtx017", prec: 9, emax: 384, emin: -383, x: "-0.0", exp: -1, neg: true}, + {id: "sqtx019", prec: 9, emax: 384, emin: -383, x: "-00.000", exp: -2, neg: true}, + {id: "sqtx9010", prec: 4, emax: 9, emin: -9, x: "0E-9", exp: -5}, + {id: "sqtx9012", prec: 4, emax: 9, emin: -9, x: "0E-11", exp: -6}, + {id: "sqtx9014", prec: 4, emax: 9, emin: -9, x: "0E-13", exp: -7}, + {id: "sqtx9023", prec: 4, emax: 9, emin: -9, x: "0E-24", exp: -12}, + {id: "sqtx9024", prec: 4, emax: 9, emin: -9, x: "0E-25", exp: -12, cond: Clamped}, + {id: "sqtx9027", prec: 4, emax: 9, emin: -9, x: "0E-28", exp: -12, cond: Clamped}, + {id: "sqtx9037", prec: 4, emax: 9, emin: -9, x: "0E+19", exp: 9}, + {id: "sqtx9038", prec: 4, emax: 9, emin: -9, x: "0E+20", exp: 9, cond: Clamped}, + {id: "sqtx9040", prec: 4, emax: 9, emin: -9, x: "0E+22", exp: 9, cond: Clamped}, + // testdata/cuberoot-apd.decTest + {id: "cbtx015", prec: 9, emax: 384, emin: -383, cbrt: true, x: "-0.00", exp: -1, neg: true}, + {id: "cbtx016", prec: 9, emax: 384, emin: -383, cbrt: true, x: "-00.0", exp: -1, neg: true}, + {id: "cbtx017", prec: 9, emax: 384, emin: -383, cbrt: true, x: "-0E+9", exp: 3, neg: true}, + {id: "cbtx020", prec: 9, emax: 384, emin: -383, cbrt: true, x: "-0E+12", exp: 4, neg: true}, + } + for _, tc := range tests { + // Parse the operand exactly, as TestGDA does; the context under test + // applies to the operation, not to the literal. + x, _, err := NewFromString(tc.x) + if err != nil { + t.Fatalf("%s: %v", tc.id, err) + } + c := &Context{Precision: tc.prec, MaxExponent: tc.emax, MinExponent: tc.emin, Rounding: RoundHalfUp} + d := new(Decimal) + var res Condition + if tc.cbrt { + res, err = c.Cbrt(d, x) + } else { + res, err = c.Sqrt(d, x) + } + if err != nil { + t.Fatalf("%s: %v", tc.id, err) + } + if !d.IsZero() || d.Exponent != tc.exp || d.Negative != tc.neg { + t.Errorf("%s: got %#v, expected zero with exponent %d, negative %t", tc.id, d, tc.exp, tc.neg) + } + if res != tc.cond { + t.Errorf("%s: got conditions %q, expected %q", tc.id, res, tc.cond) + } + } +}