Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion fix_decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions fix_decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package quickfix

import (
"math"
"math/rand"
"testing"

"github.com/shopspring/decimal"
Expand Down Expand Up @@ -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
Expand Down
Loading