diff --git a/fix_decimal.go b/fix_decimal.go index a435c79fc..054b182dd 100644 --- a/fix_decimal.go +++ b/fix_decimal.go @@ -15,7 +15,11 @@ package quickfix -import "github.com/shopspring/decimal" +import ( + "math/bits" + + "github.com/shopspring/decimal" +) // FIXDecimal is a FIX Float Value that implements an arbitrary precision fixed-point decimal. Implements FieldValue. type FIXDecimal struct { @@ -25,10 +29,103 @@ type FIXDecimal struct { Scale int32 } +// pow10 holds 10^0 through 10^19, the powers of ten that fit in a uint64. +var pow10 = [...]uint64{ + 1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, +} + func (d FIXDecimal) Write() []byte { + if b, ok := d.appendFast(); ok { + return b + } return []byte(d.Decimal.StringFixed(d.Scale)) } +// appendFast formats the value with Scale decimal places, rounding half away from zero +// as decimal.StringFixed does, without going through big.Int arithmetic or intermediate strings. +// It reports false when the value or scale is outside the range it handles, and the caller must fall back. +func (d FIXDecimal) appendFast() ([]byte, bool) { + scale := d.Scale + if scale < 0 || int(scale) >= len(pow10) { + return nil, false + } + + coef := d.Decimal.Coefficient() + if !coef.IsInt64() { + return nil, false + } + c := coef.Int64() + neg := c < 0 + mag := uint64(c) + if neg { + mag = -mag + } + + // The value is mag * 10^exp; shift it so that the last digit of mag is the last decimal place. + shift := int64(d.Decimal.Exponent()) + int64(scale) + switch { + case shift > 0: + if shift >= int64(len(pow10)) { + if mag == 0 { + break + } + return nil, false + } + hi, lo := bits.Mul64(mag, pow10[shift]) + if hi != 0 { + return nil, false + } + mag = lo + case shift < 0: + if -shift >= int64(len(pow10)) { + // Everything is rounded away. + mag = 0 + break + } + div := pow10[-shift] + q, r := mag/div, mag%div + if r >= div-r { // r >= div/2, without overflowing for div > 1<<63 + q++ + } + mag = q + } + + var digits [20]byte + n := len(digits) + for mag > 0 || n > len(digits)-int(scale)-1 { + n-- + digits[n] = byte('0' + mag%10) + mag /= 10 + } + intLen := len(digits) - n - int(scale) + isZero := true + for _, ch := range digits[n:] { + if ch != '0' { + isZero = false + break + } + } + + size := len(digits) - n + if scale > 0 { + size++ + } + if neg && !isZero { + size++ + } + b := make([]byte, 0, size) + if neg && !isZero { + b = append(b, '-') + } + b = append(b, digits[n:n+intLen]...) + if scale > 0 { + b = append(b, '.') + b = append(b, digits[n+intLen:]...) + } + return b, true +} + func (d *FIXDecimal) Read(bytes []byte) (err error) { d.Decimal, err = decimal.NewFromString(string(bytes)) return diff --git a/fix_decimal_test.go b/fix_decimal_test.go index f893cdc7b..d442458cb 100644 --- a/fix_decimal_test.go +++ b/fix_decimal_test.go @@ -16,6 +16,8 @@ package quickfix import ( + "math" + "math/rand" "testing" "github.com/shopspring/decimal" @@ -62,6 +64,34 @@ func TestFIXDecimalWrite(t *testing.T) { } } +func TestFIXDecimalWriteMatchesStringFixed(t *testing.T) { + coefs := []int64{ + 0, 1, -1, 5, -5, 9, 10, 49, 50, 51, -49, -50, -51, 99, 100, 995, -995, 1243456, -1243456, + math.MaxInt32, math.MinInt32, math.MaxInt64, math.MinInt64, math.MaxInt64 - 1, math.MinInt64 + 1, + 1e18, -1e18, 9999999999999999999 / 10, + } + r := rand.New(rand.NewSource(1)) + for i := 0; i < 200; i++ { + coefs = append(coefs, r.Int63()>>uint(r.Intn(63)), -(r.Int63() >> uint(r.Intn(63)))) + } + + for _, c := range coefs { + for exp := int32(-25); exp <= 25; exp++ { + for _, scale := range []int32{-2, 0, 1, 2, 4, 8, 12, 18, 19, 20} { + dec := FIXDecimal{Decimal: decimal.New(c, exp), Scale: scale} + expected := dec.Decimal.StringFixed(scale) + assert.Equal(t, expected, string(dec.Write()), "coef=%d exp=%d scale=%d", c, exp, scale) + } + } + } + + big := decimal.RequireFromString("123456789012345678901234567890.123456789") + for _, scale := range []int32{0, 4, 9, 12} { + dec := FIXDecimal{Decimal: big, Scale: scale} + assert.Equal(t, big.StringFixed(scale), string(dec.Write())) + } +} + func TestFIXDecimalRead(t *testing.T) { var tests = []struct { bytes string