mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-08 03:47:51 +00:00
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:
parent
3750fea632
commit
50787fd37a
2 changed files with 119 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue