diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f79d3..90fc371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.20] - 2024-01-01 + +### Changed + +- Eliminated heap allocations in big.Int arithmetic. +- Improved documentation. + ## [0.1.19] - 2023-12-18 ### Changed diff --git a/README.md b/README.md index eeb4e8f..ba2ec83 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,7 @@ This package is designed specifically for use in transactional financial systems - **Simple String Representation** - Decimals are represented without the complexities of scientific or engineering notation. - **Correctness** - Fuzz testing is used to [cross-validate] arithmetic operations - against the [cockroachdb] and [shopspring] decimal packages. - + against the [cockroachdb/apd] and [shopspring/decimal] packages. ## Getting Started @@ -66,9 +65,9 @@ func main() { fmt.Println(d.FMA(e, f)) // 8 * 12.5 + 2.567 fmt.Println(d.Pow(2)) // 8 ^ 2 - fmt.Println(d.Quo(e)) // 8 / 12.5 + fmt.Println(d.Quo(e)) // 8 ÷ 12.5 fmt.Println(d.QuoRem(e)) // 8 div 12.5, 8 mod 12.5 - fmt.Println(d.Inv()) // 1 / 8 + fmt.Println(d.Inv()) // 1 ÷ 8 // Rounding to 2 decimal places fmt.Println(g.Round(2)) // 7.90 @@ -98,20 +97,20 @@ For examples related to financial calculations, see the `money` package Comparison with other popular packages: -| Feature | govalues | [cockroachdb] v3.2.1 | [shopspring] v1.3.1 | -| ---------------- | ------------ | -------------------- | ------------------- | -| Speed | High | Medium | Low[^reason] | -| Mutability | Immutable | Mutable[^reason] | Immutable | -| Memory Footprint | Low | Medium | High | -| Panic Free | Yes | Yes | No[^divzero] | -| Precision | 19 digits | Arbitrary | Arbitrary | -| Default Rounding | Half to even | Half up | Half away from 0 | -| Context | Implicit | Explicit | Implicit | +| Feature | govalues | [cockroachdb/apd] v3.2.1 | [shopspring/decimal] v1.3.1 | +| ---------------- | ------------ | ------------------------ | --------------------------- | +| Speed | High | Medium | Low[^reason] | +| Mutability | Immutable | Mutable[^reason] | Immutable | +| Memory Footprint | Low | Medium | High | +| Panic Free | Yes | Yes | No[^divzero] | +| Precision | 19 digits | Arbitrary | Arbitrary | +| Default Rounding | Half to even | Half up | Half away from 0 | +| Context | Implicit | Explicit | Implicit | -[^reason]: decimal package was created simply because shopspring's decimal was -too slow and cockroachdb's decimal was mutable. +[^reason]: decimal package was created simply because [shopspring/decimal] was +too slow and [cockroachdb/apd] was mutable. -[^divzero]: [shopspring]'s decimal panics on division by zero. +[^divzero]: [shopspring/decimal] panics on division by zero. ### Benchmarks @@ -122,22 +121,22 @@ pkg: github.com/govalues/decimal-tests cpu: AMD Ryzen 7 3700C with Radeon Vega Mobile Gfx ``` -| Test Case | Expression | govalues | [cockroachdb] v3.2.1 | [shopspring] v1.3.1 | govalues vs cockroachdb | govalues vs shopspring | -| ----------- | -------------------- | -------: | -------------------: | ------------------: | ----------------------: | ---------------------: | -| Add | 2 + 3 | 15.53n | 46.68n | 142.30n | +200.45% | +816.00% | -| Mul | 2 * 3 | 15.64n | 52.83n | 137.35n | +237.76% | +778.20% | -| QuoFinite | 2 / 4 | 51.65n | 179.60n | 619.40n | +247.76% | +1099.34% | -| QuoInfinite | 2 / 3 | 568.80n | 935.20n | 2749.00n | +64.43% | +383.30% | -| Pow | 1.1^60 | 1.28µ | 3.28µ | 16.03µ | +156.99% | +1156.09% | -| Pow | 1.01^600 | 4.31µ | 10.43µ | 37.00µ | +142.15% | +758.69% | -| Pow | 1.001^6000 | 7.54µ | 20.39µ | 651.51µ | +170.58% | +8544.78% | -| Parse | 1 | 17.14n | 77.64n | 129.15n | +353.00% | +653.50% | -| Parse | 123.456 | 36.15n | 201.85n | 235.25n | +458.37% | +550.76% | -| Parse | 123456789.1234567890 | 98.90n | 210.95n | 475.05n | +113.30% | +380.33% | -| String | 1 | 5.18n | 21.43n | 208.00n | +313.99% | +3918.16% | -| String | 123.456 | 42.31n | 67.55n | 226.55n | +59.66% | +435.52% | -| String | 123456789.1234567890 | 76.04n | 209.50n | 329.95n | +175.49% | +333.89% | -| Telco | see [specification] | 134.00n | 947.60n | 3945.50n | +607.13% | +2844.40% | +| Test Case | Expression | govalues | [cockroachdb/apd] v3.2.1 | [shopspring/decimal] v1.3.1 | govalues vs cockroachdb | govalues vs shopspring | +| ----------- | -------------------- | -------: | -----------------------: | --------------------------: | ----------------------: | ---------------------: | +| Add | 5 + 6 | 16.89n | 80.96n | 140.50n | +379.48% | +732.10% | +| Mul | 2 * 3 | 16.85n | 58.14n | 145.30n | +245.15% | +762.57% | +| QuoExact | 2 ÷ 4 | 66.00n | 193.25n | 619.15n | +192.78% | +838.03% | +| QuoInfinite | 2 ÷ 3 | 453.30n | 961.00n | 2767.00n | +112.01% | +510.41% | +| Pow | 1.1^60 | 1.04µ | 3.42µ | 15.76µ | +227.72% | +1408.43% | +| Pow | 1.01^600 | 3.57µ | 10.70µ | 35.70µ | +200.11% | +901.23% | +| Pow | 1.001^6000 | 6.19µ | 20.72µ | 634.41µ | +234.65% | +10148.95% | +| Parse | 1 | 18.10n | 85.66n | 136.75n | +373.23% | +655.52% | +| Parse | 123.456 | 54.16n | 197.25n | 238.45n | +264.20% | +340.27% | +| Parse | 123456789.1234567890 | 111.00n | 238.20n | 498.00n | +114.59% | +348.65% | +| String | 1 | 5.70n | 20.89n | 203.25n | +266.24% | +3464.23% | +| String | 123.456 | 42.74n | 75.71n | 235.65n | +77.14% | +451.36% | +| String | 123456789.1234567890 | 72.34n | 215.90n | 331.20n | +198.47% | +357.87% | +| Telco | see [specification] | 148.00n | 1075.00n | 4010.50n | +626.35% | +2609.80% | The benchmark results shown in the table are provided for informational purposes only and may vary depending on your specific use case. @@ -170,7 +169,7 @@ This ensures alignment with the project's objectives and roadmap. [licenseb]: https://img.shields.io/github/license/govalues/decimal?color=blue [awesome]: https://github.com/avelino/awesome-go#financial [awesomeb]: https://awesome.re/mentioned-badge.svg -[cockroachdb]: https://pkg.go.dev/github.com/cockroachdb/apd -[shopspring]: https://pkg.go.dev/github.com/shopspring/decimal +[cockroachdb/apd]: https://pkg.go.dev/github.com/cockroachdb/apd +[shopspring/decimal]: https://pkg.go.dev/github.com/shopspring/decimal [specification]: https://speleotrove.com/decimal/telcoSpec.html [cross-validate]: https://github.com/govalues/decimal-tests/blob/main/decimal_fuzz_test.go diff --git a/coefficient.go b/coefficient.go index 7457c8e..ddcec7c 100644 --- a/coefficient.go +++ b/coefficient.go @@ -2,6 +2,7 @@ package decimal import ( "math/big" + "sync" ) // fint (Fast INTeger) is a wrapper around uint64. @@ -174,7 +175,7 @@ func (x fint) rshDown(shift int) fint { } // prec returns length of x in decimal digits. -// prec assumes that 0 has zero digits. +// prec assumes that 0 has no digits. func (x fint) prec() int { left, right := 0, len(pow10) for left < right { @@ -188,8 +189,9 @@ func (x fint) prec() int { return left } -// tzeroes returns number of trailing zeros in x. -func (x fint) tzeros() int { +// ntz returns number of trailing zeros in x. +// ntz assumes that 0 has no trailing zeros. +func (x fint) ntz() int { left, right := 1, x.prec() for left < right { mid := (left + right) / 2 @@ -215,12 +217,6 @@ func (x fint) hasPrec(prec int) bool { return x >= pow10[prec-1] } -// bint converts uint64 to *big.Int. -func (x fint) bint() *bint { - z := new(big.Int).SetUint64(uint64(x)) - return (*bint)(z) -} - // bint (Big INTeger) is a wrapper around big.Int. type bint big.Int @@ -328,10 +324,11 @@ var bpow10 = [...]*bint{ newBintFromPow10(99), } -// newBintFromPow10 returns 10^exp as *big.Int. -func newBintFromPow10(exp int) *bint { - z := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil) - return (*bint)(z) +// newBintFromPow10 creates a *big.Int equal to 10^power. +func newBintFromPow10(power int) *bint { + z := (*bint)(new(big.Int)) + z.pow10(power) + return z } func (z *bint) sign() int { @@ -350,6 +347,10 @@ func (z *bint) setBint(x *bint) { (*big.Int)(z).Set((*big.Int)(x)) } +func (z *bint) setInt64(x int64) { + (*big.Int)(z).SetInt64(x) +} + func (z *bint) setFint(x fint) { (*big.Int)(z).SetUint64(uint64(x)) } @@ -357,8 +358,8 @@ func (z *bint) setFint(x fint) { // fint converts *big.Int to uint64. // If z cannot be represented as uint64, the result is undefined. func (z *bint) fint() fint { - i := (*big.Int)(z).Uint64() - return fint(i) + f := (*big.Int)(z).Uint64() + return fint(f) } // add calculates z = x + y. @@ -397,50 +398,78 @@ func (z *bint) mul(x, y *bint) { (*big.Int)(z).Mul((*big.Int)(x), (*big.Int)(y)) } +// exp calculates z = x^y. +// If y is negative, the result is unpredictable. +func (z *bint) exp(x, y *bint) { + (*big.Int)(z).Exp((*big.Int)(x), (*big.Int)(y), nil) +} + +// pow10 calculates z = 10^power. +// If power is negative, the result is unpredictable. +func (z *bint) pow10(power int) { + x := getBint() + defer putBint(x) + x.setInt64(10) + y := getBint() + defer putBint(y) + y.setInt64(int64(power)) + z.exp(x, y) +} + // quo calculates z = x / y. func (z *bint) quo(x, y *bint) { - (*big.Int)(z).Quo((*big.Int)(x), (*big.Int)(y)) + r := getBint() + defer putBint(r) + // Passing r to prevent heap allocations. + z.quoRem(x, y, r) } // quoRem calculates z and r such that x = z * y + r. -func (z *bint) quoRem(x, y *bint) *bint { - _, r := (*big.Int)(z).QuoRem((*big.Int)(x), (*big.Int)(y), new(big.Int)) - return (*bint)(r) +func (z *bint) quoRem(x, y, r *bint) { + (*big.Int)(z).QuoRem((*big.Int)(x), (*big.Int)(y), (*big.Int)(r)) } func (z *bint) isOdd() bool { return (*big.Int)(z).Bit(0) != 0 } -// lsh (Left Shift) calculates x * 10^shift. +// lsh (Left Shift) calculates z = x * 10^shift. func (z *bint) lsh(x *bint, shift int) { var y *bint if shift < len(bpow10) { y = bpow10[shift] } else { - y = newBintFromPow10(shift) + y = getBint() + defer putBint(y) + y.pow10(shift) } z.mul(x, y) } -// fsa (Fused Shift and Addition) calculates x * 10^shift + y. -func (z *bint) fsa(shift int, y byte) { - z.lsh(z, shift) - z.add(z, fint(y).bint()) +// fsa (Fused Shift and Addition) calculates z = x * 10^shift + f. +func (z *bint) fsa(x *bint, shift int, f fint) { + y := getBint() + defer putBint(y) + y.setFint(f) + z.lsh(x, shift) + z.add(z, y) } -// rshDown (Right Shift) calculates x / 10^shift and rounds result towards zero. +// rshDown (Right Shift) calculates z = x / 10^shift and rounds +// result towards zero. func (z *bint) rshDown(x *bint, shift int) { var y *bint if shift < len(bpow10) { y = bpow10[shift] } else { - y = newBintFromPow10(shift) + y = getBint() + defer putBint(y) + y.pow10(shift) } z.quo(x, y) } -// rshHalfEven (Right Shift) calculates x / 10^shift and +// rshHalfEven (Right Shift) calculates z = x / 10^shift and // rounds result using "half to even" rule. func (z *bint) rshHalfEven(x *bint, shift int) { // Special cases @@ -453,13 +482,17 @@ func (z *bint) rshHalfEven(x *bint, shift int) { return } // General case - var y *bint + var y, r *bint if shift < len(bpow10) { y = bpow10[shift] } else { - y = newBintFromPow10(shift) + y = getBint() + defer putBint(y) + y.pow10(shift) } - r := z.quoRem(x, y) + r = getBint() + defer putBint(r) + z.quoRem(x, y, r) r.dbl(r) // r = r * 2 switch y.cmp(r) { case -1: @@ -473,8 +506,8 @@ func (z *bint) rshHalfEven(x *bint, shift int) { } // prec returns length of *big.Int in decimal digits. -// It considers 0 to have zero digits. -// If *big.Int is negative, the result is unpredictable. +// prec assumes that 0 has no digits. +// If z is negative, the result is unpredictable. // // z.prec() provides a more efficient approach than len(z.string()) // when dealing with decimals having less than len(bpow10) digits. @@ -497,7 +530,7 @@ func (z *bint) prec() int { } // hasPrec checks if *big.Int has a given number of digits or more. -// It considers 0 to have zero digits. +// hasPrec assumes that 0 has no digits. // If *big.Int is negative, the result is unpredictable. // // z.hasPrec() provides a more efficient approach than (z.prec() >= prec) @@ -513,3 +546,20 @@ func (z *bint) hasPrec(prec int) bool { // General case return z.cmp(bpow10[prec-1]) >= 0 } + +// pool is a cache of reusable *big.Int instances. +var pool = sync.Pool{ + New: func() any { + return (*bint)(new(big.Int)) + }, +} + +// getBint obtains a *big.Int from the pool. +func getBint() *bint { + return pool.Get().(*bint) +} + +// putBint returns the *big.Int into the pool. +func putBint(b *bint) { + pool.Put(b) +} diff --git a/coefficient_test.go b/coefficient_test.go index 5d01601..12ac203 100644 --- a/coefficient_test.go +++ b/coefficient_test.go @@ -366,7 +366,7 @@ func TestFint_prec(t *testing.T) { } } -func TestFint_tzeros(t *testing.T) { +func TestFint_ntz(t *testing.T) { cases := []struct { x fint want int @@ -415,9 +415,9 @@ func TestFint_tzeros(t *testing.T) { {math.MaxUint64, 0}, } for _, tt := range cases { - got := tt.x.tzeros() + got := tt.x.ntz() if got != tt.want { - t.Errorf("%v.tzeros() = %v, want %v", tt.x, got, tt.want) + t.Errorf("%v.ntz() = %v, want %v", tt.x, got, tt.want) } } } diff --git a/decimal.go b/decimal.go index ec61ef7..bd66b0d 100644 --- a/decimal.go +++ b/decimal.go @@ -32,8 +32,8 @@ var ( Ten = MustNew(10, 0) // Ten represents the decimal value of 10. Hundred = MustNew(100, 0) // Hundred represents the decimal value of 100. Thousand = MustNew(1_000, 0) // Thousand represents the decimal value of 1,000. - E = MustNew(2_718_281_828_459_045_235, 18) // E represents Euler’s number rounded to 18 decimals. - Pi = MustNew(3_141_592_653_589_793_238, 18) // Pi represents the value of π rounded to 18 decimals. + E = MustNew(2_718_281_828_459_045_235, 18) // E represents Euler’s number rounded to 18 digits. + Pi = MustNew(3_141_592_653_589_793_238, 18) // Pi represents the value of π rounded to 18 digits. errDecimalOverflow = errors.New("decimal overflow") errInvalidDecimal = errors.New("invalid decimal") errScaleRange = errors.New("scale out of range") @@ -362,10 +362,12 @@ func parseBint(s string, minScale int) (Decimal, error) { } // Integer - coef := new(bint) + coef := getBint() + defer putBint(coef) + coef.setFint(0) hascoef := false for pos < width && s[pos] >= '0' && s[pos] <= '9' { - coef.fsa(1, s[pos]-'0') + coef.fsa(coef, 1, fint(s[pos]-'0')) hascoef = true pos++ } @@ -375,7 +377,7 @@ func parseBint(s string, minScale int) (Decimal, error) { if pos < width && s[pos] == '.' { pos++ for pos < width && s[pos] >= '0' && s[pos] <= '9' { - coef.fsa(1, s[pos]-'0') + coef.fsa(coef, 1, fint(s[pos]-'0')) hascoef = true scale++ pos++ @@ -628,9 +630,9 @@ func (d Decimal) Format(state fmt.State, verb rune) { } // Rescaling - tzeroes := 0 + var tzeros int if verb == 'f' || verb == 'F' || verb == 'k' || verb == 'K' { - scale := 0 + var scale int switch p, ok := state.Precision(); { case ok: scale = p @@ -646,12 +648,13 @@ func (d Decimal) Format(state fmt.State, verb rune) { case scale < d.Scale(): d = d.Round(scale) case scale > d.Scale(): - tzeroes = scale - d.Scale() + tzeros = scale - d.Scale() } } // Integer and fractional digits - intdigs, fracdigs := 0, d.Scale() + var intdigs int + fracdigs := d.Scale() if dprec := d.Prec(); dprec > fracdigs { intdigs = dprec - fracdigs } @@ -660,38 +663,38 @@ func (d Decimal) Format(state fmt.State, verb rune) { } // Decimal point - dpoint := 0 - if fracdigs > 0 || tzeroes > 0 { + var dpoint int + if fracdigs > 0 || tzeros > 0 { dpoint = 1 } // Arithmetic sign - rsign := 0 + var rsign int if d.IsNeg() || state.Flag('+') || state.Flag(' ') { rsign = 1 } // Percentage sign - psign := 0 + var psign int if verb == 'k' || verb == 'K' { psign = 1 } // Openning and closing quotes - lquote, tquote := 0, 0 + var lquote, tquote int if verb == 'q' || verb == 'Q' { lquote, tquote = 1, 1 } // Calculating padding - width := lquote + rsign + intdigs + dpoint + fracdigs + tzeroes + psign + tquote - lspaces, tspaces, lzeroes := 0, 0, 0 + width := lquote + rsign + intdigs + dpoint + fracdigs + tzeros + psign + tquote + var lspaces, tspaces, lzeros int if w, ok := state.Width(); ok && w > width { switch { case state.Flag('-'): tspaces = w - width case state.Flag('0'): - lzeroes = w - width + lzeros = w - width default: lspaces = w - width } @@ -719,8 +722,8 @@ func (d Decimal) Format(state fmt.State, verb rune) { pos-- } - // Trailing zeroes - for i := 0; i < tzeroes; i++ { + // Trailing zeros + for i := 0; i < tzeros; i++ { buf[pos] = '0' pos-- } @@ -746,8 +749,8 @@ func (d Decimal) Format(state fmt.State, verb rune) { dcoef /= 10 } - // Leading zeroes - for i := 0; i < lzeroes; i++ { + // Leading zeros + for i := 0; i < lzeros; i++ { buf[pos] = '0' pos-- } @@ -815,7 +818,7 @@ func (d Decimal) MinScale() int { return MinScale } // General case - z := d.coef.tzeros() + z := d.coef.ntz() if d.Scale() <= z { return MinScale } @@ -1108,8 +1111,12 @@ func (d Decimal) mulFint(e Decimal, minScale int) (Decimal, error) { // mulBint computes the product of two decimals using *big.Int arithmetic. func (d Decimal) mulBint(e Decimal, minScale int) (Decimal, error) { - dcoef := d.coef.bint() - ecoef := e.coef.bint() + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(e.coef) // Coefficient dcoef.mul(dcoef, ecoef) @@ -1168,13 +1175,18 @@ func (d Decimal) PowExact(power, scale int) (Decimal, error) { // powFint computes the power of a decimal using uint64 arithmetic. // powFint does not support negative powers. func (d Decimal) powFint(power, minScale int) (Decimal, error) { + dcoef := d.coef + dneg := d.IsNeg() + dscale := d.Scale() + + ecoef := One.coef + eneg := One.IsNeg() + escale := One.Scale() + if power < 0 { return Decimal{}, errInvalidOperation } - dneg, dcoef, dscale := d.IsNeg(), d.coef, d.Scale() - eneg, ecoef, escale := One.IsNeg(), One.coef, One.Scale() - for power > 0 { if power%2 == 1 { power = power - 1 @@ -1209,21 +1221,31 @@ func (d Decimal) powFint(power, minScale int) (Decimal, error) { dscale = dscale * 2 } } + return newFromFint(eneg, ecoef, escale, minScale) } // powBint computes the power of a decimal using *big.Int arithmetic. // powBint supports negative powers. func (d Decimal) powBint(power, minScale int) (Decimal, error) { + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + dneg := d.IsNeg() + dscale := d.Scale() + + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(One.coef) + eneg := One.IsNeg() + escale := One.Scale() + inv := false if power < 0 { power = -power inv = true } - dneg, dcoef, dscale := d.IsNeg(), d.coef.bint(), d.Scale() - eneg, ecoef, escale := One.IsNeg(), One.coef.bint(), One.Scale() - for power > 0 { if power%2 == 1 { power = power - 1 @@ -1353,8 +1375,13 @@ func (d Decimal) addFint(e Decimal, minScale int) (Decimal, error) { // addBint computes the sum of two decimals using *big.Int arithmetic. func (d Decimal) addBint(e Decimal, minScale int) (Decimal, error) { - dcoef := d.coef.bint() - ecoef := e.coef.bint() + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(e.coef) // Alignment and scale var scale int @@ -1495,9 +1522,17 @@ func (d Decimal) fmaFint(e, f Decimal, minScale int) (Decimal, error) { // fmaBint computes the fused multiply-addition of three decimals using *big.Int arithmetic. func (d Decimal) fmaBint(e, f Decimal, minScale int) (Decimal, error) { - dcoef := d.coef.bint() - ecoef := e.coef.bint() - fcoef := f.coef.bint() + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(e.coef) + + fcoef := getBint() + defer putBint(fcoef) + fcoef.setFint(f.coef) // Coefficient (Multiplication) dcoef.mul(dcoef, ecoef) @@ -1598,7 +1633,7 @@ func (d Decimal) quoFint(e Decimal, minScale int) (Decimal, error) { } // Divisor alignment - if t := ecoef.tzeros(); t > 0 { + if t := ecoef.ntz(); t > 0 { ecoef = ecoef.rshDown(t) scale = scale + t } @@ -1617,8 +1652,13 @@ func (d Decimal) quoFint(e Decimal, minScale int) (Decimal, error) { // quoBint computes the quotient of two decimals using *big.Int arithmetic. func (d Decimal) quoBint(e Decimal, minScale int) (Decimal, error) { - dcoef := d.coef.bint() - ecoef := e.coef.bint() + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(e.coef) // Scale scale := 2 * MaxScale @@ -1741,8 +1781,13 @@ func (d Decimal) cmpFint(e Decimal) (int, error) { // cmpBint compares decimals using *big.Int arithmetic. func (d Decimal) cmpBint(e Decimal) int { - dcoef := d.coef.bint() - ecoef := e.coef.bint() + dcoef := getBint() + defer putBint(dcoef) + dcoef.setFint(d.coef) + + ecoef := getBint() + defer putBint(ecoef) + ecoef.setFint(e.coef) // Alignment switch { diff --git a/decimal_test.go b/decimal_test.go index aa129b1..8308868 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math" + "math/big" "testing" "unsafe" ) @@ -104,7 +105,7 @@ func TestNewFromInt64(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {0, 0, 0, "0"}, {0, 0, 19, "0"}, // Negatives @@ -167,7 +168,7 @@ func TestNewFromFloat64(t *testing.T) { f float64 want string }{ - // Zeroes + // Zeros {-0, "0"}, {0, "0"}, {0.0, "0"}, @@ -425,6 +426,17 @@ func TestDecimal_String(t *testing.T) { {false, maxCoef, 2, "99999999999999999.99"}, {false, maxCoef, 3, "9999999999999999.999"}, {false, maxCoef, 19, "0.9999999999999999999"}, + + // Exported Constants + {NegOne.neg, NegOne.coef, int(NegOne.scale), "-1"}, + {Zero.neg, Zero.coef, int(Zero.scale), "0"}, + {One.neg, One.coef, int(One.scale), "1"}, + {Two.neg, Two.coef, int(Two.scale), "2"}, + {Ten.neg, Ten.coef, int(Ten.scale), "10"}, + {Hundred.neg, Hundred.coef, int(Hundred.scale), "100"}, + {Thousand.neg, Thousand.coef, int(Thousand.scale), "1000"}, + {E.neg, E.coef, int(E.scale), "2.718281828459045235"}, + {Pi.neg, Pi.coef, int(Pi.scale), "3.141592653589793238"}, } for _, tt := range tests { d, err := newSafe(tt.neg, tt.coef, tt.scale) @@ -474,7 +486,7 @@ func TestDecimal_Int64(t *testing.T) { wantWhole, wantFrac int64 wantOk bool }{ - // Zeroes + // Zeros {"0", 0, 0, 0, true}, {"0.0", 1, 0, 0, true}, {"00.0", 1, 0, 0, true}, @@ -825,7 +837,7 @@ func TestDecimal_Rescale(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", 0, "0"}, {"0", 1, "0.0"}, {"0", 2, "0.00"}, @@ -981,7 +993,7 @@ func TestDecimal_Pad(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", 0, "0"}, {"0", 1, "0.0"}, {"0", 2, "0.00"}, @@ -1050,7 +1062,7 @@ func TestDecimal_Round(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", -1, "0"}, {"0", 0, "0"}, {"0", 1, "0"}, @@ -1136,7 +1148,7 @@ func TestDecimal_Trunc(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", -1, "0"}, {"0", 0, "0"}, {"0", 1, "0"}, @@ -1221,7 +1233,7 @@ func TestDecimal_Ceil(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", -1, "0"}, {"0", 0, "0"}, {"0", 1, "0"}, @@ -1317,7 +1329,7 @@ func TestDecimal_Floor(t *testing.T) { scale int want string }{ - // Zeroes + // Zeros {"0", -1, "0"}, {"0", 0, "0"}, {"0", 1, "0"}, @@ -1863,7 +1875,7 @@ func TestDecimal_Pow(t *testing.T) { power int want string }{ - // Zeroes + // Zeros {"0", 0, "1"}, {"0", 1, "0"}, {"0", 2, "0"}, @@ -2707,21 +2719,30 @@ var corpus = []struct { func FuzzParse(f *testing.F) { for _, c := range corpus { - d, err := newSafe(c.neg, fint(c.coef), c.scale) - if err != nil { - continue + for s := 0; s <= MaxScale; s++ { + d, err := newSafe(c.neg, fint(c.coef), c.scale) + if err != nil { + continue + } + f.Add(d.String(), s) } - f.Add(d.String()) } f.Fuzz( - func(t *testing.T, num string) { - d, err := Parse(num) + func(t *testing.T, num string, scale int) { + got, err := parseFint(num, scale) if err != nil { t.Skip() return } - _ = d.String() + want, err := parseBint(num, scale) + if err != nil { + t.Errorf("parseBint(%q) failed: %v", num, err) + return + } + if got.CmpTotal(want) != 0 { + t.Errorf("parseBint(%q) = %q, whereas parseFint(%q) = %q", num, want, num, got) + } }, ) } @@ -3179,7 +3200,7 @@ func FuzzDecimalNew(f *testing.F) { t.Skip() return } - want, err := newFromBint(neg, fint(coef).bint(), scale, 0) + want, err := newFromBint(neg, newBintFromFint(fint(coef)), scale, 0) if err != nil { t.Errorf("newDecimalFromBint(%v, %v, %v, 0) failed: %v", neg, coef, scale, err) return @@ -3190,3 +3211,10 @@ func FuzzDecimalNew(f *testing.F) { }, ) } + +// newBintFromFint converts uint64 to *big.Int. +func newBintFromFint(f fint) *bint { + z := new(big.Int) + z.SetUint64(uint64(f)) + return (*bint)(z) +} diff --git a/doc.go b/doc.go index f6b8a31..cc2725e 100644 --- a/doc.go +++ b/doc.go @@ -41,11 +41,11 @@ Here are the ranges for frequently used scales: | Bitcoin | 8 | -99,999,999,999.99999999 | 99,999,999,999.99999999 | | Etherium | 9 | -9,999,999,999.999999999 | 9,999,999,999.999999999 | -Subnormal numbers are not supported to ensure peak performance. +[Subnormal numbers] are not supported to ensure peak performance. Consequently, decimals between -0.00000000000000000005 and 0.00000000000000000005 inclusive are rounded to 0. -Special values such as NaN, Infinity, or negative zeros are not supported. +Special values such as [NaN], [Infinity], or [negative zeros] are not supported. This ensures that arithmetic operations always produce either valid decimals or errors. @@ -164,7 +164,11 @@ Errors are not returned in the following cases: If the result is a decimal between -0.00000000000000000005 and 0.00000000000000000005 inclusive, it will be rounded to 0. +[Infinity]: https://en.wikipedia.org/wiki/Infinity#Computing +[Subnormal numbers]: https://en.wikipedia.org/wiki/Subnormal_number +[NaN]: https://en.wikipedia.org/wiki/NaN [ANSI X3.274-1996 (section 7.4)]: https://speleotrove.com/decimal/dax3274.html [big.Int]: https://pkg.go.dev/math/big#Int +[negative zeros]: https://en.wikipedia.org/wiki/Signed_zero */ package decimal