diff --git a/decimal.go b/decimal.go index 256f36a..c56ac94 100644 --- a/decimal.go +++ b/decimal.go @@ -13,15 +13,17 @@ import ( // currently support 12 precision, this is tunnable, // more precision => smaller maxInt // less precision => bigger maxInt -const precision = 12 -const scale = 1e12 -const maxInt int64 = int64(math.MaxInt64) / scale -const minInt int64 = int64(math.MinInt64) / scale -const maxIntInFixed int64 = maxInt * scale -const minIntInFixed int64 = minInt * scale -const a1000InFixed int64 = 1000 * scale -const aNeg1000InFixed int64 = -1000 * scale -const aCentInFixed int64 = scale / 100 +var ( + precision int32 = 12 + scale int64 = 1e12 + maxInt int64 = int64(math.MaxInt64) / scale + minInt int64 = int64(math.MinInt64) / scale + maxIntInFixed int64 = maxInt * scale + minIntInFixed int64 = minInt * scale + a1000InFixed int64 = 1000 * scale + aNeg1000InFixed int64 = -1000 * scale + aCentInFixed int64 = scale / 100 +) var pow10Table []int64 = []int64{ 1e0, 1e1, 1e2, 1e3, 1e4, @@ -54,6 +56,18 @@ func init() { } } +func ChangeMaxPrecision(newPrecision int) { + precision = int32(newPrecision) + scale = int64(math.Pow10(newPrecision)) + maxInt = int64(math.MaxInt64) / scale + minInt = int64(math.MinInt64) / scale + maxIntInFixed = maxInt * scale + minIntInFixed = minInt * scale + a1000InFixed = 1000 * scale + aNeg1000InFixed = -1000 * scale + aCentInFixed = scale / 100 +} + // API // APIs are marked as either "optimized" or "fallbacked" @@ -118,7 +132,7 @@ func Min(first Decimal, rest ...Decimal) Decimal { // optimized: // New returns a new fixed-point decimal, value * 10 ^ exp. func New(value int64, exp int32) Decimal { - if exp >= -12 { + if exp >= -precision { if exp <= 0 { s := pow10Table[-exp] if value >= minInt*s && value <= maxInt*s { @@ -838,8 +852,9 @@ func (d Decimal) String() string { // "-9223372.000000000000" => max length = 21 bytes var s [21]byte - start := 7 - end := 8 + dotStart := int(int32(20) - precision) + start := dotStart - 1 + end := dotStart var ufixed uint64 if d.fixed >= 0 { @@ -848,8 +863,8 @@ func (d Decimal) String() string { ufixed = uint64(d.fixed * -1) } - integerPart := ufixed / scale - fractionalPart := ufixed % scale + integerPart := ufixed / uint64(scale) + fractionalPart := ufixed % uint64(scale) // integer part if integerPart == 0 { @@ -865,14 +880,14 @@ func (d Decimal) String() string { // fractional part if fractionalPart > 0 { - s[8] = '.' - for i := 20; i > 8; i-- { + s[dotStart] = '.' + for i := 20; i > dotStart; i-- { is := fractionalPart % 10 fractionalPart /= 10 if is != 0 { s[i] = byte(is + '0') end = i + 1 - for j := i - 1; j > 8; j-- { + for j := i - 1; j > dotStart; j-- { s[j] = byte(fractionalPart%10 + '0') fractionalPart /= 10 } @@ -934,12 +949,12 @@ func (d Decimal) Tan() Decimal { // optimized: // Truncate truncates off digits from the number, without rounding. -func (d Decimal) Truncate(precision int32) Decimal { +func (d Decimal) Truncate(wantPrecision int32) Decimal { if d.fallback == nil { - s := pow10Table[12-precision] + s := pow10Table[precision-wantPrecision] return Decimal{fixed: d.fixed / s * s} } - return newFromDecimal(d.asFallback().Truncate(precision)) + return newFromDecimal(d.asFallback().Truncate(wantPrecision)) } // fallback: @@ -1155,7 +1170,7 @@ func parseFixed[T string | []byte](v T) (int64, bool) { } else if c == '.' { // handle fractional part s := v[i+1:] - if len(s) > 12 { + if int32(len(s)) > precision { // out of range return 0, false } @@ -1168,7 +1183,7 @@ func parseFixed[T string | []byte](v T) (int64, bool) { return 0, false } } - fixed *= pow10Table[12-len(s)] + fixed *= pow10Table[precision-int32(len(s))] if negative { return -fixed, true } else { @@ -1190,7 +1205,7 @@ func parseFixed[T string | []byte](v T) (int64, bool) { func (d Decimal) asFallback() decimal.Decimal { if d.fallback == nil { x := big.NewInt(d.fixed) - return decimal.NewFromBigInt(x, -12) + return decimal.NewFromBigInt(x, -precision) } return *d.fallback } @@ -1298,7 +1313,7 @@ func div(x, y int64) (int64, bool) { } fz := float64(x) / float64(y) - z := int64(fz * scale) + z := int64(fz * float64(scale)) // this `mul` check is to ensure we do not // lose precision from previous float64 operations. diff --git a/decimal_test.go b/decimal_test.go index f78b571..dbbef61 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -55,44 +55,150 @@ func requireCompatible2[T any](t *testing.T, f func(input1, input2 string) (x, y } func TestDecimal(t *testing.T) { + // alpacadecimal.ChangeMaxPrecision(12) + one := alpacadecimal.NewFromInt(1) - two := alpacadecimal.NewFromInt(2) - three := alpacadecimal.NewFromInt(3) - t.Run("Zero", func(t *testing.T) { - require.Equal(t, "0", alpacadecimal.Zero.String()) - require.True(t, alpacadecimal.Zero.Equal(alpacadecimal.Zero)) - require.True(t, alpacadecimal.Zero.GreaterThan(alpacadecimal.NewFromInt(-1))) - require.True(t, alpacadecimal.Zero.LessThan(alpacadecimal.NewFromInt(1))) + commontFunctionTest(t) + + t.Run("New", func(t *testing.T) { + { + x := alpacadecimal.New(1, -13) + shouldEqual(t, x, alpacadecimal.RequireFromString("0.0000000000001")) + require.False(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(1_000_000_000_000, -12) + shouldEqual(t, x, one) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(1, -3) + shouldEqual(t, x, alpacadecimal.NewFromFloat(0.001)) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(3, 0) + shouldEqual(t, x, alpacadecimal.RequireFromString("3")) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(3, 0) + shouldEqual(t, x, alpacadecimal.RequireFromString("3")) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(4, 1) + shouldEqual(t, x, alpacadecimal.RequireFromString("40")) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(5, 6) + shouldEqual(t, x, alpacadecimal.RequireFromString("5000000")) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(-9, 6) + shouldEqual(t, x, alpacadecimal.RequireFromString("-9000000")) + require.True(t, x.IsOptimized()) + } + + { + x := alpacadecimal.New(1, 7) + shouldEqual(t, x, alpacadecimal.RequireFromString("10000000")) + require.False(t, x.IsOptimized()) + } }) - t.Run("RescalePair", func(t *testing.T) { - d1, d2 := alpacadecimal.RescalePair(one, two) - shouldEqual(t, d1, one) - shouldEqual(t, d2, two) + t.Run("Decimal.GetFixed", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.Equal(t, int64(123_000_000_000_000), x.GetFixed()) + + y := alpacadecimal.NewFromInt(1234567890) + require.Equal(t, int64(0), y.GetFixed()) }) - t.Run("Avg", func(t *testing.T) { - shouldEqual(t, alpacadecimal.Avg(one, two, three), two) + t.Run("Decimal.GetFallback", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.Nil(t, x.GetFallback()) + + y := alpacadecimal.NewFromInt(1234567890) + require.NotNil(t, y.GetFallback()) + require.Equal(t, "1234567890", y.GetFallback().String()) }) - t.Run("Max", func(t *testing.T) { - require.True(t, alpacadecimal.Max(one, two, three).Equal(three)) + t.Run("Decimal.IsOptimized", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.True(t, x.IsOptimized()) + + y := alpacadecimal.NewFromInt(1234567890) + require.False(t, y.IsOptimized()) }) - t.Run("Min", func(t *testing.T) { - require.True(t, alpacadecimal.Min(one, two, three).Equal(one)) + t.Run("Decimal.Exponent", func(t *testing.T) { + require.Equal(t, int32(-12), alpacadecimal.RequireFromString("1").Exponent()) }) + t.Run("Decimal.Truncate", func(t *testing.T) { + x := alpacadecimal.NewFromFloat(1.234) + require.Equal(t, "1", x.Truncate(0).String()) + require.Equal(t, "1.2", x.Truncate(1).String()) + require.Equal(t, "1.23", x.Truncate(2).String()) + require.Equal(t, "1.234", x.Truncate(3).String()) + require.Equal(t, "1.234", x.Truncate(4).String()) + + y := alpacadecimal.NewFromFloat(-1.234) + require.Equal(t, "-1", y.Truncate(0).String()) + require.Equal(t, "-1.2", y.Truncate(1).String()) + require.Equal(t, "-1.23", y.Truncate(2).String()) + require.Equal(t, "-1.234", y.Truncate(3).String()) + require.Equal(t, "-1.234", y.Truncate(4).String()) + + for i := int32(0); i < 10; i++ { + requireCompatible(t, func(input string) (string, string) { + x := alpacadecimal.RequireFromString(input).Truncate(i).String() + y := decimal.RequireFromString(input).Truncate(i).String() + return x, y + }) + } + }) +} + +func TestChangePrecission(t *testing.T) { + alpacadecimal.ChangeMaxPrecision(6) + + x := alpacadecimal.NewFromInt(123) + require.Equal(t, int32(-6), x.Exponent()) + require.Equal(t, "123000000", x.Coefficient().String()) + require.Equal(t, int64(123000000), x.CoefficientInt64()) + require.Equal(t, 9, x.NumDigits()) + + y := decimal.NewFromInt(123) + require.Equal(t, int32(0), y.Exponent()) + require.Equal(t, "123", y.Coefficient().String()) + require.Equal(t, int64(123), y.CoefficientInt64()) + require.Equal(t, 3, y.NumDigits()) + + commontFunctionTest(t) + + one := alpacadecimal.NewFromInt(1) + t.Run("New", func(t *testing.T) { { - x := alpacadecimal.New(1, -13) - shouldEqual(t, x, alpacadecimal.RequireFromString("0.0000000000001")) + x := alpacadecimal.New(1, -7) + shouldEqual(t, x, alpacadecimal.RequireFromString("0.0000001")) require.False(t, x.IsOptimized()) } { - x := alpacadecimal.New(1_000_000_000_000, -12) + x := alpacadecimal.New(1_000_000, -6) shouldEqual(t, x, one) require.True(t, x.IsOptimized()) } @@ -140,6 +246,82 @@ func TestDecimal(t *testing.T) { } }) + t.Run("Decimal.GetFixed", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.Equal(t, int64(123_000_000), x.GetFixed()) + + y := alpacadecimal.NewFromInt(10_000_000_000_000) + require.Equal(t, int64(0), y.GetFixed()) + }) + + t.Run("Decimal.GetFallback", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.Nil(t, x.GetFallback()) + + y := alpacadecimal.NewFromInt(10_000_000_000_000) + require.NotNil(t, y.GetFallback()) + require.Equal(t, "10000000000000", y.GetFallback().String()) + }) + + t.Run("Decimal.IsOptimized", func(t *testing.T) { + x := alpacadecimal.NewFromInt(123) + require.True(t, x.IsOptimized()) + + y := alpacadecimal.NewFromInt(10_000_000_000_000) + require.False(t, y.IsOptimized()) + }) + + t.Run("Decimal.Exponent", func(t *testing.T) { + require.Equal(t, int32(-6), alpacadecimal.RequireFromString("1").Exponent()) + }) +} + +func TestSpecialAPIs(t *testing.T) { + alpacadecimal.ChangeMaxPrecision(12) + + x := alpacadecimal.NewFromInt(123) + require.Equal(t, int32(-12), x.Exponent()) + require.Equal(t, "123000000000000", x.Coefficient().String()) + require.Equal(t, int64(123000000000000), x.CoefficientInt64()) + require.Equal(t, 15, x.NumDigits()) + + y := decimal.NewFromInt(123) + require.Equal(t, int32(0), y.Exponent()) + require.Equal(t, "123", y.Coefficient().String()) + require.Equal(t, int64(123), y.CoefficientInt64()) + require.Equal(t, 3, y.NumDigits()) +} + +func commontFunctionTest(t *testing.T) { + one := alpacadecimal.NewFromInt(1) + two := alpacadecimal.NewFromInt(2) + three := alpacadecimal.NewFromInt(3) + + t.Run("Zero", func(t *testing.T) { + require.Equal(t, "0", alpacadecimal.Zero.String()) + require.True(t, alpacadecimal.Zero.Equal(alpacadecimal.Zero)) + require.True(t, alpacadecimal.Zero.GreaterThan(alpacadecimal.NewFromInt(-1))) + require.True(t, alpacadecimal.Zero.LessThan(alpacadecimal.NewFromInt(1))) + }) + + t.Run("RescalePair", func(t *testing.T) { + d1, d2 := alpacadecimal.RescalePair(one, two) + shouldEqual(t, d1, one) + shouldEqual(t, d2, two) + }) + + t.Run("Avg", func(t *testing.T) { + shouldEqual(t, alpacadecimal.Avg(one, two, three), two) + }) + + t.Run("Max", func(t *testing.T) { + require.True(t, alpacadecimal.Max(one, two, three).Equal(three)) + }) + + t.Run("Min", func(t *testing.T) { + require.True(t, alpacadecimal.Min(one, two, three).Equal(one)) + }) + t.Run("NewFromBigInt", func(t *testing.T) { input := big.NewInt(123) @@ -454,10 +636,6 @@ func TestDecimal(t *testing.T) { // } }) - t.Run("Decimal.Exponent", func(t *testing.T) { - require.Equal(t, int32(-12), alpacadecimal.RequireFromString("1").Exponent()) - }) - t.Run("Decimal.Float64", func(t *testing.T) { f, exact := alpacadecimal.RequireFromString("1.0").Float64() require.True(t, exact) @@ -981,30 +1159,6 @@ func TestDecimal(t *testing.T) { }) }) - t.Run("Decimal.Truncate", func(t *testing.T) { - x := alpacadecimal.NewFromFloat(1.234) - require.Equal(t, "1", x.Truncate(0).String()) - require.Equal(t, "1.2", x.Truncate(1).String()) - require.Equal(t, "1.23", x.Truncate(2).String()) - require.Equal(t, "1.234", x.Truncate(3).String()) - require.Equal(t, "1.234", x.Truncate(4).String()) - - y := alpacadecimal.NewFromFloat(-1.234) - require.Equal(t, "-1", y.Truncate(0).String()) - require.Equal(t, "-1.2", y.Truncate(1).String()) - require.Equal(t, "-1.23", y.Truncate(2).String()) - require.Equal(t, "-1.234", y.Truncate(3).String()) - require.Equal(t, "-1.234", y.Truncate(4).String()) - - for i := int32(0); i < 10; i++ { - requireCompatible(t, func(input string) (string, string) { - x := alpacadecimal.RequireFromString(input).Truncate(i).String() - y := decimal.RequireFromString(input).Truncate(i).String() - return x, y - }) - } - }) - t.Run("Decimal.UnmarshalBinary", func(t *testing.T) { x := alpacadecimal.NewFromInt(123) data, err := x.MarshalBinary() @@ -1082,32 +1236,6 @@ func TestDecimal(t *testing.T) { checkFloat(12345.123456789, "12345.123456789") checkFloat(-12345.123456789, "-12345.123456789") }) - - t.Run("Decimal.GetFixed", func(t *testing.T) { - x := alpacadecimal.NewFromInt(123) - require.Equal(t, int64(123_000_000_000_000), x.GetFixed()) - - y := alpacadecimal.NewFromInt(1234567890) - require.Equal(t, int64(0), y.GetFixed()) - }) - - t.Run("Decimal.GetFallback", func(t *testing.T) { - x := alpacadecimal.NewFromInt(123) - require.Nil(t, x.GetFallback()) - - y := alpacadecimal.NewFromInt(1234567890) - require.NotNil(t, y.GetFallback()) - require.Equal(t, "1234567890", y.GetFallback().String()) - }) - - t.Run("Decimal.IsOptimized", func(t *testing.T) { - x := alpacadecimal.NewFromInt(123) - require.True(t, x.IsOptimized()) - - y := alpacadecimal.NewFromInt(1234567890) - require.False(t, y.IsOptimized()) - }) - t.Run("NullDecimal", func(t *testing.T) { var _ alpacadecimal.NullDecimal = alpacadecimal.NullDecimal{Decimal: alpacadecimal.NewFromInt(1), Valid: true} var _ alpacadecimal.NullDecimal = alpacadecimal.NullDecimal{Valid: false} @@ -1272,17 +1400,3 @@ func TestDecimal(t *testing.T) { } }) } - -func TestSpecialAPIs(t *testing.T) { - x := alpacadecimal.NewFromInt(123) - require.Equal(t, int32(-12), x.Exponent()) - require.Equal(t, "123000000000000", x.Coefficient().String()) - require.Equal(t, int64(123000000000000), x.CoefficientInt64()) - require.Equal(t, 15, x.NumDigits()) - - y := decimal.NewFromInt(123) - require.Equal(t, int32(0), y.Exponent()) - require.Equal(t, "123", y.Coefficient().String()) - require.Equal(t, int64(123), y.CoefficientInt64()) - require.Equal(t, 3, y.NumDigits()) -}