From 17621dd2a0e22d0874da29715ba8d89a23e5cc62 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:17:33 +1000 Subject: [PATCH 01/17] verify that all tests pass --- decimal64.go | 17 ++- decimal64_debug.go | 33 +++++ decimal64_ndebug.go | 13 ++ decimal64_test.go | 8 +- decimal64const.go | 15 ++- decimal64fmt.go | 110 +++++++++++++-- decimal64fmt_test.go | 182 +++++++++++++++++++++---- decimal64math.go | 46 +++++-- decimal64scan.go | 4 +- decimalSuite_test.go | 303 +++++++++++++++++++++--------------------- dectest/ddFMA.decTest | 9 +- go.mod | 6 +- go.sum | 6 - 13 files changed, 520 insertions(+), 232 deletions(-) create mode 100644 decimal64_debug.go create mode 100644 decimal64_ndebug.go diff --git a/decimal64.go b/decimal64.go index c0e74bf..e20b408 100644 --- a/decimal64.go +++ b/decimal64.go @@ -5,8 +5,6 @@ import ( "math/bits" ) -type flavor int -type roundingMode int type discardedDigit int const ( @@ -16,6 +14,8 @@ const ( gt5 ) +type flavor int + const ( flNormal flavor = iota flInf @@ -23,6 +23,8 @@ const ( flSNaN ) +type roundingMode int + const ( roundHalfUp roundingMode = iota roundHalfEven @@ -33,7 +35,8 @@ const ( // It uses the binary representation method. // Decimal64 is intentionally a struct to ensure users don't accidentally cast it to uint64 type Decimal64 struct { - bits uint64 + bits uint64 + debugInfo //nolint:unused } // decParts stores the constituting decParts of a decimal64. @@ -166,7 +169,7 @@ func roundStatus(significand uint64, exp int, targetExp int) discardedDigit { return gt5 } -//func from stack overflow: samgak +// func from stack overflow: samgak // TODO: make this more efficent func countTrailingZeros(n uint64) int { zeros := 0 @@ -198,12 +201,12 @@ func newFromParts(sign int, exp int, significand uint64) Decimal64 { if significand < 0x8<<50 { // s EEeeeeeeee (0)ttt tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt // EE ∈ {00, 01, 10} - return Decimal64{s | uint64(exp+expOffset)<<(63-10) | significand} + return Decimal64{bits: s | uint64(exp+expOffset)<<(63-10) | significand}.debug() } // s 11EEeeeeeeee (100)t tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt // EE ∈ {00, 01, 10} significand &= 0x8<<50 - 1 - return Decimal64{s | uint64(0xc00|(exp+expOffset))<<(63-12) | significand} + return Decimal64{bits: s | uint64(0xc00|(exp+expOffset))<<(63-12) | significand}.debug() } func (d Decimal64) parts() (fl flavor, sign int, exp int, significand uint64) { @@ -408,7 +411,7 @@ func (d Decimal64) Class() string { } -//numDecimalDigits returns the magnitude (number of digits) of a uint64. +// numDecimalDigits returns the magnitude (number of digits) of a uint64. func numDecimalDigits(n uint64) int { numBits := 64 - bits.LeadingZeros64(n) numDigits := numBits * 3 / 10 diff --git a/decimal64_debug.go b/decimal64_debug.go new file mode 100644 index 0000000..5a8d254 --- /dev/null +++ b/decimal64_debug.go @@ -0,0 +1,33 @@ +//go:build decimal_debug +// +build decimal_debug + +package decimal + +// Decimal64 represents an IEEE 754 64-bit floating point decimal number. +// It uses the binary representation method. +// Decimal64 is intentionally a struct to ensure users don't accidentally cast it to uint64 +type debugInfo struct { + s string + fl flavor + sign int + exp int + significand uint64 +} + +func (d Decimal64) debug() Decimal64 { + s := d.s + if s == "" { + s = d.String() + } + fl, sign, exp, significand := d.parts() + return Decimal64{ + bits: d.bits, + debugInfo: debugInfo{ + s: s, + fl: fl, + sign: sign, + exp: exp, + significand: significand, + }, + } +} diff --git a/decimal64_ndebug.go b/decimal64_ndebug.go new file mode 100644 index 0000000..d48baec --- /dev/null +++ b/decimal64_ndebug.go @@ -0,0 +1,13 @@ +//go:build !decimal_debug +// +build !decimal_debug + +package decimal + +// Decimal64 represents an IEEE 754 64-bit floating point decimal number. +// It uses the binary representation method. +// Decimal64 is intentionally a struct to ensure users don't accidentally cast it to uint64 +type debugInfo struct{} //nolint:unused + +func (d Decimal64) debug() Decimal64 { + return d +} diff --git a/decimal64_test.go b/decimal64_test.go index 1165346..2836411 100644 --- a/decimal64_test.go +++ b/decimal64_test.go @@ -70,12 +70,10 @@ func TestDecimal64Int64(t *testing.T) { require.EqualValues(int64(math.MaxInt64), Infinity64.Int64()) require.EqualValues(int64(math.MinInt64), NegInfinity64.Int64()) - googol, err := Parse64("1e100") - require.NoError(err) + googol := MustParse64("1e100") require.EqualValues(int64(math.MaxInt64), googol.Int64()) - long, err := Parse64("91234567890123456789e20") - require.NoError(err) + long := MustParse64("91234567890123456789e20") require.EqualValues(int64(math.MaxInt64), long.Int64()) } @@ -154,7 +152,7 @@ func TestDecimal64isZero(t *testing.T) { require := require.New(t) require.Equal(true, Zero64.IsZero()) - require.Equal(true, Decimal64{Zero64.bits | neg64}.IsZero()) + require.Equal(true, Decimal64{bits: Zero64.bits | neg64}.IsZero()) require.Equal(false, One64.IsZero()) } diff --git a/decimal64const.go b/decimal64const.go index c3693b4..b885d3e 100644 --- a/decimal64const.go +++ b/decimal64const.go @@ -13,28 +13,29 @@ var One64 = newFromParts(0, -15, decimal64Base) var NegOne64 = newFromParts(1, -15, decimal64Base) // Infinity64 represents ∞ as a Decimal64. -var Infinity64 = Decimal64{inf64} +var Infinity64 = Decimal64{bits: inf64}.debug() // NegInfinity64 represents -∞ as a Decimal64. -var NegInfinity64 = Decimal64{neg64 | inf64} +var NegInfinity64 = Decimal64{bits: neg64 | inf64}.debug() // QNaN64 represents a quiet NaN as a Decimal64. -var QNaN64 = Decimal64{0x7c << 56} +var QNaN64 = Decimal64{bits: 0x7c << 56}.debug() // SNaN64 represents a signalling NaN as a Decimal64. -var SNaN64 = Decimal64{0x7e << 56} +var SNaN64 = Decimal64{bits: 0x7e << 56}.debug() // Pi64 represents π. -var Pi64 = newFromParts(0, -15, 3141592653589793) +var Pi64 = newFromParts(0, -15, 3_141592653589793) // E64 represents e (lim[n→∞](1+1/n)ⁿ). -var E64 = newFromParts(0, -15, 2718281828459045) +var E64 = newFromParts(0, -15, 2_718281828459045) var neg64 uint64 = 0x80 << 56 var inf64 uint64 = 0x78 << 56 // 1E15 -const decimal64Base uint64 = 1000 * 1000 * 1000 * 1000 * 1000 +const decimal64Base uint64 = 1_000_000_000_000_000 +const decimal64Digits = 16 // maxSig is the maximum significand possible that fits in 16 decimal places. const maxSig = 10*decimal64Base - 1 diff --git a/decimal64fmt.go b/decimal64fmt.go index 39f3bbc..1991c7a 100644 --- a/decimal64fmt.go +++ b/decimal64fmt.go @@ -1,7 +1,9 @@ package decimal import ( + "bytes" "fmt" + "strconv" ) func appendFrac64(buf []byte, n, limit uint64) []byte { @@ -14,6 +16,36 @@ func appendFrac64(buf []byte, n, limit uint64) []byte { return buf } +var zeros = []byte("0000000000000000") + +func appendFrac64Prec(buf []byte, n uint64, prec int) []byte { + // Add a digit in front so strconv.AppendUint doesn't trim leading zeros. + n += 10 * decimal64Base + if prec < 16 { + unit := powersOf10[16-prec] + rem := n % unit + n /= unit + if rem > unit/2 || rem == unit/2 && n%2 == 1 { + n++ + } + } + + // p/2 adds 5 to the digit past the desired precision in order to round up. + buflen := len(buf) + prefix := buf[buflen-1] + buf = strconv.AppendUint(buf[:buflen-1], n, 10) + buf[buflen-1] = prefix + + for ; prec >= 2*decimal64Digits; prec -= decimal64Digits { + buf = append(buf, zeros...) + } + if prec > decimal64Digits { + buf = append(buf, zeros[:prec-decimal64Digits]...) + } + + return buf +} + func appendUint64(buf []byte, n, limit uint64) []byte { zeroPrefix := false for limit > 0 { @@ -28,15 +60,30 @@ func appendUint64(buf []byte, n, limit uint64) []byte { return buf } +func appendUint64New(buf []byte, n, limit uint64) []byte { + return strconv.AppendUint(buf, n/(decimal64Base/limit), 10) +} + // Append appends the text representation of d to buf. func (d Decimal64) Append(buf []byte, format byte, prec int) []byte { + return d.append(buf, format, -1, prec) +} + +var dotSuffix = []byte{'.'} + +// Append appends the text representation of d to buf. +func (d Decimal64) append(buf []byte, format byte, width, prec int) []byte { flav, sign, exp, significand := d.parts() if sign == 1 { buf = append(buf, '-') } switch flav { case flQNaN, flSNaN: - return appendUint64(append(buf, []byte("NaN")...), significand, 10000) + buf = append(buf, []byte("NaN")...) + if significand != 0 { + return appendUint64(buf, significand, 10000) + } + return buf case flInf: return append(buf, []byte("inf")...) } @@ -66,19 +113,32 @@ formatBlock: case 'f', 'F': exponent, whole, frac := expWholeFrac(exp, significand) if whole > 0 { - buf = appendUint64(buf, whole, decimal64Base) + buf = appendUint64New(buf, whole, decimal64Base) for ; exponent > 0; exponent-- { buf = append(buf, '0') } } else { buf = append(buf, '0') } - if frac > 0 { - buf = appendFrac64(append(buf, '.'), frac, decimal64Base) + if frac > 0 || prec != 0 { + p := prec + if prec == -1 { + p = decimal64Digits + } + buf = append(buf, '.') + if exponent < 0 { + p += exponent + buf = append(buf, zeros[:-exponent]...) + } + buf = appendFrac64Prec(buf, frac, p) + if prec == -1 { + buf = bytes.TrimRight(buf, "0") + } + buf = bytes.TrimSuffix(buf, dotSuffix) } return buf case 'g', 'G': - if exp < -16-4 || exp > prec { + if exp < -decimal64Digits-4 || prec >= 0 && exp > prec { format -= 'g' - 'e' } else { format -= 'g' - 'f' @@ -91,29 +151,57 @@ formatBlock: // Format implements fmt.Formatter. func (d Decimal64) Format(s fmt.State, format rune) { + width, hasWidth := s.Width() + if !hasWidth { + width = -1 + } prec, hasPrec := s.Precision() if !hasPrec { - prec = 6 + prec = -1 } switch format { - case 'e', 'E', 'f', 'F', 'g', 'G': - // nothing to do + case 'e', 'E', 'f', 'F': + if !hasPrec { + prec = 6 + } + case 'g', 'G': case 'v': format = 'g' default: fmt.Fprintf(s, "%%!%c(*decimal.Decimal64=%s)", format, d.String()) return } - s.Write(d.Append(make([]byte, 0, 8), byte(format), prec)) + s.Write(d.append(make([]byte, 0, 16), byte(format), width, prec)) } // String returns a string representation of d. func (d Decimal64) String() string { - return d.Text('g', 10) + return d.Text('g', -1) } // Text converts the floating-point number x to a string according to the given // format and precision prec. func (d Decimal64) Text(format byte, prec int) string { - return string(d.Append(make([]byte, 0, 8), format, prec)) + return string(d.Append(make([]byte, 0, 16), format, prec)) +} + +// RoundHalfAwayFromZero returns a Decimal64 with the smallest possible +// increment applied to the significand. +// +// The default behaviour when formatting Decimal64 is to use half-even rounding, +// which rounds the last digit away from zero if it is odd or leaves it as is if +// it is even. +// This function changes the rounding behaviour such that the last formatted +// digit will always round away from zero when the next digit is a 5. +// The downside is that the number just before a half might round up, but this +// very unlikely since halves are far more likely that almost halves. +func (d Decimal64) RoundHalfAwayFromZero() Decimal64 { + flav, sign, exp, significand := d.parts() + if flav != flNormal { + return d + } + if significand < 10*decimal64Base-1 { + return Decimal64{bits: d.bits + 1}.debug() + } + return newFromParts(sign, exp+1, decimal64Base) } diff --git a/decimal64fmt_test.go b/decimal64fmt_test.go index 943691d..78d67f3 100644 --- a/decimal64fmt_test.go +++ b/decimal64fmt_test.go @@ -6,12 +6,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDecimal64String(t *testing.T) { require := require.New(t) + require.Equal(strconv.Itoa(0), New64FromInt64(0).String()) for i := int64(-1000); i <= 1000; i++ { require.Equal(strconv.Itoa(int(i)), New64FromInt64(i).String()) } @@ -57,6 +59,140 @@ func TestDecimal64Format(t *testing.T) { require.Equal("42", New64FromInt64(42).String()) } +func TestDecimal64FormatNaN(t *testing.T) { + t.Parallel() + + n := MustParse64("-sNan33") + assert.Equal(t, "-NaN33", n.String()) +} + +func TestDecimal64FormatPrec(t *testing.T) { + t.Parallel() + pi := MustParse64("3.1415926535897932384626433") + + assert.Equal(t, "3.141592653589793", fmt.Sprintf("%v", pi)) + assert.Equal(t, "3.141593", fmt.Sprintf("%f", pi)) + assert.Equal(t, "3", fmt.Sprintf("%.0f", pi)) + assert.Equal(t, "3.1", fmt.Sprintf("%.1f", pi)) + assert.Equal(t, "3.14", fmt.Sprintf("%.2f", pi)) + assert.Equal(t, "3.142", fmt.Sprintf("%.3f", pi)) + assert.Equal(t, "3.141593", fmt.Sprintf("%.6f", pi)) + assert.Equal(t, "3.141592654", fmt.Sprintf("%.9f", pi)) + assert.Equal(t, "3.1415926536", fmt.Sprintf("%.10f", pi)) + assert.Equal(t, "3.141592653589793", fmt.Sprintf("%.15f", pi)) + assert.Equal(t, "3.14159265358979300000", fmt.Sprintf("%.20f", pi)) + assert.Equal(t, "3.1415926535897930"+strings.Repeat("0", 64), fmt.Sprintf("%.80f", pi)) + assert.Equal(t, "3.1415926535897930"+strings.Repeat("0", 100), fmt.Sprintf("%.116f", pi)) + + pi = pi.Add(New64FromInt64(100)) + assert.Equal(t, "103.1415926535898", fmt.Sprintf("%v", pi)) + assert.Equal(t, "103.141593", fmt.Sprintf("%f", pi)) + assert.Equal(t, "103", fmt.Sprintf("%.0f", pi)) + assert.Equal(t, "103.1", fmt.Sprintf("%.1f", pi)) + assert.Equal(t, "103.14", fmt.Sprintf("%.2f", pi)) + assert.Equal(t, "103.142", fmt.Sprintf("%.3f", pi)) + assert.Equal(t, "103.141593", fmt.Sprintf("%.6f", pi)) + assert.Equal(t, "103.141592654", fmt.Sprintf("%.9f", pi)) + assert.Equal(t, "103.1415926536", fmt.Sprintf("%.10f", pi)) + assert.Equal(t, "103.141592653589800", fmt.Sprintf("%.15f", pi)) + assert.Equal(t, "103.14159265358980000000", fmt.Sprintf("%.20f", pi)) + assert.Equal(t, "103.1415926535898000"+strings.Repeat("0", 64), fmt.Sprintf("%.80f", pi)) + assert.Equal(t, "103.1415926535898000"+strings.Repeat("0", 100), fmt.Sprintf("%.116f", pi)) + + pi = pi.Add(New64FromInt64(100_000)) + assert.Equal(t, "100103.1415926536", fmt.Sprintf("%v", pi)) + assert.Equal(t, "100103.141593", fmt.Sprintf("%f", pi)) + assert.Equal(t, "100103", fmt.Sprintf("%.0f", pi)) + assert.Equal(t, "100103.1", fmt.Sprintf("%.1f", pi)) + assert.Equal(t, "100103.14", fmt.Sprintf("%.2f", pi)) + assert.Equal(t, "100103.142", fmt.Sprintf("%.3f", pi)) + assert.Equal(t, "100103.141593", fmt.Sprintf("%.6f", pi)) + assert.Equal(t, "100103.141592654", fmt.Sprintf("%.9f", pi)) + assert.Equal(t, "100103.1415926536", fmt.Sprintf("%.10f", pi)) + assert.Equal(t, "100103.141592653600000", fmt.Sprintf("%.15f", pi)) + assert.Equal(t, "100103.14159265360000000000", fmt.Sprintf("%.20f", pi)) + assert.Equal(t, "100103.1415926536000000"+strings.Repeat("0", 64), fmt.Sprintf("%.80f", pi)) + assert.Equal(t, "100103.1415926536000000"+strings.Repeat("0", 100), fmt.Sprintf("%.116f", pi)) + + // Add five digits to the significand so we round at a 2. + pi = pi.Add(New64FromInt64(10_100_000_000)) + assert.Equal(t, "10100100103.14159", fmt.Sprintf("%v", pi)) + assert.Equal(t, "10100100103.141590", fmt.Sprintf("%f", pi)) + assert.Equal(t, "10100100103", fmt.Sprintf("%.0f", pi)) + assert.Equal(t, "10100100103.1", fmt.Sprintf("%.1f", pi)) + assert.Equal(t, "10100100103.14", fmt.Sprintf("%.2f", pi)) + assert.Equal(t, "10100100103.142", fmt.Sprintf("%.3f", pi)) + assert.Equal(t, "10100100103.141590", fmt.Sprintf("%.6f", pi)) + assert.Equal(t, "10100100103.141590000", fmt.Sprintf("%.9f", pi)) + assert.Equal(t, "10100100103.1415900000", fmt.Sprintf("%.10f", pi)) + assert.Equal(t, "10100100103.141590000000000", fmt.Sprintf("%.15f", pi)) + assert.Equal(t, "10100100103.14159000000000000000", fmt.Sprintf("%.20f", pi)) + assert.Equal(t, "10100100103.1415900000000000"+strings.Repeat("0", 64), fmt.Sprintf("%.80f", pi)) + assert.Equal(t, "10100100103.1415900000000000"+strings.Repeat("0", 100), fmt.Sprintf("%.116f", pi)) +} + +func TestDecimal64FormatPrecEdgeCases(t *testing.T) { + t.Parallel() + + tests := []struct{ expected, input string }{ + {"0.062", "0.0625"}, + {"0.063", "0.062500001"}, + {"0.062", "0.0625000000000000000000000000000000001"}, + {"-0.062", "-0.0625"}, + {"-0.063", "-0.062500001"}, + {"-0.062", "-0.0625000000000000000000000000000000001"}, + {"0.188", "0.1875"}, + {"0.188", "0.187500001"}, + {"0.188", "0.1875000000000000000000000000000000001"}, + {"-0.188", "-0.1875"}, + {"-0.188", "-0.187500001"}, + {"-0.188", "-0.1875000000000000000000000000000000001"}, + } + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + n, err := Parse64(test.input) + require.NoError(t, err) + assert.Equal(t, test.expected, fmt.Sprintf("%.3f", n)) + }) + } +} + +func TestDecimal64FormatPrecEdgeCasesHalfAway(t *testing.T) { + t.Parallel() + + tests := []struct{ expected, input string }{ + {"0.063", "0.0625"}, + {"0.063", "0.062500001"}, + {"0.063", "0.0625000000000000000000000000000000001"}, + {"-0.063", "-0.0625"}, + {"-0.063", "-0.062500001"}, + {"-0.063", "-0.0625000000000000000000000000000000001"}, + {"0.188", "0.1875"}, + {"0.188", "0.187500001"}, + {"0.188", "0.1875000000000000000000000000000000001"}, + {"-0.188", "-0.1875"}, + {"-0.188", "-0.187500001"}, + {"-0.188", "-0.1875000000000000000000000000000000001"}, + } + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + n, err := Parse64(test.input) + require.NoError(t, err) + assert.Equal(t, test.expected, fmt.Sprintf("%.3f", n.RoundHalfAwayFromZero().Float64())) + }) + } +} + +func TestDecimal64Format2(t *testing.T) { + t.Parallel() + + a := MustParse64("0.0001643835616") + require.Equal(t, "0.000164383562", fmt.Sprintf("%.12f", a)) + b := New64FromInt64(600).Quo(New64FromInt64(10000)) + b = b.Quo(New64FromInt64(365)) + require.Equal(t, "0.000164383562", fmt.Sprintf("%.12f", b)) +} + func BenchmarkDecimal64Format(b *testing.B) { d := New64FromInt64(123456789) for i := 0; i <= b.N; i++ { @@ -65,38 +201,36 @@ func BenchmarkDecimal64Format(b *testing.B) { } func TestDecimal64Append(t *testing.T) { - require := require.New(t) - - requireAppend := func(expected string, d Decimal64, format byte, prec int) { - require.Equal(expected, string(d.Append([]byte{}, format, prec))) + assertAppend := func(expected string, d Decimal64, format byte, prec int) { + assert.Equal(t, expected, string(d.Append([]byte{}, format, prec))) } for i := int64(-1000); i <= 1000; i++ { - require.Equal( + assert.Equal(t, strconv.FormatInt(i, 10), string(New64FromInt64(i).Append([]byte{}, 'g', 0)), ) } - requireAppend("NaN", QNaN64, 'g', 0) - requireAppend("inf", Infinity64, 'g', 0) - requireAppend("-inf", NegInfinity64, 'g', 0) - requireAppend("-0", NegZero64, 'g', 0) - requireAppend("NaN", QNaN64, 'f', 0) - requireAppend("NaN", SNaN64, 'f', 0) - requireAppend("inf", Infinity64, 'f', 0) - requireAppend("-inf", NegInfinity64, 'f', 0) - requireAppend("%w", Zero64, 'w', 0) - - requireAppend("1.23456789e+8", MustParse64("123456789"), 'e', 0) - requireAppend("1.23456789e+18", MustParse64("123456789e10"), 'e', 0) - requireAppend("1.23456789e-18", MustParse64("123456789e-26"), 'e', 0) - requireAppend("1234567890000000000", MustParse64("123456789e10"), 'f', 0) - - requireAppend("123456789", MustParse64("123456789"), 'g', 0) - requireAppend("1.23456789e+18", MustParse64("123456789e10"), 'g', 0) - requireAppend("1.23456789e-18", MustParse64("123456789e-26"), 'g', 0) - requireAppend("1.23456789e+18", MustParse64("123456789e10"), 'g', 0) + assertAppend("NaN", QNaN64, 'g', 0) + assertAppend("inf", Infinity64, 'g', 0) + assertAppend("-inf", NegInfinity64, 'g', 0) + assertAppend("-0", NegZero64, 'g', 0) + assertAppend("NaN", QNaN64, 'f', 0) + assertAppend("NaN", SNaN64, 'f', 0) + assertAppend("inf", Infinity64, 'f', 0) + assertAppend("-inf", NegInfinity64, 'f', 0) + assertAppend("%w", Zero64, 'w', 0) + + assertAppend("1.23456789e+8", MustParse64("123456789"), 'e', 0) + assertAppend("1.23456789e+18", MustParse64("123456789e10"), 'e', 0) + assertAppend("1.23456789e-18", MustParse64("123456789e-26"), 'e', 0) + assertAppend("1234567890000000000", MustParse64("123456789e10"), 'f', 0) + + assertAppend("123456789", MustParse64("123456789"), 'g', 0) + assertAppend("1.23456789e+18", MustParse64("123456789e10"), 'g', 0) + assertAppend("1.23456789e-18", MustParse64("123456789e-26"), 'g', 0) + assertAppend("1.23456789e+18", MustParse64("123456789e10"), 'g', 0) } diff --git a/decimal64math.go b/decimal64math.go index cc993f7..b7afd4e 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -5,7 +5,7 @@ func (d Decimal64) Abs() Decimal64 { if d.IsNaN() { return d } - return Decimal64{^neg64 & uint64(d.bits)} + return Decimal64{bits: ^neg64 & uint64(d.bits)}.debug() } // Add computes d + e with default rounding @@ -35,11 +35,10 @@ func (d Decimal64) Quo(e Decimal64) Decimal64 { // Cmp returns: // -// -2 if d or e is NaN -// -1 if d < e -// 0 if d == e (incl. -0 == 0, -Inf == -Inf, and +Inf == +Inf) -// +1 if d > e -// +// -2 if d or e is NaN +// -1 if d < e +// 0 if d == e (incl. -0 == 0, -Inf == -Inf, and +Inf == +Inf) +// +1 if d > e func (d Decimal64) Cmp(e Decimal64) int { var dp decParts dp.unpack(d) @@ -48,19 +47,42 @@ func (d Decimal64) Cmp(e Decimal64) int { if _, isNan := checkNan(&dp, &ep); isNan { return -2 } - if dp.isZero() && ep.isZero() { - return 0 + return cmp(&dp, &ep) +} + +// Cmp64 returns the same output as Cmp as a Decimal64, unless d or e is NaN, in +// which case it returns a corresponding NaN result. +func (d Decimal64) Cmp64(e Decimal64) Decimal64 { + var dp decParts + dp.unpack(d) + var ep decParts + ep.unpack(e) + if n, isNan := checkNan(&dp, &ep); isNan { + return n + } + switch cmp(&dp, &ep) { + case -1: + return NegOne64 + case 1: + return One64 + default: + return Zero64 } - if d == e { +} + +func cmp(dp, ep *decParts) int { + switch { + case dp.isZero() && ep.isZero(), dp.original == ep.original: return 0 + default: + diff := dp.original.Sub(ep.original) + return 1 - 2*int(diff.bits>>63) } - d = d.Sub(e) - return 1 - 2*int(d.bits>>63) } // Neg computes -d. func (d Decimal64) Neg() Decimal64 { - return Decimal64{neg64 ^ d.bits} + return Decimal64{bits: neg64 ^ d.bits}.debug() } // Quo computes d / e. diff --git a/decimal64scan.go b/decimal64scan.go index 98ca974..ecaa624 100644 --- a/decimal64scan.go +++ b/decimal64scan.go @@ -189,9 +189,9 @@ func newPayloadNan(sign int, fl flavor, weight uint64) Decimal64 { s := uint64(sign) << 63 switch fl { case flQNaN: - return Decimal64{s | QNaN64.bits | weight} + return Decimal64{bits: s | QNaN64.bits | weight}.debug() case flSNaN: - return Decimal64{s | SNaN64.bits | weight} + return Decimal64{bits: s | SNaN64.bits | weight}.debug() default: return QNaN64 } diff --git a/decimalSuite_test.go b/decimalSuite_test.go index dad7267..c23c15b 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -5,18 +5,22 @@ import ( "fmt" "os" "regexp" + "slices" + "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -type decValContainer struct { - val1, val2, val3, expected, calculated Decimal64 - calculatedString string - parseError error +type opResult struct { + val1, val2, val3, expected, result Decimal64 + text string } -type testCaseStrings struct { - testName string - testFunc string +type testCase struct { + name string + function string val1 string val2 string val3 string @@ -24,89 +28,72 @@ type testCaseStrings struct { rounding string } -const PrintFiles bool = true -const PrintTests bool = false -const RunTests bool = true const IgnorePanics bool = false const IgnoreRounding bool = false -var tests = []string{ - "dectest/ddAdd.decTest", - "dectest/ddMultiply.decTest", - "dectest/ddFMA.decTest", - "dectest/ddClass.decTest", - // TODO: Implement following tests - "dectest/ddCompare.decTest", - "dectest/ddAbs.decTest", - // "dectest/ddCopysign.decTest", - "dectest/ddDivide.decTest", - // "dectest/ddLogB.decTest", - // "dectest/ddMin.decTest", - // "dectest/ddMinMag.decTest", - // "dectest/ddMinus.decTest", +func (testVal *testCase) String() string { + if testVal == nil { + return "nil" + } + return fmt.Sprintf("%s %s (%v, %v, %v) -> %v", testVal.name, testVal.function, testVal.val1, testVal.val2, testVal.val3, testVal.expectedResult) } -func (testVal testCaseStrings) String() string { - return fmt.Sprintf("%s %s %v %v %v -> %v\n", testVal.testName, testVal.testFunc, testVal.val1, testVal.val2, testVal.val3, testVal.expectedResult) +type set[K comparable] map[K]struct{} + +func (s set[K]) Has(k K) bool { + _, ok := s[k] + return ok } -var supportedRounding = []string{"half_up", "half_even"} -var ignoredFunctions = []string{"apply"} +var supportedRounding = set[string]{"half_up": {}, "half_even": {}} +var ignoredFunctions = set[string]{"apply": {}} -// TODO(joshcarp): This test cannot fail. Proper assertions will be added once the whole suite passes // TestFromSuite is the master tester for the dectest suite. func TestFromSuite(t *testing.T) { - if RunTests { - for _, file := range tests { - if PrintFiles { - fmt.Println("starting test:", file) - } + for _, file := range []string{ + "dectest/ddAdd.decTest", + "dectest/ddMultiply.decTest", + "dectest/ddFMA.decTest", + "dectest/ddClass.decTest", + // TODO: Implement following tests + "dectest/ddCompare.decTest", + "dectest/ddAbs.decTest", + // "dectest/ddCopysign.decTest", + "dectest/ddDivide.decTest", + // "dectest/ddLogB.decTest", + // "dectest/ddMin.decTest", + // "dectest/ddMinMag.decTest", + // "dectest/ddMinus.decTest", + } { + t.Run(file, func(t *testing.T) { f, _ := os.Open(file) scanner := bufio.NewScanner(f) numTests := 0 - failedTests := 0 var roundingSupported bool var scannedContext Context64 for scanner.Scan() { testVal := getInput(scanner.Text()) + if testVal == nil { + continue + } if testVal.rounding != "" { - roundingSupported = isInList(testVal.rounding, supportedRounding) + roundingSupported = supportedRounding.Has(testVal.rounding) if roundingSupported { scannedContext = setRoundingFromString(testVal.rounding) } } - if testVal.testFunc != "" && roundingSupported { + if testVal.function != "" && roundingSupported { numTests++ - dec64vals := convertToDec64(testVal) - testErr := runTest(scannedContext, dec64vals, testVal) - if PrintTests { - fmt.Printf("%s\n", testVal) - } - if testErr != nil { - fmt.Println(testErr) - fmt.Println("Rounding mode:", supportedRounding[scannedContext.roundingMode]) - failedTests++ - if dec64vals.parseError != nil { - fmt.Println(dec64vals.parseError) - } - } + t.Run(testVal.name, func(t *testing.T) { + dec64vals, err := convertToDec64(testVal) + require.NoError(t, err) + runTest(t, scannedContext, dec64vals, testVal) + }) } } - if PrintFiles { - fmt.Println("Number of tests ran:", numTests, "Number of failed tests:", failedTests) - } - } - fmt.Printf("decimalSuite_test settings (These should only be true for debug):\n Ignore Rounding errors: %v\n Ignore Panics: %v\n", IgnoreRounding, IgnorePanics) + }) } -} - -func isInList(s string, list []string) bool { - for _, item := range list { - if item == s { - return true - } - } - return false + fmt.Printf("decimalSuite_test settings (These should only be true for debug):\n Ignore Rounding errors: %v\n Ignore Panics: %v\n", IgnoreRounding, IgnorePanics) } func setRoundingFromString(s string) Context64 { @@ -120,8 +107,8 @@ func setRoundingFromString(s string) Context64 { default: panic("Rounding not supported" + s) } - } + func isRoundingErr(res, expected Decimal64) bool { var resP decParts resP.unpack(res) @@ -138,104 +125,124 @@ func isRoundingErr(res, expected Decimal64) bool { return false } -// getInput gets the test file and extracts test using regex, then returns a map object and a list of test names. -func getInput(line string) testCaseStrings { - testRegex := regexp.MustCompile( - `(?Pdd[\w]*)` + // first capturing group: testfunc made of anything that isn't a whitespace - `(?:\s*)` + // match any whitespace (?: non capturing group) - `(?P[\S]*)` + // testfunc made of anything that isn't a whitespace - `(?:\s*\'?)` + // after can be any number of spaces and quotations if they exist (?: non capturing group) - `(?P\+?-?[^\t\f\v\' ]*)` + // first test val is anything that isnt a whitespace or a quoteation mark - `(?:'?\s*'?)` + // match any quotation marks and any space (?: non capturing group) - `(?P\+?-?[^\t\f\v\' ]*)` + // second test val is anything that isnt a whitespace or a quoteation mark - `(?:'?\s*'?)` + - `(?P\+?-?[^->]?[^\t\f\v\' ]*)` + //testvals3 same as 1 but specifically dont match with '->' - `(?:'?\s*->\s*'?)` + // matches the indicator to answer and surrounding whitespaces (?: non capturing group) - `(?P\+?-?[^\r\n\t\f\v\' ]*)`) // matches the answer that's anything that is plus minus but not quotations - - // Add regex to match to rounding: rounding mode her +var ( + testRegex = regexp.MustCompile(`'((?:''+|[^'])*)'|(\S+)`) + roundingRegex = regexp.MustCompile(`(?:rounding:[\s]*)(?P[\S]*)`) +) - // capturing gorups are testName, testFunc, val1, val2, and expectedResult) - ans := testRegex.FindStringSubmatch(line) +// getInput gets the test file and extracts test using regex, then returns a map object and a list of test names. +func getInput(line string) *testCase { + // TODO: Figure out what this comment means. + // Add regex to match to rounding: rounding mode here - if len(ans) == 0 { - roundingRegex := regexp.MustCompile(`(?:rounding:[\s]*)(?P[\S]*)`) - ans = roundingRegex.FindStringSubmatch(line) - if len(ans) == 0 { - return testCaseStrings{} + m := testRegex.FindAllStringSubmatch(line, -1) + if m == nil || !strings.HasPrefix(m[0][2], "dd") { + m := roundingRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + return &testCase{rounding: m[1]} + } + fields := make([]string, 0, len(m)) + for _, f := range m { + fields = append(fields, strings.ReplaceAll(f[1], "''", "'")+f[2]) + } + if i := slices.Index(fields, "->"); i < 5 { + if i == -1 { + panic(fmt.Errorf("malformed input: %s", line)) + } + head, tail := fields[:i], fields[i:] + for ; i < 5; i++ { + head = append(append([]string{}, head...), "") } - return testCaseStrings{rounding: ans[1]} + fields = append(head, tail...) + } + test := &testCase{ + name: fields[0], + function: fields[1], + val1: fields[2], + val2: fields[3], + val3: fields[4], + expectedResult: fields[6], // field[6] == "->" + } + if ignoredFunctions.Has(test.function) { + return nil } - if isInList(ans[2], ignoredFunctions) { - return testCaseStrings{} + if test.val1 == "#" { + test.val1 = "" } - data := testCaseStrings{ - testName: ans[1], - testFunc: ans[2], - val1: ans[3], - val2: ans[4], - val3: ans[5], - expectedResult: ans[6], + if test.val2 == "#" { + test.val2 = "" } - return data + return test } // convertToDec64 converts the map object strings to decimal64s. -func convertToDec64(testvals testCaseStrings) (dec64vals decValContainer) { - var err1, err2, err3, expectedErr error - dec64vals.val1, err1 = Parse64(testvals.val1) - dec64vals.val2, err2 = Parse64(testvals.val2) - dec64vals.val3, err3 = Parse64(testvals.val3) - dec64vals.expected, expectedErr = Parse64(testvals.expectedResult) - - if err1 != nil || err2 != nil || expectedErr != nil { - dec64vals.parseError = fmt.Errorf("error parsing in test: %s: \nval 1:%s: \nval 2: %s \nval 3: %s\nexpected: %s ", - testvals.testName, - err1, - err2, - err3, - expectedErr) +func convertToDec64(testvals *testCase) (opResult, error) { + var r opResult + var err error + parseNotEmpty := func(s string) (Decimal64, error) { + if s == "" { + return QNaN64, nil + } + return Parse64(s) + } + r.val1, err = parseNotEmpty(testvals.val1) + if err != nil { + return opResult{}, fmt.Errorf("error parsing val1: %w", err) } - return + r.val2, err = parseNotEmpty(testvals.val2) + if err != nil { + return opResult{}, fmt.Errorf("error parsing val2: %w", err) + } + r.val3, err = parseNotEmpty(testvals.val3) + if err != nil { + return opResult{}, fmt.Errorf("error parsing val3: %w", err) + } + if textResults.Has(testvals.function) { + r.text = testvals.expectedResult + } else { + r.expected, err = parseNotEmpty(testvals.expectedResult) + if err != nil { + return opResult{}, fmt.Errorf("error parsing expected: %w", err) + } + } + return r, nil } // runTest completes the tests and returns a boolean and string on if the test passes. -func runTest(context Context64, testVals decValContainer, testValStrings testCaseStrings) error { - calculatedContainer := execOp(context, testVals.val1, testVals.val2, testVals.val3, testValStrings.testFunc) - calcRestul := calculatedContainer.calculated +func runTest(t *testing.T, context Context64, expected opResult, testValStrings *testCase) bool { + actual := execOp(context, expected.val1, expected.val2, expected.val3, testValStrings.function) - if calculatedContainer.calculatedString != "" { - if testValStrings.testFunc == "compare" && calculatedContainer.calculatedString == "-2" && testVals.expected.IsNaN() { - return nil + if actual.text != "" { + if testValStrings.function == "compare" && actual.text == "-2" && expected.expected.IsNaN() { + return true } - if calculatedContainer.calculatedString != testValStrings.expectedResult { - return fmt.Errorf( - "failed:\n%scalculated result: %s", - testValStrings, - calculatedContainer.calculatedString) + if actual.text != testValStrings.expectedResult { + return assert.Failf(t, "unexpected result", "test:\n%s\ncalculated text: %s", testValStrings, actual.text) } - - } else if calcRestul.IsNaN() || testVals.expected.IsNaN() { - - if testVals.expected.String() != calcRestul.String() { - return fmt.Errorf( - "failed NaN TEST:\n%scalculated result: %v", - testValStrings, - calcRestul) + return true + } + if actual.result.IsNaN() || expected.expected.IsNaN() { + if expected.expected.String() != actual.result.String() { + return assert.Failf(t, "failed NaN test", "test:\n%s\ncalculated result: %v", testValStrings, actual.result) } - return nil - } else if testVals.expected.Cmp(calcRestul) != 0 && !(isRoundingErr(calcRestul, testVals.expected) && IgnoreRounding) { - return fmt.Errorf( - "failed:\n%scalculated result: %v", + return true + } + if expected.expected.Cmp(actual.result) != 0 && !(isRoundingErr(actual.result, expected.expected) && IgnoreRounding) { + return assert.Fail(t, + "failed", "test:\n%s\ncalculated result: %v", testValStrings, - calcRestul) + actual.result) } - return nil + return true } +var textResults = set[string]{"class": {}} + // TODO: get runTest to run more functions such as FMA. // execOp returns the calculated answer to the operation as Decimal64. -func execOp(context Context64, a, b, c Decimal64, op string) decValContainer { +func execOp(context Context64, a, b, c Decimal64, op string) opResult { if IgnorePanics { defer func() { if r := recover(); r != nil { @@ -245,21 +252,21 @@ func execOp(context Context64, a, b, c Decimal64, op string) decValContainer { } switch op { case "add": - return decValContainer{calculated: context.Add(a, b)} + return opResult{result: context.Add(a, b)} case "multiply": - return decValContainer{calculated: context.Mul(a, b)} + return opResult{result: context.Mul(a, b)} case "abs": - return decValContainer{calculated: a.Abs()} + return opResult{result: a.Abs()} case "divide": - return decValContainer{calculated: context.Quo(a, b)} + return opResult{result: context.Quo(a, b)} case "fma": - return decValContainer{calculated: context.FMA(a, b, c)} + return opResult{result: context.FMA(a, b, c)} case "compare": - return decValContainer{calculatedString: fmt.Sprintf("%d", int64(a.Cmp(b)))} + return opResult{result: a.Cmp64(b)} case "class": - return decValContainer{calculatedString: a.Class()} + return opResult{text: a.Class()} default: fmt.Println("end of execOp, no tests ran", op) } - return decValContainer{calculated: Zero64} + return opResult{result: Zero64} } diff --git a/dectest/ddFMA.decTest b/dectest/ddFMA.decTest index f0074e8..e9a7426 100644 --- a/dectest/ddFMA.decTest +++ b/dectest/ddFMA.decTest @@ -1473,14 +1473,7 @@ ddfma371459 fma 1 1.123456789012345E-19 0 -> 1.123456789012345E-19 -- same, Es on the 0 ddfma371460 fma 1 1.123456789012345 0E-0 -> 1.123456789012345 ddfma371461 fma 1 1.123456789012345 0E-1 -> 1.123456789012345 -ddfma371462 fma 1 1.123456789012345 } - -const TESTDEBUG bool = true -const PRINTTESTS bool = true -const PRINTTESTS bool = false -const RUNSUITES bool = true - -var tests = []string{"",0E-2 -> 1.123456789012345 +ddfma371462 fma 1 1.123456789012345 0E-2 -> 1.123456789012345 ddfma371463 fma 1 1.123456789012345 0E-3 -> 1.123456789012345 ddfma371464 fma 1 1.123456789012345 0E-4 -> 1.123456789012345 ddfma371465 fma 1 1.123456789012345 0E-5 -> 1.123456789012345 diff --git a/go.mod b/go.mod index 8a1073d..11de83d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module github.com/anz-bank/decimal +go 1.18 + +require github.com/stretchr/testify v1.2.2 + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 - golang.org/x/tools v0.0.0-20190419195823-c39e7748f6eb // indirect ) diff --git a/go.sum b/go.sum index 5b340eb..e03ee77 100644 --- a/go.sum +++ b/go.sum @@ -4,9 +4,3 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190419195823-c39e7748f6eb h1:JbWwiXQ1L1jWKTGSwj6y63WT+bESGWOhXY8xoAs0yoo= -golang.org/x/tools v0.0.0-20190419195823-c39e7748f6eb/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= From b52d4f464711d919ae491df479a5cdefc4265ffc Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:37:41 +1000 Subject: [PATCH 02/17] Test suite cases for Decimal64.Neg passing. --- decimal64math.go | 3 +++ decimalSuite_test.go | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/decimal64math.go b/decimal64math.go index b7afd4e..a443f0f 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -82,6 +82,9 @@ func cmp(dp, ep *decParts) int { // Neg computes -d. func (d Decimal64) Neg() Decimal64 { + if d.IsNaN() { + return d + } return Decimal64{bits: neg64 ^ d.bits}.debug() } diff --git a/decimalSuite_test.go b/decimalSuite_test.go index c23c15b..a2c23ad 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -14,8 +14,8 @@ import ( ) type opResult struct { - val1, val2, val3, expected, result Decimal64 - text string + val1, val2, val3, result Decimal64 + text string } type testCase struct { @@ -63,7 +63,7 @@ func TestFromSuite(t *testing.T) { // "dectest/ddLogB.decTest", // "dectest/ddMin.decTest", // "dectest/ddMinMag.decTest", - // "dectest/ddMinus.decTest", + "dectest/ddMinus.decTest", } { t.Run(file, func(t *testing.T) { f, _ := os.Open(file) @@ -202,7 +202,7 @@ func convertToDec64(testvals *testCase) (opResult, error) { if textResults.Has(testvals.function) { r.text = testvals.expectedResult } else { - r.expected, err = parseNotEmpty(testvals.expectedResult) + r.result, err = parseNotEmpty(testvals.expectedResult) if err != nil { return opResult{}, fmt.Errorf("error parsing expected: %w", err) } @@ -210,12 +210,12 @@ func convertToDec64(testvals *testCase) (opResult, error) { return r, nil } -// runTest completes the tests and returns a boolean and string on if the test passes. +// runTest completes the tests and compares actual and expected results. func runTest(t *testing.T, context Context64, expected opResult, testValStrings *testCase) bool { actual := execOp(context, expected.val1, expected.val2, expected.val3, testValStrings.function) if actual.text != "" { - if testValStrings.function == "compare" && actual.text == "-2" && expected.expected.IsNaN() { + if testValStrings.function == "compare" && actual.text == "-2" && expected.result.IsNaN() { return true } if actual.text != testValStrings.expectedResult { @@ -223,13 +223,15 @@ func runTest(t *testing.T, context Context64, expected opResult, testValStrings } return true } - if actual.result.IsNaN() || expected.expected.IsNaN() { - if expected.expected.String() != actual.result.String() { + if actual.result.IsNaN() || expected.result.IsNaN() { + e := expected.result.String() + a := actual.result.String() + if e != a { return assert.Failf(t, "failed NaN test", "test:\n%s\ncalculated result: %v", testValStrings, actual.result) } return true } - if expected.expected.Cmp(actual.result) != 0 && !(isRoundingErr(actual.result, expected.expected) && IgnoreRounding) { + if expected.result.Cmp(actual.result) != 0 && !(isRoundingErr(actual.result, expected.result) && IgnoreRounding) { return assert.Fail(t, "failed", "test:\n%s\ncalculated result: %v", testValStrings, @@ -257,6 +259,8 @@ func execOp(context Context64, a, b, c Decimal64, op string) opResult { return opResult{result: context.Mul(a, b)} case "abs": return opResult{result: a.Abs()} + case "minus": + return opResult{result: a.Neg()} case "divide": return opResult{result: context.Quo(a, b)} case "fma": From eddae4e7cf595bd5554384860603b3be2375f43e Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:55:59 +1000 Subject: [PATCH 03/17] Decimal64.Min and Decimal64.MinMag --- decimal64.go | 16 ++++------- decimal64decParts.go | 17 +++++++++++- decimal64math.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ decimalSuite_test.go | 34 ++++++++--------------- 4 files changed, 98 insertions(+), 34 deletions(-) diff --git a/decimal64.go b/decimal64.go index e20b408..14d3656 100644 --- a/decimal64.go +++ b/decimal64.go @@ -17,7 +17,7 @@ const ( type flavor int const ( - flNormal flavor = iota + flNormal flavor = 1 << iota flInf flQNaN flSNaN @@ -39,15 +39,6 @@ type Decimal64 struct { debugInfo //nolint:unused } -// decParts stores the constituting decParts of a decimal64. -type decParts struct { - fl flavor - sign int - exp int - significand uint128T - original Decimal64 -} - // Context64 stores the rounding type for arithmetic operations. type Context64 struct { roundingMode roundingMode @@ -364,6 +355,11 @@ func (d Decimal64) IsInt() bool { } } +// quiet returns a quiet form of d, which must be a NaN. +func (d Decimal64) quiet() Decimal64 { + return Decimal64{bits: d.bits &^ (2 << 56)} +} + // IsSubnormal returns true iff d is a subnormal. func (d Decimal64) IsSubnormal() bool { flav, _, _, significand := d.parts() diff --git a/decimal64decParts.go b/decimal64decParts.go index c51f56c..7183b6c 100644 --- a/decimal64decParts.go +++ b/decimal64decParts.go @@ -1,5 +1,14 @@ package decimal +// decParts stores the constituting decParts of a decimal64. +type decParts struct { + fl flavor + sign int + exp int + significand uint128T + original Decimal64 +} + // add128 adds two decParts with full precision in 128 bits of significand func (dp *decParts) add128(ep *decParts) decParts { dp.matchScales128(ep) @@ -67,7 +76,7 @@ func (dp *decParts) isInf() bool { } func (dp *decParts) isNaN() bool { - return dp.fl == flQNaN || dp.fl == flSNaN + return dp.fl&(flQNaN|flSNaN) != 0 } func (dp *decParts) isQNaN() bool { @@ -78,6 +87,10 @@ func (dp *decParts) isSNaN() bool { return dp.fl == flSNaN } +func (dp *decParts) nanWeight() int { + return int(dp.significand.lo) +} + func (dp *decParts) isSubnormal() bool { return (dp.significand != uint128T{}) && dp.significand.lo < decimal64Base && dp.fl == flNormal } @@ -123,9 +136,11 @@ func (dp *decParts) unpack(d Decimal64) { dp.fl = flInf case 2: dp.fl = flQNaN + dp.significand.lo = d.bits & (1<<51 - 1) // Payload return case 3: dp.fl = flSNaN + dp.significand.lo = d.bits & (1<<51 - 1) // Payload return } case 12, 13, 14: diff --git a/decimal64math.go b/decimal64math.go index a443f0f..ae8bb7c 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -80,6 +80,71 @@ func cmp(dp, ep *decParts) int { } } +// Min returns the lower of d and e. +func (d Decimal64) Min(e Decimal64) Decimal64 { + var dp decParts + dp.unpack(d) + var ep decParts + ep.unpack(e) + + dnan := dp.isNaN() + enan := ep.isNaN() + + switch { + case !dnan && !enan: // Fast path for non-NaNs. + if cmp(&dp, &ep) < 0 { + return d + } + return e + + case dp.isSNaN(): + return d.quiet() + case ep.isSNaN(): + return e.quiet() + + case !enan: + return e + default: + return d + } +} + +// MinMag returns the lower of d and e. +func (d Decimal64) MinMag(e Decimal64) Decimal64 { + var dp decParts + dp.unpack(d.Abs()) + var ep decParts + ep.unpack(e.Abs()) + + dnan := dp.isNaN() + enan := ep.isNaN() + + switch { + case !dnan && !enan: // Fast path for non-NaNs. + switch cmp(&dp, &ep) { + case -1: + return d + case 1: + return e + default: + if d.bits&neg64 != 0 { + return d + } + return e + } + + case dp.isSNaN(): + return d.quiet() + case ep.isSNaN(): + return e.quiet() + + case !enan: + return e + default: + return d + } +} + // Neg computes -d. func (d Decimal64) Neg() Decimal64 { if d.IsNaN() { diff --git a/decimalSuite_test.go b/decimalSuite_test.go index a2c23ad..a38cd68 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -28,9 +28,6 @@ type testCase struct { rounding string } -const IgnorePanics bool = false -const IgnoreRounding bool = false - func (testVal *testCase) String() string { if testVal == nil { return "nil" @@ -61,8 +58,8 @@ func TestFromSuite(t *testing.T) { // "dectest/ddCopysign.decTest", "dectest/ddDivide.decTest", // "dectest/ddLogB.decTest", - // "dectest/ddMin.decTest", - // "dectest/ddMinMag.decTest", + "dectest/ddMin.decTest", + "dectest/ddMinMag.decTest", "dectest/ddMinus.decTest", } { t.Run(file, func(t *testing.T) { @@ -93,7 +90,6 @@ func TestFromSuite(t *testing.T) { } }) } - fmt.Printf("decimalSuite_test settings (These should only be true for debug):\n Ignore Rounding errors: %v\n Ignore Panics: %v\n", IgnoreRounding, IgnorePanics) } func setRoundingFromString(s string) Context64 { @@ -168,11 +164,9 @@ func getInput(line string) *testCase { if ignoredFunctions.Has(test.function) { return nil } - if test.val1 == "#" { - test.val1 = "" - } - if test.val2 == "#" { - test.val2 = "" + // # represents a null value, which isn't meaningful for this package. + if test.val1 == "#" || test.val2 == "#" { + return nil } return test } @@ -231,11 +225,8 @@ func runTest(t *testing.T, context Context64, expected opResult, testValStrings } return true } - if expected.result.Cmp(actual.result) != 0 && !(isRoundingErr(actual.result, expected.result) && IgnoreRounding) { - return assert.Fail(t, - "failed", "test:\n%s\ncalculated result: %v", - testValStrings, - actual.result) + if expected.result.Cmp(actual.result) != 0 { + return assert.Fail(t, "failed", "test:\n%s\ncalculated result: %v", testValStrings, actual.result) } return true } @@ -245,13 +236,6 @@ var textResults = set[string]{"class": {}} // TODO: get runTest to run more functions such as FMA. // execOp returns the calculated answer to the operation as Decimal64. func execOp(context Context64, a, b, c Decimal64, op string) opResult { - if IgnorePanics { - defer func() { - if r := recover(); r != nil { - fmt.Println("failed", r, a, b) - } - }() - } switch op { case "add": return opResult{result: context.Add(a, b)} @@ -267,6 +251,10 @@ func execOp(context Context64, a, b, c Decimal64, op string) opResult { return opResult{result: context.FMA(a, b, c)} case "compare": return opResult{result: a.Cmp64(b)} + case "min": + return opResult{result: a.Min(b)} + case "minmag": + return opResult{result: a.MinMag(b)} case "class": return opResult{text: a.Class()} default: From e8e8e7bc6206fd8078ee18c293445ce5e71bb856 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:06:59 +1000 Subject: [PATCH 04/17] Decimal64.Max/MagMag + more tests --- decimal64math.go | 56 ++++++++++++++++++++++++++++++--- decimalSuite_test.go | 73 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/decimal64math.go b/decimal64math.go index ae8bb7c..1ad6cba 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -82,6 +82,16 @@ func cmp(dp, ep *decParts) int { // Min returns the lower of d and e. func (d Decimal64) Min(e Decimal64) Decimal64 { + return d.min(e, 1) +} + +// Max returns the lower of d and e. +func (d Decimal64) Max(e Decimal64) Decimal64 { + return d.min(e, -1) +} + +// Min returns the lower of d and e. +func (d Decimal64) min(e Decimal64, sign int) Decimal64 { var dp decParts dp.unpack(d) var ep decParts @@ -92,7 +102,7 @@ func (d Decimal64) Min(e Decimal64) Decimal64 { switch { case !dnan && !enan: // Fast path for non-NaNs. - if cmp(&dp, &ep) < 0 { + if sign*cmp(&dp, &ep) < 0 { return d } return e @@ -111,6 +121,16 @@ func (d Decimal64) Min(e Decimal64) Decimal64 { // MinMag returns the lower of d and e. func (d Decimal64) MinMag(e Decimal64) Decimal64 { + return d.minMag(e, 1) +} + +// MaxMag returns the lower of d and e. +func (d Decimal64) MaxMag(e Decimal64) Decimal64 { + return d.minMag(e, -1) +} + +// MinMag returns the lower of d and e. +func (d Decimal64) minMag(e Decimal64, sign int) Decimal64 { var dp decParts dp.unpack(d.Abs()) var ep decParts @@ -121,23 +141,21 @@ func (d Decimal64) MinMag(e Decimal64) Decimal64 { switch { case !dnan && !enan: // Fast path for non-NaNs. - switch cmp(&dp, &ep) { + switch sign * cmp(&dp, &ep) { case -1: return d case 1: return e default: - if d.bits&neg64 != 0 { + if 2*int(d.bits>>63) == 1+sign { return d } return e } - case dp.isSNaN(): return d.quiet() case ep.isSNaN(): return e.quiet() - case !enan: return e default: @@ -153,6 +171,34 @@ func (d Decimal64) Neg() Decimal64 { return Decimal64{bits: neg64 ^ d.bits}.debug() } +// Logb return the integral log10 of d. +func (d Decimal64) Logb() Decimal64 { + switch { + case d.IsNaN(): + return d + case d.IsZero(): + return NegInfinity64 + case d.IsInf(): + return Infinity64 + default: + var dp decParts + dp.unpack(d) + + // Adjust for subnormals. + e := dp.exp + for s := dp.significand.lo; s < decimal64Base; s *= 10 { + e-- + } + + return New64FromInt64(int64(15 + e)) + } +} + +// CopySign copies d, but with the sign taken from e. +func (d Decimal64) CopySign(e Decimal64) Decimal64 { + return Decimal64{bits: d.bits&^neg64 | e.bits&neg64} +} + // Quo computes d / e. func (ctx Context64) Quo(d, e Decimal64) Decimal64 { var dp decParts diff --git a/decimalSuite_test.go b/decimalSuite_test.go index a38cd68..740bf07 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -47,22 +47,60 @@ var ignoredFunctions = set[string]{"apply": {}} // TestFromSuite is the master tester for the dectest suite. func TestFromSuite(t *testing.T) { + t.Parallel() + for _, file := range []string{ + "dectest/ddAbs.decTest", "dectest/ddAdd.decTest", - "dectest/ddMultiply.decTest", - "dectest/ddFMA.decTest", "dectest/ddClass.decTest", - // TODO: Implement following tests "dectest/ddCompare.decTest", - "dectest/ddAbs.decTest", - // "dectest/ddCopysign.decTest", + "dectest/ddCopysign.decTest", "dectest/ddDivide.decTest", - // "dectest/ddLogB.decTest", + "dectest/ddFMA.decTest", + "dectest/ddLogB.decTest", + "dectest/ddMax.decTest", + "dectest/ddMaxMag.decTest", "dectest/ddMin.decTest", "dectest/ddMinMag.decTest", "dectest/ddMinus.decTest", + "dectest/ddMultiply.decTest", + "dectest/ddPlus.decTest", + "dectest/ddSubtract.decTest", + + // Future + // "dectest/ddBase.decTest", + // "dectest/ddNextMinus.decTest", + // "dectest/ddNextPlus.decTest", + // "dectest/ddNextToward.decTest", + // "dectest/ddRemainder.decTest", + // "dectest/ddRemainderNear.decTest", + // "dectest/ddScaleB.decTest", + // "dectest/ddQuantize.decTest", + // "dectest/ddToIntegral.decTest", + + // Not planned + // "dectest/ddAnd.decTest", + // "dectest/ddCanonical.decTest", + // "dectest/ddCompareSig.decTest", + // "dectest/ddCompareTotal.decTest", + // "dectest/ddCompareTotalMag.decTest", + // "dectest/ddCopy.decTest", + // "dectest/ddCopyAbs.decTest", + // "dectest/ddCopyNegate.decTest", + // "dectest/ddDivideInt.decTest", + // "dectest/ddEncode.decTest", + // "dectest/ddInvert.decTest", + // "dectest/ddOr.decTest", + // "dectest/ddReduce.decTest", + // "dectest/ddRotate.decTest", + // "dectest/ddSameQuantum.decTest", + // "dectest/ddShift.decTest", + // "dectest/ddXor.decTest", } { + file := file t.Run(file, func(t *testing.T) { + t.Parallel() + f, _ := os.Open(file) scanner := bufio.NewScanner(f) numTests := 0 @@ -243,22 +281,33 @@ func execOp(context Context64, a, b, c Decimal64, op string) opResult { return opResult{result: context.Mul(a, b)} case "abs": return opResult{result: a.Abs()} - case "minus": - return opResult{result: a.Neg()} + case "compare": + return opResult{result: a.Cmp64(b)} + case "copysign": + return opResult{result: a.CopySign(b)} case "divide": return opResult{result: context.Quo(a, b)} case "fma": return opResult{result: context.FMA(a, b, c)} - case "compare": - return opResult{result: a.Cmp64(b)} + case "logb": + return opResult{result: a.Logb()} + case "max": + return opResult{result: a.Max(b)} + case "maxmag": + return opResult{result: a.MaxMag(b)} case "min": return opResult{result: a.Min(b)} case "minmag": return opResult{result: a.MinMag(b)} + case "minus": + return opResult{result: a.Neg()} + case "plus": + return opResult{result: a} + case "subtract": + return opResult{result: context.Add(a, b.Neg())} case "class": return opResult{text: a.Class()} default: - fmt.Println("end of execOp, no tests ran", op) + panic(fmt.Errorf("unhandled op: %s", op)) } - return opResult{result: Zero64} } From 9c57fe3152883421b5878f0db616de065f5cfa25 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:33:06 +1000 Subject: [PATCH 05/17] Decimal.NextPlus/NextMinus with suite tests --- decimal64const.go | 3 ++ decimal64fmt.go | 83 ++++++++++++++++++++++++++++++++++++-------- decimal64fmt_test.go | 3 +- decimalSuite_test.go | 8 +++-- 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/decimal64const.go b/decimal64const.go index b885d3e..5791afa 100644 --- a/decimal64const.go +++ b/decimal64const.go @@ -52,6 +52,9 @@ var NegMax64 = newFromParts(1, expMax, maxSig) // Min64 is the smallest number that is subnormal possible with Decimal64. var Min64 = newFromParts(0, -398, 1) +// Min64 is the smallest number that is subnormal possible with Decimal64. +var NegMin64 = newFromParts(1, -398, 1) + var zeroes = []Decimal64{Zero64, NegZero64} var infinities = []Decimal64{Infinity64, NegInfinity64} diff --git a/decimal64fmt.go b/decimal64fmt.go index 1991c7a..17f89ee 100644 --- a/decimal64fmt.go +++ b/decimal64fmt.go @@ -185,23 +185,76 @@ func (d Decimal64) Text(format byte, prec int) string { return string(d.Append(make([]byte, 0, 16), format, prec)) } -// RoundHalfAwayFromZero returns a Decimal64 with the smallest possible -// increment applied to the significand. -// -// The default behaviour when formatting Decimal64 is to use half-even rounding, -// which rounds the last digit away from zero if it is odd or leaves it as is if -// it is even. -// This function changes the rounding behaviour such that the last formatted -// digit will always round away from zero when the next digit is a 5. -// The downside is that the number just before a half might round up, but this -// very unlikely since halves are far more likely that almost halves. -func (d Decimal64) RoundHalfAwayFromZero() Decimal64 { +// NextPlus returns the next value above d. +func (d Decimal64) NextPlus() Decimal64 { flav, sign, exp, significand := d.parts() - if flav != flNormal { + switch { + case flav == flInf: + if sign == 1 { + return NegMax64 + } + return Infinity64 + case flav != flNormal: return d + case significand == 0: + return Min64 + case sign == 1: + switch { + case significand > decimal64Base: + return Decimal64{bits: d.bits - 1}.debug() + case exp == -398: + if significand > 1 { + return Decimal64{bits: d.bits - 1}.debug() + } + return Zero64 + default: + return newFromParts(sign, exp-1, 10*decimal64Base-1) + } + default: + switch { + case significand < 10*decimal64Base-1: + return Decimal64{bits: d.bits + 1}.debug() + case exp == 369: + return Infinity64 + default: + return newFromParts(sign, exp+1, decimal64Base) + } } - if significand < 10*decimal64Base-1 { - return Decimal64{bits: d.bits + 1}.debug() +} + +// NextMinus returns the next value above d. +func (d Decimal64) NextMinus() Decimal64 { + flav, sign, exp, significand := d.parts() + switch { + case flav == flInf: + if sign == 0 { + return Max64 + } + return NegInfinity64 + case flav != flNormal: + return d + case significand == 0: + return NegMin64 + case sign == 0: + switch { + case significand > decimal64Base: + return Decimal64{bits: d.bits - 1}.debug() + case exp == -398: + if significand > 1 { + return Decimal64{bits: d.bits - 1}.debug() + } + return Zero64 + default: + return newFromParts(sign, exp-1, 10*decimal64Base-1) + } + default: + switch { + case significand < 10*decimal64Base-1: + return Decimal64{bits: d.bits + 1}.debug() + case exp == 369: + return NegInfinity64 + default: + return newFromParts(sign, exp+1, decimal64Base) + } } - return newFromParts(sign, exp+1, decimal64Base) } diff --git a/decimal64fmt_test.go b/decimal64fmt_test.go index 78d67f3..8eabb1d 100644 --- a/decimal64fmt_test.go +++ b/decimal64fmt_test.go @@ -178,7 +178,8 @@ func TestDecimal64FormatPrecEdgeCasesHalfAway(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) { n, err := Parse64(test.input) require.NoError(t, err) - assert.Equal(t, test.expected, fmt.Sprintf("%.3f", n.RoundHalfAwayFromZero().Float64())) + n = n.Abs().NextPlus().CopySign(n) + assert.Equal(t, test.expected, fmt.Sprintf("%.3f", n.Float64())) }) } } diff --git a/decimalSuite_test.go b/decimalSuite_test.go index 740bf07..4807e5e 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -64,13 +64,13 @@ func TestFromSuite(t *testing.T) { "dectest/ddMinMag.decTest", "dectest/ddMinus.decTest", "dectest/ddMultiply.decTest", + "dectest/ddNextMinus.decTest", + "dectest/ddNextPlus.decTest", "dectest/ddPlus.decTest", "dectest/ddSubtract.decTest", // Future // "dectest/ddBase.decTest", - // "dectest/ddNextMinus.decTest", - // "dectest/ddNextPlus.decTest", // "dectest/ddNextToward.decTest", // "dectest/ddRemainder.decTest", // "dectest/ddRemainderNear.decTest", @@ -301,6 +301,10 @@ func execOp(context Context64, a, b, c Decimal64, op string) opResult { return opResult{result: a.MinMag(b)} case "minus": return opResult{result: a.Neg()} + case "nextminus": + return opResult{result: a.NextMinus()} + case "nextplus": + return opResult{result: a.NextPlus()} case "plus": return opResult{result: a} case "subtract": From c1431ce01dfeedd55acb08f82b4a14b444db3b24 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:47:57 +1000 Subject: [PATCH 06/17] fix MarshalText and relocate NextPlus and NextMinus --- decimal64const.go | 4 +-- decimal64fmt.go | 74 ---------------------------------------- decimal64gob.go | 13 ++++--- decimal64json.go | 7 +++- decimal64marshal.go | 28 +++++++++++++-- decimal64marshal_test.go | 9 +++-- decimal64math.go | 74 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 90 deletions(-) diff --git a/decimal64const.go b/decimal64const.go index 5791afa..6527993 100644 --- a/decimal64const.go +++ b/decimal64const.go @@ -49,10 +49,10 @@ var Max64 = newFromParts(0, expMax, maxSig) // NegMax64 is the minimum finite number (most negative) possible with Decimal64 (negative). var NegMax64 = newFromParts(1, expMax, maxSig) -// Min64 is the smallest number that is subnormal possible with Decimal64. +// Min64 is the closest positive number to zero. var Min64 = newFromParts(0, -398, 1) -// Min64 is the smallest number that is subnormal possible with Decimal64. +// Min64 is the closest negative number to zero. var NegMin64 = newFromParts(1, -398, 1) var zeroes = []Decimal64{Zero64, NegZero64} diff --git a/decimal64fmt.go b/decimal64fmt.go index 17f89ee..1cb3e63 100644 --- a/decimal64fmt.go +++ b/decimal64fmt.go @@ -184,77 +184,3 @@ func (d Decimal64) String() string { func (d Decimal64) Text(format byte, prec int) string { return string(d.Append(make([]byte, 0, 16), format, prec)) } - -// NextPlus returns the next value above d. -func (d Decimal64) NextPlus() Decimal64 { - flav, sign, exp, significand := d.parts() - switch { - case flav == flInf: - if sign == 1 { - return NegMax64 - } - return Infinity64 - case flav != flNormal: - return d - case significand == 0: - return Min64 - case sign == 1: - switch { - case significand > decimal64Base: - return Decimal64{bits: d.bits - 1}.debug() - case exp == -398: - if significand > 1 { - return Decimal64{bits: d.bits - 1}.debug() - } - return Zero64 - default: - return newFromParts(sign, exp-1, 10*decimal64Base-1) - } - default: - switch { - case significand < 10*decimal64Base-1: - return Decimal64{bits: d.bits + 1}.debug() - case exp == 369: - return Infinity64 - default: - return newFromParts(sign, exp+1, decimal64Base) - } - } -} - -// NextMinus returns the next value above d. -func (d Decimal64) NextMinus() Decimal64 { - flav, sign, exp, significand := d.parts() - switch { - case flav == flInf: - if sign == 0 { - return Max64 - } - return NegInfinity64 - case flav != flNormal: - return d - case significand == 0: - return NegMin64 - case sign == 0: - switch { - case significand > decimal64Base: - return Decimal64{bits: d.bits - 1}.debug() - case exp == -398: - if significand > 1 { - return Decimal64{bits: d.bits - 1}.debug() - } - return Zero64 - default: - return newFromParts(sign, exp-1, 10*decimal64Base-1) - } - default: - switch { - case significand < 10*decimal64Base-1: - return Decimal64{bits: d.bits + 1}.debug() - case exp == 369: - return NegInfinity64 - default: - return newFromParts(sign, exp+1, decimal64Base) - } - } -} diff --git a/decimal64gob.go b/decimal64gob.go index 3637f60..67c56ff 100644 --- a/decimal64gob.go +++ b/decimal64gob.go @@ -1,19 +1,18 @@ package decimal import ( - "encoding/binary" + "encoding/gob" ) +var _ gob.GobDecoder = (*Decimal64)(nil) +var _ gob.GobEncoder = Decimal64{} + // GobDecode implements encoding.GobDecoder. func (d *Decimal64) GobDecode(buf []byte) error { - d.bits = binary.BigEndian.Uint64(buf) - // TODO: Check for out of bounds significand. - return nil + return d.UnmarshalBinary(buf) } // GobEncode implements encoding.GobEncoder. func (d Decimal64) GobEncode() ([]byte, error) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, d.bits) - return buf, nil + return d.MarshalBinary() } diff --git a/decimal64json.go b/decimal64json.go index 597ab7d..a980312 100644 --- a/decimal64json.go +++ b/decimal64json.go @@ -1,8 +1,13 @@ package decimal +import "encoding/json" + +var _ json.Marshaler = Decimal64{} +var _ json.Unmarshaler = (*Decimal64)(nil) + // MarshalText implements the encoding.TextMarshaler interface. func (d Decimal64) MarshalJSON() ([]byte, error) { - return d.MarshalText(), nil + return d.MarshalText() } // UnmarshalText implements the encoding.TextUnmarshaler interface. diff --git a/decimal64marshal.go b/decimal64marshal.go index 5506a21..8b724dc 100644 --- a/decimal64marshal.go +++ b/decimal64marshal.go @@ -1,13 +1,18 @@ package decimal import ( + "encoding" + "encoding/binary" "fmt" ) +var _ encoding.TextMarshaler = Decimal64{} +var _ encoding.TextUnmarshaler = (*Decimal64)(nil) + // MarshalText implements the encoding.TextMarshaler interface. -func (d Decimal64) MarshalText() []byte { - var buf []byte - return d.Append(buf, 'g', -1) +func (d Decimal64) MarshalText() ([]byte, error) { + data := d.Append(make([]byte, 0, 16), 'g', -1) + return data, nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. @@ -20,3 +25,20 @@ func (d *Decimal64) UnmarshalText(text []byte) error { } return err } + +var _ encoding.BinaryMarshaler = Decimal64{} +var _ encoding.BinaryUnmarshaler = (*Decimal64)(nil) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Decimal64) MarshalBinary() ([]byte, error) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, d.bits) + return buf, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (d *Decimal64) UnmarshalBinary(data []byte) error { + d.bits = binary.BigEndian.Uint64(data) + // TODO: Check for out of bounds significand. + return nil +} diff --git a/decimal64marshal_test.go b/decimal64marshal_test.go index a64a3fd..aa0330a 100644 --- a/decimal64marshal_test.go +++ b/decimal64marshal_test.go @@ -3,20 +3,23 @@ package decimal import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDecimal64Marshal(t *testing.T) { - require.Equal(t, []byte("23456"), New64FromInt64(23456).MarshalText()) + data, err := New64FromInt64(23456).MarshalText() + require.NoError(t, err) + assert.Equal(t, []byte("23456"), data) } func TestDecimal64Unmarshal(t *testing.T) { var d Decimal64 require.NoError(t, d.UnmarshalText([]byte("23456"))) - require.Equal(t, New64FromInt64(23456), d) + assert.Equal(t, New64FromInt64(23456), d) } func TestDecimal64UnmarshalBadInput(t *testing.T) { var d Decimal64 - require.Error(t, d.UnmarshalText([]byte("omg"))) + assert.Error(t, d.UnmarshalText([]byte("omg"))) } diff --git a/decimal64math.go b/decimal64math.go index 1ad6cba..4aedfe9 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -439,3 +439,77 @@ func (ctx Context64) Mul(d, e Decimal64) Decimal64 { } return newFromParts(ans.sign, ans.exp, ans.significand.lo) } + +// NextPlus returns the next value above d. +func (d Decimal64) NextPlus() Decimal64 { + flav, sign, exp, significand := d.parts() + switch { + case flav == flInf: + if sign == 1 { + return NegMax64 + } + return Infinity64 + case flav != flNormal: + return d + case significand == 0: + return Min64 + case sign == 1: + switch { + case significand > decimal64Base: + return Decimal64{bits: d.bits - 1}.debug() + case exp == -398: + if significand > 1 { + return Decimal64{bits: d.bits - 1}.debug() + } + return Zero64 + default: + return newFromParts(sign, exp-1, 10*decimal64Base-1) + } + default: + switch { + case significand < 10*decimal64Base-1: + return Decimal64{bits: d.bits + 1}.debug() + case exp == 369: + return Infinity64 + default: + return newFromParts(sign, exp+1, decimal64Base) + } + } +} + +// NextMinus returns the next value above d. +func (d Decimal64) NextMinus() Decimal64 { + flav, sign, exp, significand := d.parts() + switch { + case flav == flInf: + if sign == 0 { + return Max64 + } + return NegInfinity64 + case flav != flNormal: + return d + case significand == 0: + return NegMin64 + case sign == 0: + switch { + case significand > decimal64Base: + return Decimal64{bits: d.bits - 1}.debug() + case exp == -398: + if significand > 1 { + return Decimal64{bits: d.bits - 1}.debug() + } + return Zero64 + default: + return newFromParts(sign, exp-1, 10*decimal64Base-1) + } + default: + switch { + case significand < 10*decimal64Base-1: + return Decimal64{bits: d.bits + 1}.debug() + case exp == 369: + return NegInfinity64 + default: + return newFromParts(sign, exp+1, decimal64Base) + } + } +} From ec66413ab2f69a9af23302827a38a2463338d452 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:48:38 +1000 Subject: [PATCH 07/17] beef up docs --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ece4d25..c53ac34 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # decimal This library implements fixed-precision decimal numbers based on IEEE 754R standard; - +. More info can be found at: -# Features +## Features + - Decimal64, partial implementation of the ieee-754R standard - Half up and half even rounding - Up to 3 times faster than arbitrary precision decimal libraries in Go -# Goals -- To implement 128 bit decimal +## Goals -# Installation and use +- Implement 128 bit decimal +- Implement as much of as possible. -Run `go get github.com/anz-bank/decimal` +## Installation and use +Run `go get github.com/anz-bank/decimal` ```go package main @@ -35,22 +37,64 @@ func main() { fmt.Println(a, b, c, d) } - ``` -# Docs +## Usage notes + +### Formatting + +`Decimal64` provides a range of ways to present numbers, both for human and +machine consumption. + +`Decimal64` implements the following conventional interfaces: + +- `fmt`: `Formatter`, `Scanner` and `Stringer` + - It currently supports specifying a precision argument, e.g., `%.10f` for the `f` and `F` verbs, while support `g` and `G` is [on the board](https://github.com/anz-bank/decimal/issues/72), as is [support for a width argument](https://github.com/anz-bank/decimal/issues/72). +- `json`: `Marshaller` and `Unmarshaller` +- `encoding`: `BinaryMarshaler`, `BinaryUnmarshaler`, `TextMarshaler` and `TextUnmarshaler` +- `encoging/gob`: `GobEncoder` and `GobDecoder` +- thus enabling the use of decimal numbers in the `fmt.Printf` family. +- `Decimal64.Append` formats straight into a `[]byte` buffer. +- `Decimal64.Text` formats in the same way, but returns a `string`. + +### Debugging + +tl;dr: Use the `decimal_debug` compiler tag during debugging to greatly ease +runtime inspection of `Decimal64` values. + +Debugging with the `decimal` package can be challeging because a `Decimal64` +number is encoded in a `uint64` and the values it holds are inscrutable even to +the trained eye. For example, the number one `decimal.One64` is represented +internally as the number `3450757314565799936` (`2fe38d7ea4c68000` in +hexadecimal). + +To ease debugging, `Decimal64` holds an optional `debugInfo` structure that +contains a string representation and unpacked components of the `uint64` +representation for every `Decimal64` value. + +This feature is enabled through the `decimal_debug` compiler tag. This is done +at compile time instead of through runtime flags because having the structure +there even if not used would double the size of each number and greatly increase +the cost of using it. The size and runtime cost of this feature is zero when the +compiler tag is not present. + +## Docs + -# Why decimal +## Why decimal? + Binary floating point numbers are fundamentally flawed when it comes to representing exact numbers in a decimal world. Just like 1/3 can't be represented in base 10 (it evaluates to 0.3333333333 repeating), 1/10 can't be represented in binary. The solution is to use a decimal floating point number. -Binary floating point numbers (often just called floating point numbers) are usually in the form -`Sign * Significand * 2 ^ exp` -and decimal floating point numbers change this to -`Sign * Significand * 10 ^ exp` -This eliminates the decimal fraction problem, as the base is in 10. +Binary floating point numbers (often just called floating point numbers) are usually in the form `Sign * Significand * 2 ^ exp` and decimal floating point numbers change this to `Sign * Significand * 10 ^ exp`. +The use of base 10 eliminates the decimal fraction problem. + +## Why fixed precision? +Most implementations of a decimal floating point datatype implement an *arbitrary precision* type, which often uses an underlying big int. This gives flexibility in that as the number grows, the number of bits assigned to the number grows ( hence the term "arbitrary precision"). +This library is different. It uses a 64-bit decimal datatype as specified in the IEEE-754R standard. This sacrifices the ability to represent arbitrarily large numbers, but is much faster than arbitrary precision libraries. +There are two main reasons for this: -# Why fixed precision -Most implementations of a decimal floating point datatype implement an 'arbitrary precision' type, which often uses an underlying big int. This gives flexibility in that as the number grows, the number of bits assigned to the number grows ( and thus 'arbitrary precision'). -This library is different as it specifies a 64 bit decimal datatype as specified in the ieee-754R standard. This gives the sacrifice of being able to represent arbitrarily large numbers, but is faster than other arbitrary precision libraries. +1. The fixed-size data type is a `uint64` under the hood and never requires + heap allocation. +2. All the algorithms can hard-code assumptions about the number of bits to work with. In fact, many of the operations work on the entire number as a single unit using 64-bit integer arithmetic and, on the occasions it needs to use more, 128 bits always suffices. From 2e459d4466065e0a12c3597e738af6e497d9b7aa Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:57:09 +1000 Subject: [PATCH 08/17] add missing .debug() calls --- decimal64.go | 2 +- decimal64math.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/decimal64.go b/decimal64.go index 14d3656..e0cc056 100644 --- a/decimal64.go +++ b/decimal64.go @@ -357,7 +357,7 @@ func (d Decimal64) IsInt() bool { // quiet returns a quiet form of d, which must be a NaN. func (d Decimal64) quiet() Decimal64 { - return Decimal64{bits: d.bits &^ (2 << 56)} + return Decimal64{bits: d.bits &^ (2 << 56)}.debug() } // IsSubnormal returns true iff d is a subnormal. diff --git a/decimal64math.go b/decimal64math.go index 4aedfe9..c32bc6c 100644 --- a/decimal64math.go +++ b/decimal64math.go @@ -196,7 +196,7 @@ func (d Decimal64) Logb() Decimal64 { // CopySign copies d, but with the sign taken from e. func (d Decimal64) CopySign(e Decimal64) Decimal64 { - return Decimal64{bits: d.bits&^neg64 | e.bits&neg64} + return Decimal64{bits: d.bits&^neg64 | e.bits&neg64}.debug() } // Quo computes d / e. From 75635255f72d1ebe6244e99976479caea73bde40 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:14:08 +1000 Subject: [PATCH 09/17] declare that Decimal64 implements fmt interfaces --- decimal64fmt.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/decimal64fmt.go b/decimal64fmt.go index 1cb3e63..1df5abd 100644 --- a/decimal64fmt.go +++ b/decimal64fmt.go @@ -6,6 +6,10 @@ import ( "strconv" ) +var _ fmt.Formatter = Decimal64{} +var _ fmt.Scanner = (*Decimal64)(nil) +var _ fmt.Stringer = Decimal64{} + func appendFrac64(buf []byte, n, limit uint64) []byte { for n > 0 { msd := n / limit From d9de3183e19ba8947e5f251d3ffa8e72fe489765 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:28:01 +1000 Subject: [PATCH 10/17] use go 1.18 in ci --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d1297bc..bd3c096 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.13 + - name: Set up Go 1.18 uses: actions/setup-go@v1 with: - go-version: 1.13 + go-version: 1.18 id: go - name: Check out code into the Go module directory From 31d7c1f861ff8b53dfa3a0670d3bd768fd54e964 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:31:27 +1000 Subject: [PATCH 11/17] remove slices depdendency --- decimalSuite_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/decimalSuite_test.go b/decimalSuite_test.go index 4807e5e..f6f5aa8 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "regexp" - "slices" "strings" "testing" @@ -181,7 +180,16 @@ func getInput(line string) *testCase { for _, f := range m { fields = append(fields, strings.ReplaceAll(f[1], "''", "'")+f[2]) } - if i := slices.Index(fields, "->"); i < 5 { + i := 0 + for ; i < len(fields); i++ { + if fields[i] == "->" { + break + } + } + if i == len(fields) { + panic("missing ->") + } + if i < 5 { if i == -1 { panic(fmt.Errorf("malformed input: %s", line)) } From 7d4b74818745afb86a8f301f0c5ccdb1ac08938d Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:33:59 +1000 Subject: [PATCH 12/17] degenericize the one generic type --- decimalSuite_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/decimalSuite_test.go b/decimalSuite_test.go index f6f5aa8..c8d3a6d 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -34,15 +34,15 @@ func (testVal *testCase) String() string { return fmt.Sprintf("%s %s (%v, %v, %v) -> %v", testVal.name, testVal.function, testVal.val1, testVal.val2, testVal.val3, testVal.expectedResult) } -type set[K comparable] map[K]struct{} +type set map[string]struct{} -func (s set[K]) Has(k K) bool { +func (s set) Has(k string) bool { _, ok := s[k] return ok } -var supportedRounding = set[string]{"half_up": {}, "half_even": {}} -var ignoredFunctions = set[string]{"apply": {}} +var supportedRounding = set{"half_up": {}, "half_even": {}} +var ignoredFunctions = set{"apply": {}} // TestFromSuite is the master tester for the dectest suite. func TestFromSuite(t *testing.T) { @@ -277,7 +277,7 @@ func runTest(t *testing.T, context Context64, expected opResult, testValStrings return true } -var textResults = set[string]{"class": {}} +var textResults = set{"class": {}} // TODO: get runTest to run more functions such as FMA. // execOp returns the calculated answer to the operation as Decimal64. From d1b9f154ff52b6255e75fe448ba0224eff8d0d6b Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:42:49 +1000 Subject: [PATCH 13/17] bumpd go to 1.19 --- .github/workflows/go.yml | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bd3c096..a05e4e5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.18 + - name: Set up Go 1.19 uses: actions/setup-go@v1 with: - go-version: 1.18 + go-version: 1.19 id: go - name: Check out code into the Go module directory diff --git a/go.mod b/go.mod index 11de83d..cc375ce 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/anz-bank/decimal -go 1.18 +go 1.19 require github.com/stretchr/testify v1.2.2 From a0a654d502203698dd26f6f30154cb619177d631 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:44:12 +1000 Subject: [PATCH 14/17] bump go to 1.20 --- .github/workflows/go.yml | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a05e4e5..822e195 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.19 + - name: Set up Go 1.20 uses: actions/setup-go@v1 with: - go-version: 1.19 + go-version: 1.20 id: go - name: Check out code into the Go module directory diff --git a/go.mod b/go.mod index cc375ce..6048820 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/anz-bank/decimal -go 1.19 +go 1.20 require github.com/stretchr/testify v1.2.2 From 1b0bb5aa09fcf625cde9ee1e9521f6cf51481ac1 Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:47:23 +1000 Subject: [PATCH 15/17] upgrade ci actions --- .github/workflows/reviewdog.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index b814698..6ebf849 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -1,4 +1,3 @@ - name: reviewdog on: [pull_request] jobs: @@ -7,9 +6,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: golangci-lint - uses: reviewdog/action-golangci-lint@v1 + uses: reviewdog/action-golangci-lint@v2 with: github_token: ${{ secrets.github_token }} - golangci_lint_flags: "--enable-all --exclude-use-default=false" \ No newline at end of file + golangci_lint_flags: "--enable-all --exclude-use-default=false" From 2593dfca654c26039e669f40eaac6090e1cea2bf Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:53:22 +1000 Subject: [PATCH 16/17] lint --- decimal64_test.go | 1 - decimal64decParts.go | 4 ---- decimal64fmt.go | 2 +- decimal64scan_test.go | 2 +- decimalSuite_test.go | 16 ---------------- 5 files changed, 2 insertions(+), 23 deletions(-) diff --git a/decimal64_test.go b/decimal64_test.go index 2836411..c8b6ba2 100644 --- a/decimal64_test.go +++ b/decimal64_test.go @@ -41,7 +41,6 @@ func TestDecimal64Float64(t *testing.T) { require.Equal(-1.0, NegOne64.Float64()) require.Equal(0.0, Zero64.Float64()) - require.Equal(-0.0, NegZero64.Float64()) require.Equal(1.0, One64.Float64()) require.Equal(10.0, New64FromInt64(10).Float64()) diff --git a/decimal64decParts.go b/decimal64decParts.go index 7183b6c..e88ff62 100644 --- a/decimal64decParts.go +++ b/decimal64decParts.go @@ -87,10 +87,6 @@ func (dp *decParts) isSNaN() bool { return dp.fl == flSNaN } -func (dp *decParts) nanWeight() int { - return int(dp.significand.lo) -} - func (dp *decParts) isSubnormal() bool { return (dp.significand != uint128T{}) && dp.significand.lo < decimal64Base && dp.fl == flNormal } diff --git a/decimal64fmt.go b/decimal64fmt.go index 1df5abd..37ca6b9 100644 --- a/decimal64fmt.go +++ b/decimal64fmt.go @@ -175,7 +175,7 @@ func (d Decimal64) Format(s fmt.State, format rune) { fmt.Fprintf(s, "%%!%c(*decimal.Decimal64=%s)", format, d.String()) return } - s.Write(d.append(make([]byte, 0, 16), byte(format), width, prec)) + s.Write(d.append(make([]byte, 0, 16), byte(format), width, prec)) //nolint:errcheck } // String returns a string representation of d. diff --git a/decimal64scan_test.go b/decimal64scan_test.go index 2f5330f..b930448 100644 --- a/decimal64scan_test.go +++ b/decimal64scan_test.go @@ -95,7 +95,7 @@ func BenchmarkParse64(b *testing.B) { var d Decimal64 for n := 0; n < b.N; n++ { buf := bytes.NewBufferString("123456789") - fmt.Fscanf(buf, "%g", &d) + fmt.Fscanf(buf, "%g", &d) //nolint:errcheck } } diff --git a/decimalSuite_test.go b/decimalSuite_test.go index c8d3a6d..63b8646 100644 --- a/decimalSuite_test.go +++ b/decimalSuite_test.go @@ -142,22 +142,6 @@ func setRoundingFromString(s string) Context64 { } } -func isRoundingErr(res, expected Decimal64) bool { - var resP decParts - resP.unpack(res) - var expectedP decParts - expectedP.unpack(expected) - sigDiff := int64(resP.significand.lo - expectedP.significand.lo) - expDiff := resP.exp - expectedP.exp - if (sigDiff == 1 || sigDiff == -1) && (expDiff == 1 || expDiff == -1 || expDiff == 0) { - return true - } - if resP.significand.lo == maxSig && resP.exp == expMax && expectedP.fl == flInf { - return true - } - return false -} - var ( testRegex = regexp.MustCompile(`'((?:''+|[^'])*)'|(\S+)`) roundingRegex = regexp.MustCompile(`(?:rounding:[\s]*)(?P[\S]*)`) From 6f9354706fdc9a2fc60345d52913c4aeb265859e Mon Sep 17 00:00:00 2001 From: Marcelo Cantos <13160581+anzdaddy@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:58:40 +1000 Subject: [PATCH 17/17] use default golangci-lint settings in ci --- .github/workflows/reviewdog.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 6ebf849..4941c1a 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -11,4 +11,3 @@ jobs: uses: reviewdog/action-golangci-lint@v2 with: github_token: ${{ secrets.github_token }} - golangci_lint_flags: "--enable-all --exclude-use-default=false"