diff --git a/decimal.go b/decimal.go index 775c417..25c8f68 100644 --- a/decimal.go +++ b/decimal.go @@ -489,6 +489,30 @@ func (d *Decimal) CmpTotal(x *Decimal) int { } } +// CmpTotalMag compares the magnitudes of d and x using the total ordering, and +// returns -1, 0 or +1. It is equivalent to d.Abs().CmpTotal(x.Abs()) but does +// not modify d or x. See CmpTotal for the definition of the total ordering. +func (d *Decimal) CmpTotalMag(x *Decimal) int { + var da, xa Decimal + da.Abs(d) + xa.Abs(x) + return da.CmpTotal(&xa) +} + +// SameQuantum reports whether d and x have the same exponent. Two infinities +// are considered to have the same quantum, as are two NaNs (signaling or quiet); +// a special value and a finite value never do. +func (d *Decimal) SameQuantum(x *Decimal) bool { + dSpecial := d.Form != Finite + xSpecial := x.Form != Finite + if dSpecial || xSpecial { + dNaN := d.Form == NaN || d.Form == NaNSignaling + xNaN := x.Form == NaN || x.Form == NaNSignaling + return (d.Form == Infinite && x.Form == Infinite) || (dNaN && xNaN) + } + return d.Exponent == x.Exponent +} + func (d *Decimal) cmpOrder() int { v := int(d.Form) + 1 if d.Negative { diff --git a/ordering.go b/ordering.go new file mode 100644 index 0000000..9af2b77 --- /dev/null +++ b/ordering.go @@ -0,0 +1,389 @@ +// Copyright 2026 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package apd + +// This file implements the General Decimal Arithmetic ordering and adjacency +// operations: max, min, max-magnitude, min-magnitude, next-plus, next-minus, +// next-toward, logb, scaleb and copy-sign. + +// maxMinSetAsNaN applies the NaN rules that are shared by Max, Min, MaxAbs and +// MinAbs. Unlike most operations, these treat a quiet NaN operand as absent: +// the other (numeric) operand is returned. Only a signaling NaN raises +// InvalidOperation, and two quiet NaNs produce a NaN. It reports whether it +// handled the inputs (i.e. at least one operand was a NaN); if so, d has been +// set and the returned Condition and error should be returned to the caller. +func (c *Context) maxMinSetAsNaN(d, x, y *Decimal) (bool, Condition, error) { + xNaN := x.Form == NaN || x.Form == NaNSignaling + yNaN := y.Form == NaN || y.Form == NaNSignaling + if !xNaN && !yNaN { + return false, 0, nil + } + // A signaling NaN in either operand, or a NaN in both, follows the standard + // NaN handling: signaling raises InvalidOperation and quiets, otherwise the + // first NaN propagates. + if x.Form == NaNSignaling || y.Form == NaNSignaling || (xNaN && yNaN) { + res, err := c.setAsNaN(d, x, y) + return true, res, err + } + // Exactly one operand is a quiet NaN; return the other, rounded to context. + other := y + if yNaN { + other = x + } + res := c.selectResult(d, other) + resErr, err := c.goError(res) + return true, resErr, err +} + +// selectResult sets d to the chosen operand, rounding a finite value to the +// context. Special values (infinities and NaNs) are copied unchanged, matching +// the GDA rule that these selection operations do not otherwise alter their +// operands. +func (c *Context) selectResult(d, chosen *Decimal) Condition { + if chosen.Form != Finite { + d.Set(chosen) + return 0 + } + return c.round(d, chosen) +} + +// Max sets d to the larger of x and y and returns d. If the operands compare +// equal numerically, the one with the larger total order (see CmpTotal) is +// returned; for example Max(1.0, 1.00) is 1.0. A quiet NaN operand is treated +// as absent, so the other operand is returned; a signaling NaN raises +// InvalidOperation. +func (c *Context) Max(d, x, y *Decimal) (Condition, error) { + if handled, res, err := c.maxMinSetAsNaN(d, x, y); handled { + return res, err + } + cmp := x.Cmp(y) + if cmp == 0 { + cmp = x.CmpTotal(y) + } + chosen := x + if cmp < 0 { + chosen = y + } + return c.goError(c.selectResult(d, chosen)) +} + +// Min sets d to the smaller of x and y. If the operands compare equal +// numerically, the one with the smaller total order is returned. NaN handling +// is the same as Max. +func (c *Context) Min(d, x, y *Decimal) (Condition, error) { + if handled, res, err := c.maxMinSetAsNaN(d, x, y); handled { + return res, err + } + cmp := x.Cmp(y) + if cmp == 0 { + cmp = x.CmpTotal(y) + } + chosen := x + if cmp > 0 { + chosen = y + } + return c.goError(c.selectResult(d, chosen)) +} + +// MaxAbs sets d to whichever of x and y has the larger magnitude, preserving +// its sign. If the magnitudes are equal the total order of the signed operands +// is used as a tie-break, as in Max. NaN handling is the same as Max. +func (c *Context) MaxAbs(d, x, y *Decimal) (Condition, error) { + if handled, res, err := c.maxMinSetAsNaN(d, x, y); handled { + return res, err + } + var xa, ya Decimal + cmp := xa.Abs(x).Cmp(ya.Abs(y)) + if cmp == 0 { + cmp = x.CmpTotal(y) + } + chosen := x + if cmp < 0 { + chosen = y + } + return c.goError(c.selectResult(d, chosen)) +} + +// MinAbs sets d to whichever of x and y has the smaller magnitude, preserving +// its sign. If the magnitudes are equal the total order of the signed operands +// is used as a tie-break, as in Min. NaN handling is the same as Max. +func (c *Context) MinAbs(d, x, y *Decimal) (Condition, error) { + if handled, res, err := c.maxMinSetAsNaN(d, x, y); handled { + return res, err + } + var xa, ya Decimal + cmp := xa.Abs(x).Cmp(ya.Abs(y)) + if cmp == 0 { + cmp = x.CmpTotal(y) + } + chosen := x + if cmp > 0 { + chosen = y + } + return c.goError(c.selectResult(d, chosen)) +} + +// setNmax sets d to the largest finite magnitude representable in the context, +// Nmax, with the given sign. +func (c *Context) setNmax(d *Decimal, negative bool) { + var tmp BigInt + d.Coeff.Sub(tableExp10(int64(c.Precision), &tmp), bigOne) + d.Exponent = c.MaxExponent - int32(c.Precision) + 1 + d.Negative = negative + d.Form = Finite +} + +// tinyBelowEtiny sets t to a positive quantity one decimal place below the +// smallest subnormal, so that directed rounding of a value plus or minus t +// yields the adjacent representable number. +func (c *Context) tinyBelowEtiny(t *Decimal) { + t.Form = Finite + t.Negative = false + t.Exponent = c.etiny() - 1 + t.Coeff.Set(bigOne) +} + +// nextPlus sets d to the number adjacent to x in the direction of +Infinity, +// without raising any conditions. x must not be a NaN. It is the shared kernel +// of NextPlus and NextToward. +// +// Negative operands are handled by symmetry (nextPlus(x) == -nextMinus(-x)) so +// that the increment arithmetic always runs on a non-negative value. This +// avoids relying on directed rounding of negative subnormals, which apd rounds +// by magnitude regardless of sign. +func (c *Context) nextPlus(d, x *Decimal) { + if x.Form == Infinite { + if x.Negative { + // The largest representable negative number. + c.setNmax(d, true) + } else { + d.Set(decimalInfinity) + } + return + } + if x.Negative { + var m Decimal + m.Abs(x) + c.nextDownPos(d, &m) + d.Negative = !d.Negative + return + } + c.nextUpPos(d, x) +} + +// nextMinus sets d to the number adjacent to x in the direction of -Infinity, +// without raising any conditions. x must not be a NaN. It is the shared kernel +// of NextMinus and NextToward. +func (c *Context) nextMinus(d, x *Decimal) { + if x.Form == Infinite { + if x.Negative { + d.Set(decimalInfinity) + d.Negative = true + } else { + // The largest representable positive number. + c.setNmax(d, false) + } + return + } + if x.Negative { + var m Decimal + m.Abs(x) + c.nextUpPos(d, &m) + d.Negative = !d.Negative + return + } + c.nextDownPos(d, x) +} + +// nextUpPos sets d to the neighbour of x toward +Infinity, where x is a +// non-negative finite value. +func (c *Context) nextUpPos(d, x *Decimal) { + nc := *c + nc.Rounding = RoundCeiling + nc.Traps = 0 + // Round x toward +Infinity. If that changes its value, x was not + // representable and the rounded value is the adjacent one. + var fixed Decimal + nc.round(&fixed, x) + if fixed.Cmp(x) != 0 { + d.Set(&fixed) + return + } + // x is representable; adding a quantity below the smallest subnormal and + // rounding toward +Infinity yields the next representable number. The + // operands are non-negative, so the sum is non-negative. + var tiny Decimal + c.tinyBelowEtiny(&tiny) + nc.Add(d, x, &tiny) +} + +// nextDownPos sets d to the neighbour of x toward -Infinity, where x is a +// non-negative finite value. +func (c *Context) nextDownPos(d, x *Decimal) { + if x.IsZero() { + // The neighbour of zero toward -Infinity is the smallest negative + // subnormal; this is exact. + d.Form = Finite + d.Negative = true + d.Exponent = c.etiny() + d.Coeff.Set(bigOne) + return + } + nc := *c + nc.Rounding = RoundFloor + nc.Traps = 0 + var fixed Decimal + nc.round(&fixed, x) + if fixed.Form == Infinite { + // apd rounds every overflow to Infinity, but rounding a too-large + // positive value toward -Infinity clamps to the largest finite value. + c.setNmax(&fixed, false) + } + if fixed.Cmp(x) != 0 { + d.Set(&fixed) + return + } + // x is a positive representable value; subtracting tiny keeps the + // difference non-negative, so directed rounding stays on the positive side. + var tiny Decimal + c.tinyBelowEtiny(&tiny) + nc.Sub(d, x, &tiny) +} + +// NextPlus sets d to the smallest representable number larger than x. A NaN +// operand propagates, and a signaling NaN raises InvalidOperation. The +// operation itself raises no other conditions, even when the result overflows +// to Infinity. +func (c *Context) NextPlus(d, x *Decimal) (Condition, error) { + if c.shouldSetAsNaN(x, nil) { + return c.setAsNaN(d, x, nil) + } + c.nextPlus(d, x) + return 0, nil +} + +// NextMinus sets d to the largest representable number smaller than x. NaN +// handling matches NextPlus. +func (c *Context) NextMinus(d, x *Decimal) (Condition, error) { + if c.shouldSetAsNaN(x, nil) { + return c.setAsNaN(d, x, nil) + } + c.nextMinus(d, x) + return 0, nil +} + +// NextToward sets d to the number adjacent to x in the direction of y. If x and +// y are numerically equal, the result is x with the sign of y and no conditions +// are raised. Otherwise the result is the neighbour of x toward y; unlike +// NextPlus and NextMinus, this operation raises Overflow, Underflow, Inexact, +// Rounded, Subnormal and Clamped as appropriate. NaN handling matches NextPlus. +func (c *Context) NextToward(d, x, y *Decimal) (Condition, error) { + if c.shouldSetAsNaN(x, y) { + return c.setAsNaN(d, x, y) + } + cmp := x.Cmp(y) + if cmp == 0 { + // The result is x with the sign of y (copy-sign). + d.Abs(x) + d.Negative = y.Negative + return 0, nil + } + if cmp < 0 { + c.nextPlus(d, x) + } else { + c.nextMinus(d, x) + } + var res Condition + if d.Form == Infinite { + res = Overflow | Inexact | Rounded + } else if adj := int64(d.Exponent) + d.NumDigits() - 1; adj < int64(c.MinExponent) { + res = Underflow | Subnormal | Inexact | Rounded + if d.IsZero() { + res |= Clamped + } + } + return c.goError(res) +} + +// LogB sets d to the adjusted exponent of x: the integer floor(log10(|x|)), +// expressed as a decimal. LogB(0) is -Infinity and raises DivisionByZero; +// LogB(±Infinity) is +Infinity. A signaling NaN raises InvalidOperation. +func (c *Context) LogB(d, x *Decimal) (Condition, error) { + if c.shouldSetAsNaN(x, nil) { + return c.setAsNaN(d, x, nil) + } + if x.Form == Infinite { + d.Set(decimalInfinity) + return 0, nil + } + if x.IsZero() { + d.Set(decimalInfinity) + d.Negative = true + return c.goError(DivisionByZero) + } + adj := int64(x.Exponent) + x.NumDigits() - 1 + d.SetInt64(adj) + return c.goError(c.round(d, d)) +} + +// ScaleB sets d to x * 10**y, where y must be an integer. y is required to have +// exponent zero and to satisfy |y| <= 2*(MaxExponent+Precision); otherwise the +// result is NaN and InvalidOperation is raised. The result is rounded to the +// context and may overflow, underflow or become subnormal. NaN handling follows +// the standard rules (signaling NaN raises InvalidOperation). +func (c *Context) ScaleB(d, x, y *Decimal) (Condition, error) { + if c.shouldSetAsNaN(x, y) { + return c.setAsNaN(d, x, y) + } + // y must be an integer represented with a zero exponent, within the + // permitted range. + lim := 2 * (int64(c.MaxExponent) + int64(c.Precision)) + if y.Form != Finite || y.Exponent != 0 || !y.Coeff.IsInt64() { + d.Set(decimalNaN) + return c.goError(InvalidOperation) + } + n := y.Coeff.Int64() + if y.Negative { + n = -n + } + if n < -lim || n > lim { + d.Set(decimalNaN) + return c.goError(InvalidOperation) + } + if x.Form == Infinite { + d.Set(x) + return 0, nil + } + d.Set(x) + newExp := int64(x.Exponent) + n + if newExp > MaxExponent || newExp < MinExponent { + d.Set(decimalNaN) + return c.goError(InvalidOperation) + } + d.Exponent = int32(newExp) + // Rounding to the context finalizes the exponent, applying overflow, + // underflow, subnormal and clamping just as GDA's fix step does. + res := c.round(d, d) + return c.goError(res) +} + +// CopySign sets d to the magnitude of x combined with the sign of y. No +// rounding is performed and no conditions are raised, even for signaling NaN +// operands: only the sign is affected. +func (c *Context) CopySign(d, x, y *Decimal) (Condition, error) { + d.Abs(x) + d.Negative = y.Negative + return 0, nil +} diff --git a/ordering_test.go b/ordering_test.go new file mode 100644 index 0000000..cf67a66 --- /dev/null +++ b/ordering_test.go @@ -0,0 +1,524 @@ +// Copyright 2026 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package apd + +import ( + "fmt" + "testing" +) + +// orderingCtx returns a context configured to match the General Decimal +// Arithmetic specification's default arithmetic testing parameters, so that the +// expected values below line up with the reference implementation. +func orderingCtx() *Context { + c := BaseContext.WithPrecision(9) + c.MaxExponent = 384 + c.MinExponent = -383 + c.Rounding = RoundHalfEven + c.Traps = 0 + return c +} + +// canon renders d as a comparable string that keeps operands like 1.0 and 1.00, +// which are numerically equal but held differently, distinct. +func canon(d *Decimal) string { + switch d.Form { + case Infinite: + if d.Negative { + return "-Inf" + } + return "Inf" + case NaN: + return "NaN" + case NaNSignaling: + return "sNaN" + } + sign := "" + if d.Negative { + sign = "-" + } + return fmt.Sprintf("%s%sE%d", sign, d.Coeff.String(), d.Exponent) +} + +func TestMax(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "2", "2E0"}, + {"2", "1", "2E0"}, + // Numerically equal: the larger total ordering wins. + {"1.0", "1.00", "10E-1"}, + {"1.00", "1.0", "10E-1"}, + {"3.0", "3", "3E0"}, + // Signed zeros: +0 is the greater in the total ordering. + {"0", "-0", "0E0"}, + {"-0", "0", "0E0"}, + {"-1.0", "-1.00", "-100E-2"}, + {"7", "-7", "7E0"}, + {"-2", "1", "1E0"}, + {"0.5", "-0.5", "5E-1"}, + {"1E+2", "1E-2", "1E2"}, + {"1E-391", "-1E-391", "1E-391"}, + // Infinities. + {"Infinity", "1", "Inf"}, + {"-Infinity", "1", "1E0"}, + {"Infinity", "-Infinity", "Inf"}, + // A quiet NaN is treated as absent. + {"NaN", "1", "1E0"}, + {"1", "NaN", "1E0"}, + {"NaN", "NaN", "NaN"}, + // A signaling NaN yields NaN. + {"sNaN", "1", "NaN"}, + {"1", "sNaN", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.Max(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("Max(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestMin(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "2", "1E0"}, + {"2", "1", "1E0"}, + {"1.0", "1.00", "100E-2"}, + {"3.0", "3", "30E-1"}, + {"0", "-0", "-0E0"}, + {"-0", "0", "-0E0"}, + {"-1.0", "-1.00", "-10E-1"}, + {"7", "-7", "-7E0"}, + {"-2", "1", "-2E0"}, + {"1E+2", "1E-2", "1E-2"}, + {"1E-391", "-1E-391", "-1E-391"}, + {"Infinity", "1", "1E0"}, + {"-Infinity", "1", "-Inf"}, + {"Infinity", "-Infinity", "-Inf"}, + {"NaN", "1", "1E0"}, + {"1", "NaN", "1E0"}, + {"NaN", "NaN", "NaN"}, + {"sNaN", "1", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.Min(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("Min(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestMaxAbs(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "-2", "-2E0"}, + {"-2", "1", "-2E0"}, + {"1.0", "1.00", "10E-1"}, + {"7", "-7", "7E0"}, + // Equal magnitudes fall back to the signed total ordering. + {"1E-391", "-1E-391", "1E-391"}, + {"-1.0", "1.00", "100E-2"}, + {"0", "-0", "0E0"}, + {"-0", "0", "0E0"}, + {"Infinity", "1", "Inf"}, + {"-Infinity", "1", "-Inf"}, + {"NaN", "1", "1E0"}, + {"sNaN", "1", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.MaxAbs(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("MaxAbs(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestMinAbs(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "-2", "1E0"}, + {"-2", "1", "1E0"}, + {"1.0", "1.00", "100E-2"}, + {"7", "-7", "-7E0"}, + {"1E-391", "-1E-391", "-1E-391"}, + {"0", "-0", "-0E0"}, + {"-0", "0", "-0E0"}, + {"Infinity", "1", "1E0"}, + {"-Infinity", "1", "1E0"}, + {"Infinity", "-Infinity", "-Inf"}, + {"NaN", "1", "1E0"}, + {"sNaN", "1", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.MinAbs(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("MinAbs(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestNextPlus(t *testing.T) { + tests := []struct { + x, expected string + }{ + {"0", "1E-391"}, + {"-0", "1E-391"}, + {"1", "100000001E-8"}, + {"-1", "-999999999E-9"}, + {"1.0", "100000001E-8"}, + {"0.999999999", "100000000E-8"}, + {"9.99999999E+384", "Inf"}, + {"-9.99999999E+384", "-999999998E376"}, + {"Infinity", "Inf"}, + {"-Infinity", "-999999999E376"}, + {"1E-391", "2E-391"}, + {"-1E-391", "-0E-391"}, + {"NaN", "NaN"}, + {"sNaN", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(tc.x, func(t *testing.T) { + d := new(Decimal) + if _, err := c.NextPlus(d, newDecimal(t, c, tc.x)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("NextPlus(%s) = %s, want %s", tc.x, got, tc.expected) + } + }) + } +} + +func TestNextMinus(t *testing.T) { + tests := []struct { + x, expected string + }{ + {"0", "-1E-391"}, + {"-0", "-1E-391"}, + {"1", "999999999E-9"}, + {"-1", "-100000001E-8"}, + {"0.999999999", "999999998E-9"}, + {"9.99999999E+384", "999999998E376"}, + {"-9.99999999E+384", "-Inf"}, + {"Infinity", "999999999E376"}, + {"-Infinity", "-Inf"}, + {"1E-391", "0E-391"}, + {"-1E-391", "-2E-391"}, + {"NaN", "NaN"}, + {"sNaN", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(tc.x, func(t *testing.T) { + d := new(Decimal) + if _, err := c.NextMinus(d, newDecimal(t, c, tc.x)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("NextMinus(%s) = %s, want %s", tc.x, got, tc.expected) + } + }) + } +} + +func TestNextToward(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "2", "100000001E-8"}, + {"1", "0", "999999999E-9"}, + // Equal operands: x keeps its value, taking the sign of y. + {"1", "1", "1E0"}, + {"1.0", "1", "10E-1"}, + {"-1", "-2", "-100000001E-8"}, + {"0", "1", "1E-391"}, + {"0", "-1", "-1E-391"}, + {"1", "-0", "999999999E-9"}, + {"-1E-391", "0", "-0E-391"}, + {"1E-391", "0", "0E-391"}, + {"Infinity", "1", "999999999E376"}, + {"-Infinity", "1", "-999999999E376"}, + {"NaN", "1", "NaN"}, + {"1", "sNaN", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.NextToward(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("NextToward(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestLogB(t *testing.T) { + tests := []struct { + x, expected string + }{ + {"0", "-Inf"}, + {"-0", "-Inf"}, + {"1", "0E0"}, + {"-1", "0E0"}, + {"250", "2E0"}, + {"0.03", "-2E0"}, + {"1.0", "0E0"}, + {"-2.5", "0E0"}, + {"9.99999999E+384", "384E0"}, + {"1E-391", "-391E0"}, + {"Infinity", "Inf"}, + {"-Infinity", "Inf"}, + {"NaN", "NaN"}, + {"sNaN", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(tc.x, func(t *testing.T) { + d := new(Decimal) + if _, err := c.LogB(d, newDecimal(t, c, tc.x)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("LogB(%s) = %s, want %s", tc.x, got, tc.expected) + } + }) + } +} + +func TestScaleB(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "0", "1E0"}, + {"1", "1", "1E1"}, + {"1", "-1", "1E-1"}, + {"250", "2", "250E2"}, + {"0.03", "2", "3E0"}, + {"1.00", "-2", "100E-4"}, + {"-2.5", "1", "-25E0"}, + {"0", "5", "0E5"}, + {"1E-391", "5", "1E-386"}, + // Non-integer or out-of-range y is invalid. + {"1", "1.5", "NaN"}, + {"1", "800000", "NaN"}, + {"1", "-800000", "NaN"}, + {"Infinity", "2", "Inf"}, + {"NaN", "1", "NaN"}, + {"sNaN", "1", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.ScaleB(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("ScaleB(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestCopySign(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "-1", "-1E0"}, + {"-1", "1", "1E0"}, + {"1.0", "-0", "-10E-1"}, + {"-2.5", "1", "25E-1"}, + {"0", "-0", "-0E0"}, + {"-0", "0", "0E0"}, + {"250", "-2.5", "-250E0"}, + {"Infinity", "-1", "-Inf"}, + {"NaN", "-1", "NaN"}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + if _, err := c.CopySign(d, newDecimal(t, c, tc.x), newDecimal(t, c, tc.y)); err != nil { + t.Fatal(err) + } + if got := canon(d); got != tc.expected { + t.Errorf("CopySign(%s, %s) = %s, want %s", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestSameQuantum(t *testing.T) { + tests := []struct { + x, y string + expected bool + }{ + {"1", "2", true}, + {"1.0", "1.00", false}, + {"1.0", "1.0", true}, + {"0", "-0", true}, + {"1E+2", "1E-2", false}, + {"Infinity", "-Infinity", true}, + {"NaN", "sNaN", true}, + {"NaN", "1", false}, + {"Infinity", "1", false}, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + x := newDecimal(t, testCtx, tc.x) + y := newDecimal(t, testCtx, tc.y) + if got := x.SameQuantum(y); got != tc.expected { + t.Errorf("SameQuantum(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.expected) + } + }) + } +} + +func TestCmpTotalMag(t *testing.T) { + tests := []struct { + x, y string + expected int + }{ + {"1", "2", -1}, + {"1.0", "1.00", 1}, + {"1.00", "1.0", -1}, + {"-1.0", "1.00", 1}, + {"0", "-0", 0}, + {"7", "-7", 0}, + {"-2", "1", 1}, + {"Infinity", "-Infinity", 0}, + {"NaN", "1", 1}, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s", tc.x, tc.y), func(t *testing.T) { + x := newDecimal(t, testCtx, tc.x) + y := newDecimal(t, testCtx, tc.y) + // Ensure the operands are not mutated. + xb, yb := canon(x), canon(y) + if got := x.CmpTotalMag(y); got != tc.expected { + t.Errorf("CmpTotalMag(%s, %s) = %d, want %d", tc.x, tc.y, got, tc.expected) + } + if canon(x) != xb || canon(y) != yb { + t.Errorf("CmpTotalMag mutated an operand: %s,%s -> %s,%s", xb, yb, canon(x), canon(y)) + } + }) + } +} + +// TestOrderingConditions checks the conditions (flags) that the ordering and +// adjacency operations raise, independent of their numeric result. +func TestOrderingConditions(t *testing.T) { + const signals = InvalidOperation | DivisionByZero + tests := []struct { + op, x, y string + expected Condition + }{ + {"Max", "sNaN", "1", InvalidOperation}, + {"Max", "1", "sNaN", InvalidOperation}, + {"Max", "NaN", "1", 0}, + {"Max", "1", "2", 0}, + {"Min", "sNaN", "1", InvalidOperation}, + {"Min", "1", "NaN", 0}, + {"MaxAbs", "1", "sNaN", InvalidOperation}, + {"MinAbs", "sNaN", "1", InvalidOperation}, + {"NextPlus", "sNaN", "", InvalidOperation}, + {"NextPlus", "NaN", "", 0}, + {"NextPlus", "1", "", 0}, + {"NextMinus", "sNaN", "", InvalidOperation}, + {"NextToward", "sNaN", "1", InvalidOperation}, + {"NextToward", "1", "sNaN", InvalidOperation}, + {"NextToward", "1", "2", 0}, + {"LogB", "0", "", DivisionByZero}, + {"LogB", "-0", "", DivisionByZero}, + {"LogB", "sNaN", "", InvalidOperation}, + {"LogB", "250", "", 0}, + {"LogB", "Infinity", "", 0}, + {"ScaleB", "1", "1.5", InvalidOperation}, + {"ScaleB", "1", "800000", InvalidOperation}, + {"ScaleB", "1", "-800000", InvalidOperation}, + {"ScaleB", "sNaN", "1", InvalidOperation}, + {"ScaleB", "1", "NaN", 0}, + {"ScaleB", "1", "2", 0}, + } + c := orderingCtx() + for _, tc := range tests { + t.Run(fmt.Sprintf("%s,%s,%s", tc.op, tc.x, tc.y), func(t *testing.T) { + d := new(Decimal) + x := newDecimal(t, c, tc.x) + var res Condition + var err error + switch tc.op { + case "Max": + res, err = c.Max(d, x, newDecimal(t, c, tc.y)) + case "Min": + res, err = c.Min(d, x, newDecimal(t, c, tc.y)) + case "MaxAbs": + res, err = c.MaxAbs(d, x, newDecimal(t, c, tc.y)) + case "MinAbs": + res, err = c.MinAbs(d, x, newDecimal(t, c, tc.y)) + case "NextPlus": + res, err = c.NextPlus(d, x) + case "NextMinus": + res, err = c.NextMinus(d, x) + case "NextToward": + res, err = c.NextToward(d, x, newDecimal(t, c, tc.y)) + case "LogB": + res, err = c.LogB(d, x) + case "ScaleB": + res, err = c.ScaleB(d, x, newDecimal(t, c, tc.y)) + default: + t.Fatalf("unknown op %s", tc.op) + } + if err != nil { + t.Fatal(err) + } + if got := res & signals; got != tc.expected { + t.Errorf("%s(%s, %s) raised %v, want %v", tc.op, tc.x, tc.y, got, tc.expected) + } + }) + } +}