Add AddDecimal support to pql

This allows us to add two Decimals together as long
as the result doesn't overflow int64 either in value or in scale.
This commit is contained in:
reesporte 2022-06-01 15:47:57 -05:00 • committed by reesporte
parent 3750fea632
commit 50787fd37a
2 changed files with 119 additions and 1 deletions

View file

@ -4,6 +4,7 @@ package pql
import (
"fmt"
"math"
"math/big"
"strconv"
"strings"
@ -79,6 +80,40 @@ func MinMax(scale int64) (Decimal, Decimal) {
return min, max
}
// AddDecimal adds a and b together and returns a new Decimal with the computed sum.
//
// If the Scale of a and b don't match, the returned Decimal will have the
// smallest Scale needed to precisely represent the sum.
func AddDecimal(a, b Decimal) (Decimal, bool) {
av, bv := big.NewInt(a.Value), big.NewInt(b.Value)
// if the scales dont match,
// we add zeros to the end of the one with the smaller scale until they match
// or we overflow
// then we add the values and return the decimal
as, bs := a.Scale, b.Scale
var ok bool
if a.Scale > b.Scale {
av, bv = bv, av
as, bs = bs, as
}
for as < bs {
av = av.Mul(av, big.NewInt(10))
as++
}
av = av.Add(av, bv)
ret, ok := av.Int64(), av.IsInt64()
if !ok {
return Decimal{}, ok
}
return Decimal{
Value: ret,
Scale: as,
}, ok
}
// LessThan returns true if d < d2.
func (d Decimal) LessThan(d2 Decimal) bool {
return d.lessThan(d2, false)
@ -388,7 +423,6 @@ func ParseDecimal(s string) (Decimal, error) {
} else {
return Decimal{}, errors.Errorf("value out of range: %s", mantissa)
}
// We have to use ParseUint here (as opposed to ParseInt) because
// math.MinInt64 is a valid value, but its absolute value is not.
// So this allows us to handle that one value without overflow, and

View file

@ -3,6 +3,7 @@ package pql_test
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
@ -247,3 +248,86 @@ func TestDecimal(t *testing.T) {
})
})
}
type testAddDecimalCase struct {
a pql.Decimal
b pql.Decimal
exp pql.Decimal
notOk bool
}
func TestAddDecimal(t *testing.T) {
toDecimal := func(a interface{}) pql.Decimal {
switch ac := a.(type) {
case string:
return mustParse(t, ac)
case pql.Decimal:
return ac
default:
t.Fatalf("cannot support type %T", ac)
}
return pql.Decimal{}
}
newTestAddDecimalCase := func(a, b, exp interface{}, notOk bool) testAddDecimalCase {
return testAddDecimalCase{
a: toDecimal(a),
b: toDecimal(b),
exp: toDecimal(exp),
notOk: notOk,
}
}
tests := []testAddDecimalCase{
newTestAddDecimalCase("40.37", "40.37", "80.74", false),
newTestAddDecimalCase("18.5", "9.25", "27.75", false),
newTestAddDecimalCase("18.50", "9.25", "27.75", false),
newTestAddDecimalCase("-18.50", "-9.25", "-27.75", false),
newTestAddDecimalCase("-40.37", "40.37", "0", false),
newTestAddDecimalCase("-50.38", "-50.38", "-100.76", false),
newTestAddDecimalCase("-40.381", "-40.38", "-80.761", false),
newTestAddDecimalCase("-40.3700000000000000000001", "-50.38", "-90.7500000000000000000001", false),
newTestAddDecimalCase("10.37000000000000001", "-10", "0.37000000000000001", false),
// this is a weird one. because we reduce precision when we parse strings, we
// expect the value to be smaller than it really should be if you did
// the math yourself.
//
// there's not really a good way around this unless we use math/big.Int to represent
// our Value/Scale.
//
// but if we did this, to quote seebs: "i suspect that
// our performance would tank so badly by the time we were doing >63-bit
// numbers that there's no real-world benefit to us."
newTestAddDecimalCase("10.370000000000000001", "-10", "0.37", false),
newTestAddDecimalCase("-9223372036854775807", "-9223372036854775807", pql.Decimal{}, true),
newTestAddDecimalCase("9223372036854775807", "9223372036854775807", pql.Decimal{}, true),
newTestAddDecimalCase(pql.Decimal{Value: 0, Scale: 0}, pql.Decimal{Value: 1, Scale: 4}, pql.Decimal{Value: 1, Scale: 4}, false),
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
if got, ok := pql.AddDecimal(test.a, test.b); !got.EqualTo(test.exp) {
t.Logf("%#v + %#v, expected %#v, got %#v", test.a, test.b, test.exp, got)
t.Errorf("%v + %v, expected %v, got %v", test.a, test.b, test.exp, got)
} else if !ok != test.notOk {
t.Errorf("expected %v, got %v", test.notOk, ok)
}
if got, ok := pql.AddDecimal(test.b, test.a); !got.EqualTo(test.exp) {
t.Logf("%#v + %#v, expected %#v, got %#v", test.a, test.b, test.exp, got)
t.Errorf("%v + %v, expected %v, got %v", test.b, test.a, test.exp, got)
} else if !ok != test.notOk {
t.Errorf("expected %v, got %v", test.notOk, ok)
}
})
}
}
func mustParse(t *testing.T, num string) pql.Decimal {
t.Helper()
d, err := pql.ParseDecimal(num)
if err != nil {
t.Fatalf("unexpected error parsing %s to pql.Decimal: %v", num, err)
}
return d
}