From 01bb2034de74b9ea8251025e942a5806dbec1dbf Mon Sep 17 00:00:00 2001 From: mwoss Date: Mon, 23 Jun 2025 21:07:38 +0200 Subject: [PATCH 01/52] Allow CI jobs on PR creation and sync --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6c836..11a10b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: ci on: push: - branches: - - master + branches: [ master ] pull_request: + branches: [ master ] + types: [ opened, synchronize ] jobs: ci-job: runs-on: ubuntu-latest From 08afb35517362df21914209a026f65bb33275fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Lo=C5=BEys?= <42966213+PauliusLozys@users.noreply.github.com> Date: Wed, 25 Jun 2025 02:04:23 +0300 Subject: [PATCH 02/52] `UnmarshalJSON`, `Scan` and `NewFromString` performance improvements (#403) * `UnmarshalJSON` and `NewFromString` performance improvements This PR improves `UnmarshalJSON` performance by reducing unnecessary allocation caused by `unquoteIfQuoted` function. This also touches on `Scan` method to split `default` case into `string` and `[]byte` cases. This PR also slightly touches `NewFromString` function by making so scientific notation and dots are checked in a single loop. * Add malformed scientific notation check and tests --- decimal.go | 82 ++++++++++++++++++++----------------------- decimal_bench_test.go | 12 +++++++ decimal_test.go | 14 ++++++++ 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/decimal.go b/decimal.go index a37a230..b48cfa9 100644 --- a/decimal.go +++ b/decimal.go @@ -182,8 +182,26 @@ func NewFromString(value string) (Decimal, error) { var intString string var exp int64 - // Check if number is using scientific notation - eIndex := strings.IndexAny(value, "Ee") + // Check if number is using scientific notation and find dots + eIndex := -1 + pIndex := -1 + for i, r := range value { + if r == 'E' || r == 'e' { + if eIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", value) + } + eIndex = i + continue + } + + if r == '.' { + if pIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value) + } + pIndex = i + } + } + if eIndex != -1 { expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32) if err != nil { @@ -196,23 +214,12 @@ func NewFromString(value string) (Decimal, error) { exp = expInt } - pIndex := -1 - vLen := len(value) - for i := 0; i < vLen; i++ { - if value[i] == '.' { - if pIndex > -1 { - return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value) - } - pIndex = i - } - } - if pIndex == -1 { // There is no decimal point, we can just parse the original string as // an int intString = value } else { - if pIndex+1 < vLen { + if pIndex+1 < len(value) { intString = value[:pIndex] + value[pIndex+1:] } else { intString = value[:pIndex] @@ -1766,15 +1773,10 @@ func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { return nil } - str, err := unquoteIfQuoted(decimalBytes) - if err != nil { - return fmt.Errorf("error decoding string '%s': %s", decimalBytes, err) - } - - decimal, err := NewFromString(str) + decimal, err := NewFromString(unquoteIfQuoted(string(decimalBytes))) *d = decimal if err != nil { - return fmt.Errorf("error decoding string '%s': %s", str, err) + return fmt.Errorf("error decoding string '%s': %s", string(decimalBytes), err) } return nil } @@ -1852,14 +1854,18 @@ func (d *Decimal) Scan(value interface{}) error { *d = NewFromUint64(v) return nil - default: - // default is trying to interpret value stored as string - str, err := unquoteIfQuoted(v) - if err != nil { - return err - } - *d, err = NewFromString(str) + case string: + var err error + *d, err = NewFromString(unquoteIfQuoted(v)) return err + + case []byte: + var err error + *d, err = NewFromString(unquoteIfQuoted(string(v))) + return err + + default: + return fmt.Errorf("could not convert value '%+v' to any known type", value) } } @@ -2021,23 +2027,13 @@ func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { return d1, d2 } -func unquoteIfQuoted(value interface{}) (string, error) { - var bytes []byte - - switch v := value.(type) { - case string: - bytes = []byte(v) - case []byte: - bytes = v - default: - return "", fmt.Errorf("could not convert value '%+v' to byte array of type '%T'", value, value) - } - +func unquoteIfQuoted(value string) string { // If the amount is quoted, strip the quotes - if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' { - bytes = bytes[1 : len(bytes)-1] + if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { + return value[1 : len(value)-1] } - return string(bytes), nil + + return value } // NullDecimal represents a nullable decimal with compatibility for diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 34e038f..4e46251 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -312,3 +312,15 @@ func BenchmarkDecimal_ExpTaylor(b *testing.B) { _, _ = d.ExpTaylor(10) } } + +func BenchmarkDecimal_UnmarshalJSON(b *testing.B) { + b.ResetTimer() + + bstr := []byte("1234.56789") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = (&Decimal{}).UnmarshalJSON(bstr) + } +} diff --git a/decimal_test.go b/decimal_test.go index d398f2d..d288821 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -80,6 +80,11 @@ var testTableScientificNotation = map[string]string{ "123.456e10": "1234560000000", } +var testMalformedDecimalStrings = map[string]error{ + "1ee10": fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", "1ee10"), + "123.45.66": fmt.Errorf("can't convert %s to decimal: too many .s", "123.45.66"), +} + func init() { for _, s := range testTable { s.exact = strconv.FormatFloat(s.float, 'f', 1500, 64) @@ -239,6 +244,15 @@ func TestNewFromString(t *testing.T) { d.value.String(), d.exp) } } + + for s, e := range testMalformedDecimalStrings { + _, err := NewFromString(s) + if err == nil { + t.Errorf("expected an error, got nil %s", s) + } else if err.Error() != e.Error() { + t.Errorf("expected %v error, got %v", e, err) + } + } } func TestNewFromFormattedString(t *testing.T) { From e1e92bff939182e69723849c847fe7a665feb421 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Thu, 25 Dec 2025 16:58:32 -0600 Subject: [PATCH 03/52] TrimTrailingZeros and AvoidScientificNotation bool variables (#389) * Added varible TrimTrailingZeros * Scientific Notation support on serialization to honor negative precision scenarios --------- Co-authored-by: vasilbekk Co-authored-by: vasilbekk <70901384+vasilbekk@users.noreply.github.com> --- decimal.go | 67 ++++++++++++++++++++--- decimal_test.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 8 deletions(-) diff --git a/decimal.go b/decimal.go index b48cfa9..3cf7f68 100644 --- a/decimal.go +++ b/decimal.go @@ -65,6 +65,19 @@ var PowPrecisionNegativeExponent = 16 // silently lose precision. var MarshalJSONWithoutQuotes = false +// TrimTrailingZeros specifies whether trailing zeroes should be trimmed from a string representation of decimal. +// If set to true, trailing zeroes will be truncated (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13), +// otherwise trailing zeroes will be preserved (2.00 -> 2.00, 3.11 -> 3.11, 13.000 -> 13.000). +// Setting this value to false can be useful for APIs where exact decimal string representation matters. +var TrimTrailingZeros = true + +// AvoidScientificNotation specifies whether scientific notation should be used when decimal is turned +// into a string that has a "negative" precision. +// +// For example, 1200 rounded to the nearest 100 cannot accurately be shown as "1200" because the last two +// digits are unknown. With this set to false, that number would be expressed as "1.2E3" instead. +var AvoidScientificNotation = true + // ExpMaxIterations specifies the maximum number of iterations needed to calculate // precise natural exponent value using ExpHullAbrham method. var ExpMaxIterations = 1000 @@ -1476,7 +1489,7 @@ func (d Decimal) InexactFloat64() float64 { // // -12.345 func (d Decimal) String() string { - return d.string(true) + return d.string(TrimTrailingZeros, AvoidScientificNotation) } // StringFixed returns a rounded fixed-point string with places digits after @@ -1490,10 +1503,12 @@ func (d Decimal) String() string { // NewFromFloat(5.45).StringFixed(1) // output: "5.5" // NewFromFloat(5.45).StringFixed(2) // output: "5.45" // NewFromFloat(5.45).StringFixed(3) // output: "5.450" -// NewFromFloat(545).StringFixed(-1) // output: "550" +// NewFromFloat(545).StringFixed(-1) // output: "540" +// +// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. func (d Decimal) StringFixed(places int32) string { rounded := d.Round(places) - return rounded.string(false) + return rounded.string(false, true) } // StringFixedBank returns a banker rounded fixed-point string with places digits @@ -1508,16 +1523,20 @@ func (d Decimal) StringFixed(places int32) string { // NewFromFloat(5.45).StringFixedBank(2) // output: "5.45" // NewFromFloat(5.45).StringFixedBank(3) // output: "5.450" // NewFromFloat(545).StringFixedBank(-1) // output: "540" +// +// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. func (d Decimal) StringFixedBank(places int32) string { rounded := d.RoundBank(places) - return rounded.string(false) + return rounded.string(false, true) } // StringFixedCash returns a Swedish/Cash rounded fixed-point string. For // more details see the documentation at function RoundCash. +// +// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. func (d Decimal) StringFixedCash(interval uint8) string { rounded := d.RoundCash(interval) - return rounded.string(false) + return rounded.string(false, true) } // Round rounds the decimal to places decimal places. @@ -1526,7 +1545,7 @@ func (d Decimal) StringFixedCash(interval uint8) string { // Example: // // NewFromFloat(5.45).Round(1).String() // output: "5.5" -// NewFromFloat(545).Round(-1).String() // output: "550" +// NewFromFloat(545).Round(-1).String() // output: "550" (with AvoidScientificNotation, "5.5E2" otherwise) func (d Decimal) Round(places int32) Decimal { if d.exp == -places { return d @@ -1911,10 +1930,17 @@ func (d Decimal) StringScaled(exp int32) string { return d.rescale(exp).String() } -func (d Decimal) string(trimTrailingZeros bool) string { - if d.exp >= 0 { +func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string { + if d.exp == 0 { return d.rescale(0).value.String() } + if d.exp >= 0 { + if avoidScientificNotation { + return d.rescale(0).value.String() + } else { + return d.ScientificNotationString() + } + } abs := new(big.Int).Abs(d.value) str := abs.String() @@ -1956,6 +1982,31 @@ func (d Decimal) string(trimTrailingZeros bool) string { return number } +// ScientificNotationString serializes the decimal into standard scientific notation. +// +// The notation is normalized to have one non-zero digit followed by a decimal point and +// the remaining significant digits followed by "E" and the base-10 exponent. +// +// A zero, which has no significant digits, is simply serialized to "0". +func (d Decimal) ScientificNotationString() string { + exp := int(d.exp) + intStr := new(big.Int).Abs(d.value).String() + if intStr == "0" { + return intStr + } + first := intStr[0] + var remaining string + if len(intStr) > 1 { + remaining = "." + intStr[1:] + exp = exp + len(intStr) - 1 + } + number := string(first) + remaining + "E" + strconv.Itoa(exp) + if d.value.Sign() < 0 { + return "-" + number + } + return number +} + func (d *Decimal) ensureInitialized() { if d.value == nil { d.value = new(big.Int) diff --git a/decimal_test.go b/decimal_test.go index d288821..164bb30 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3661,3 +3661,141 @@ func ExampleNewFromFloat() { //0.123123123123123 //-10000000000000 } + +func TestDecimal_String(t *testing.T) { + type testData struct { + input string + expected string + } + + tests := []testData{ + {"1.22", "1.22"}, + {"1.00", "1"}, + {"153.192", "153.192"}, + {"999.999", "999.999"}, + {"0.0000000001", "0.0000000001"}, + {"0.0000000000", "0"}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_StringWithTrailing(t *testing.T) { + type testData struct { + input string + expected string + } + + defer func() { + TrimTrailingZeros = true + }() + + TrimTrailingZeros = false + tests := []testData{ + {"1.00", "1.00"}, + {"0.00", "0.00"}, + {"129.123000", "129.123000"}, + {"1.0000E3", "1000.0"}, // 1000 to the nearest tenth + {"10000E-1", "1000.0"}, // 1000 to the nearest tenth + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + x := d.String() + fmt.Println(x) + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) { + type testData struct { + input string + expected string + } + + defer func() { + AvoidScientificNotation = true + }() + AvoidScientificNotation = false + + tests := []testData{ + {"1.0E3", "1.0E3"}, // 1000 to the nearest hundred + {"1.00E3", "1.00E3"}, // 1000 to the nearest ten + {"1.000E3", "1000"}, // 1000 to the nearest one + {"1E3", "1E3"}, // 1000 to the nearest thousand + {"-1E3", "-1E3"}, // -1000 to the nearest thousand + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + x := d.String() + fmt.Println(x) + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_ScientificNotation(t *testing.T) { + type testData struct { + input string + expected string + } + + tests := []testData{ + {"1", "1E0"}, + {"1.0", "1.0E0"}, + {"10", "1.0E1"}, + {"123", "1.23E2"}, + {"1234", "1.234E3"}, + {"-1", "-1E0"}, + {"-10", "-1.0E1"}, + {"-123", "-1.23E2"}, + {"-1234", "-1.234E3"}, + {"0.1", "1E-1"}, + {"0.01", "1E-2"}, + {"0.123", "1.23E-1"}, + {"1.23", "1.23E0"}, + {"-0.1", "-1E-1"}, + {"-0.01", "-1E-2"}, + {"-0.010", "-1.0E-2"}, + {"-0.123", "-1.23E-1"}, + {"-1.23", "-1.23E0"}, + {"1E6", "1E6"}, + {"1e6", "1E6"}, + {"1.23E6", "1.23E6"}, + {"-1E6", "-1E6"}, + {"1E-6", "1E-6"}, + {"1.23E-6", "1.23E-6"}, + {"-1E-6", "-1E-6"}, + {"-1.0E-6", "-1.0E-6"}, + {"12345600", "1.2345600E7"}, + {"123456E2", "1.23456E7"}, + {"0", "0"}, + {"0E1", "0"}, + {"-0", "0"}, + {"-0.000", "0"}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.ScientificNotationString() != test.expected { + t.Errorf("expected %s, got %s", test.expected, d.ScientificNotationString()) + } + } +} From 9620fb7d0776651853952726217a81ba8f451882 Mon Sep 17 00:00:00 2001 From: Joseph Watson Date: Fri, 13 Feb 2026 08:18:44 -0700 Subject: [PATCH 04/52] Add Spanner support for Encode and Decode (#395) --- decimal.go | 36 +++++++++++ decimal_test.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/decimal.go b/decimal.go index 3cf7f68..ff5d330 100644 --- a/decimal.go +++ b/decimal.go @@ -1923,6 +1923,16 @@ func (d *Decimal) GobDecode(data []byte) error { return d.UnmarshalBinary(data) } +// DecodeSpanner decodes a Spanner value into a Decimal +func (d *Decimal) DecodeSpanner(val interface{}) error { + return d.Scan(val) +} + +// EncodeSpanner encodes a Decimal into a Spanner value +func (d Decimal) EncodeSpanner() (interface{}, error) { + return d.String(), nil +} + // StringScaled first scales the decimal then calls .String() on it. // // Deprecated: buggy and unintuitive. Use StringFixed instead. @@ -2164,6 +2174,32 @@ func (d NullDecimal) MarshalText() (text []byte, err error) { return d.Decimal.MarshalText() } +// DecodeSpanner decodes a Spanner value into a Decimal +func (d *NullDecimal) DecodeSpanner(value interface{}) error { + switch t := value.(type) { + case nil: + d.Valid = false + return nil + case *string: + if t == nil { + d.Valid = false + return nil + } + value = *t + } + d.Valid = true + + return d.Decimal.Scan(value) +} + +// EncodeSpanner encodes a Decimal into a Spanner value +func (d NullDecimal) EncodeSpanner() (interface{}, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.String(), nil +} + // Trig functions // Atan returns the arctangent, in radians, of x. diff --git a/decimal_test.go b/decimal_test.go index 164bb30..3602d45 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2505,6 +2505,77 @@ func TestDecimal_Value(t *testing.T) { } } +func decodeSpannerHelper(t *testing.T, dbval interface{}, expected Decimal) { + t.Helper() + + a := Decimal{} + if err := a.DecodeSpanner(dbval); err != nil { + // DecodeSpanner failed... no need to test result value + t.Errorf("a.DecodeSpanner(%v) failed with message: %s", dbval, err) + } else if !a.Equal(expected) { + // DecodeSpanner succeeded... test resulting values + t.Errorf("%s does not equal to %s", a, expected) + } +} + +type spannerDecoder interface { + DecodeSpanner(input interface{}) error +} + +func TestDecimal_DecodeSpanner(t *testing.T) { + // test the DecodeSpanner method that implements spanner.Decoder interface + if _, ok := interface{}(new(Decimal)).(spannerDecoder); !ok { + t.Error("Decimal does not implement spanner.Decoder") + } + + dbvalue := 54.33 + expected := NewFromFloat(dbvalue) + decodeSpannerHelper(t, dbvalue, expected) + + // also test uint64 + dbvalueUint64 := uint64(2) + expected = New(2, 0) + decodeSpannerHelper(t, dbvalueUint64, expected) + + // ensure we can handle the return of either []byte or string + valueStr := "535.666" + dbvalueStr := []byte(valueStr) + expected, err := NewFromString(valueStr) + if err != nil { + t.Fatal(err) + } + decodeSpannerHelper(t, dbvalueStr, expected) + decodeSpannerHelper(t, valueStr, expected) + + type foo struct{} + a := Decimal{} + err = a.DecodeSpanner(foo{}) + if err == nil { + t.Errorf("a.DecodeSpanner(Foo{}) should have thrown an error but did not") + } +} + +type spannerEncoder interface { + EncodeSpanner() (interface{}, error) +} + +func TestDecimal_EncodeSpanner(t *testing.T) { + // Make sure this does implement the spanner.Encoder interface + if _, ok := interface{}(Decimal{}).(spannerEncoder); !ok { + t.Error("Decimal does not implement spanner.Encoder") + } + + // check that normal case is handled appropriately + a := New(1234, -2) + expected := "12.34" + value, err := a.Value() + if err != nil { + t.Errorf("Decimal(12.34).Value() failed with message: %s", err) + } else if got := value.(string); got != expected { + t.Errorf("%s does not equal to %s", a, expected) + } +} + // old tests after this line func TestDecimal_Scale(t *testing.T) { @@ -3301,6 +3372,98 @@ func TestNullDecimal_Value(t *testing.T) { } } +func TestNullDecimal_DecodeSpanner(t *testing.T) { + // test the DecodeSpanner method that implements the + // spanner.Decoder interface + if _, ok := interface{}(new(NullDecimal)).(spannerDecoder); !ok { + t.Error("NullDecimal does not implement spanner.Decoder") + } + + // Make sure handles nil value + a := NullDecimal{} + var dbvaluePtr interface{} + err := a.DecodeSpanner(dbvaluePtr) + if err != nil { + // DecodeSpanner failed... no need to test result value + t.Errorf("a.DecodeSpanner(nil) failed with message: %s", err) + } else { + if a.Valid { + t.Errorf("%s is not null", a.Decimal) + } + } + + // Make sure handles nil *string + dbvaluePtr = (*string)(nil) + if err := a.DecodeSpanner(dbvaluePtr); err != nil { + // DecodeSpanner failed... no need to test result value + t.Errorf("a.DecodeSpanner((*string)(nil)) failed with message: %s", err) + } else { + if a.Valid { + t.Errorf("%s is not null", a.Decimal) + } + } + + valueStr := "535.666" + expected, err := NewFromString(valueStr) + if err != nil { + t.Fatal(err) + } + + // Handle string + err = a.DecodeSpanner(valueStr) + if err != nil { + // DecodeSpanner failed... no need to test result value + t.Errorf("a.DecodeSpanner('535.666') failed with message: %s", err) + } else { + // DecodeSpanner succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equals(expected) { + t.Errorf("%v does not equal %v", a, expected) + } + } + + // handle *string + err = a.DecodeSpanner(&valueStr) + if err != nil { + // DecodeSpanner failed... no need to test result value + t.Errorf("a.DecodeSpanner('535.666') failed with message: %s", err) + } else { + // DecodeSpanner succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equals(expected) { + t.Errorf("%v does not equal %v", a, expected) + } + } +} + +func TestNullDecimal_EncodeSpanner(t *testing.T) { + // Make sure this does implement the spanner.Encoder interface + var nullDecimal NullDecimal + if _, ok := interface{}(nullDecimal).(spannerEncoder); !ok { + t.Error("NullDecimal does not implement spanner.Encoder") + } + + // check that null is handled appropriately + value, err := nullDecimal.EncodeSpanner() + if err != nil { + t.Errorf("NullDecimal{}.Valid() failed with message: %s", err) + } else if value != nil { + t.Errorf("%v is not nil", value) + } + + // check that normal case is handled appropriately + a := NullDecimal{Decimal: New(1234, -2), Valid: true} + expected := "12.34" + value, err = a.EncodeSpanner() + if err != nil { + t.Errorf("NullDecimal(12.34).EncodeSpanner() failed with message: %s", err) + } else if value.(string) != expected { + t.Errorf("%v does not equal %v", a, expected) + } +} + func TestBinary(t *testing.T) { for _, y := range testTable { x := y.float From 6540310a7d8c2772d36be413a9f50f3154816850 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 8 Mar 2026 23:42:36 +0100 Subject: [PATCH 05/52] Add support for omitzero struct tag (#415) --- .github/workflows/ci.yml | 2 +- .gitignore | 3 + decimal.go | 160 ++++++++++++++++----------------------- decimal_go124_test.go | 58 ++++++++++++++ decimal_test.go | 48 ++++++------ 5 files changed, 151 insertions(+), 120 deletions(-) create mode 100644 decimal_go124_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11a10b0..dcce39b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.10.x', '1.19', '1.20', '1.21', '1.22', '1.x' ] + go: [ '1.10.x', '1.22', '1.23', '1.24', '1.25', '1.x' ] name: Go ${{ matrix.go }} steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index ff36b98..6838f34 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # VS code *.code-workspace +.vscode/ + +mise.toml \ No newline at end of file diff --git a/decimal.go b/decimal.go index ff5d330..2ae2726 100644 --- a/decimal.go +++ b/decimal.go @@ -84,7 +84,7 @@ var ExpMaxIterations = 1000 // Zero constant, to make computations faster. // Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. -var Zero = New(0, 1) +var Zero = Decimal{} var zeroInt = big.NewInt(0) var oneInt = big.NewInt(1) @@ -109,6 +109,13 @@ type Decimal struct { exp int32 } +func (d Decimal) getValue() *big.Int { + if d.value == nil { + return zeroInt + } + return d.value +} + // New returns a new fixed-point decimal, value * 10 ^ exp. func New(value int64, exp int32) Decimal { return Decimal{ @@ -476,9 +483,8 @@ func NewFromFloatWithExponent(value float64, exp int32) Decimal { // Copy returns a copy of decimal with the same value and exponent, but a different pointer to value. func (d Decimal) Copy() Decimal { - d.ensureInitialized() return Decimal{ - value: new(big.Int).Set(d.value), + value: new(big.Int).Set(d.getValue()), exp: d.exp, } } @@ -503,18 +509,16 @@ func (d Decimal) Copy() Decimal { // 1.2 // 1.2000 func (d Decimal) rescale(exp int32) Decimal { - d.ensureInitialized() - if d.exp == exp { return Decimal{ - new(big.Int).Set(d.value), + new(big.Int).Set(d.getValue()), d.exp, } } // NOTE(vadim): must convert exps to float64 before - to prevent overflow diff := math.Abs(float64(exp) - float64(d.exp)) - value := new(big.Int).Set(d.value) + value := new(big.Int).Set(d.getValue()) expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil) if exp > d.exp { @@ -534,8 +538,7 @@ func (d Decimal) Abs() Decimal { if !d.IsNegative() { return d } - d.ensureInitialized() - d2Value := new(big.Int).Abs(d.value) + d2Value := new(big.Int).Abs(d.getValue()) return Decimal{ value: d2Value, exp: d.exp, @@ -546,7 +549,7 @@ func (d Decimal) Abs() Decimal { func (d Decimal) Add(d2 Decimal) Decimal { rd, rd2 := RescalePair(d, d2) - d3Value := new(big.Int).Add(rd.value, rd2.value) + d3Value := new(big.Int).Add(rd.getValue(), rd2.getValue()) return Decimal{ value: d3Value, exp: rd.exp, @@ -557,7 +560,7 @@ func (d Decimal) Add(d2 Decimal) Decimal { func (d Decimal) Sub(d2 Decimal) Decimal { rd, rd2 := RescalePair(d, d2) - d3Value := new(big.Int).Sub(rd.value, rd2.value) + d3Value := new(big.Int).Sub(rd.getValue(), rd2.getValue()) return Decimal{ value: d3Value, exp: rd.exp, @@ -566,8 +569,7 @@ func (d Decimal) Sub(d2 Decimal) Decimal { // Neg returns -d. func (d Decimal) Neg() Decimal { - d.ensureInitialized() - val := new(big.Int).Neg(d.value) + val := new(big.Int).Neg(d.getValue()) return Decimal{ value: val, exp: d.exp, @@ -576,9 +578,6 @@ func (d Decimal) Neg() Decimal { // Mul returns d * d2. func (d Decimal) Mul(d2 Decimal) Decimal { - d.ensureInitialized() - d2.ensureInitialized() - expInt64 := int64(d.exp) + int64(d2.exp) if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { // NOTE(vadim): better to panic than give incorrect results, as @@ -586,7 +585,7 @@ func (d Decimal) Mul(d2 Decimal) Decimal { panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) } - d3Value := new(big.Int).Mul(d.value, d2.value) + d3Value := new(big.Int).Mul(d.getValue(), d2.getValue()) return Decimal{ value: d3Value, exp: int32(expInt64), @@ -598,9 +597,8 @@ func (d Decimal) Mul(d2 Decimal) Decimal { // In simpler terms, the given value for shift is added to the exponent // of the decimal. func (d Decimal) Shift(shift int32) Decimal { - d.ensureInitialized() return Decimal{ - value: new(big.Int).Set(d.value), + value: new(big.Int).Set(d.getValue()), exp: d.exp + shift, } } @@ -620,9 +618,7 @@ func (d Decimal) Div(d2 Decimal) Decimal { // // Note that precision<0 is allowed as input. func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { - d.ensureInitialized() - d2.ensureInitialized() - if d2.value.Sign() == 0 { + if d2.getValue().Sign() == 0 { panic("decimal division by 0") } scale := -precision @@ -635,18 +631,18 @@ func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { // d = a 10^ea // d2 = b 10^eb if e < 0 { - aa = *d.value + aa = *d.getValue() expo.SetInt64(-e) bb.Exp(tenInt, &expo, nil) - bb.Mul(d2.value, &bb) + bb.Mul(d2.getValue(), &bb) scalerest = d.exp // now aa = a // bb = b 10^(scale + eb - ea) } else { expo.SetInt64(e) aa.Exp(tenInt, &expo, nil) - aa.Mul(d.value, &aa) - bb = *d2.value + aa.Mul(d.getValue(), &aa) + bb = *d2.getValue() scalerest = scale + d2.exp // now aa = a ^ (ea - eb - scale) // bb = b @@ -671,7 +667,7 @@ func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { // the actual rounding decision is based on comparing r*10^precision and d2/2 // instead compare 2 r 10 ^precision and d2 var rv2 big.Int - rv2.Abs(r.value) + rv2.Abs(r.getValue()) rv2.Lsh(&rv2, 1) // now rv2 = abs(r.value) * 2 r2 := Decimal{value: &rv2, exp: r.exp + precision} @@ -682,7 +678,7 @@ func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { return q } - if d.value.Sign()*d2.value.Sign() < 0 { + if d.getValue().Sign()*d2.getValue().Sign() < 0 { return q.Sub(New(1, -precision)) } @@ -743,10 +739,10 @@ func (d Decimal) Pow(d2 Decimal) Decimal { return Decimal{} } - intPartPow, _ := d.PowBigInt(expIntPart.value) + intPartPow, _ := d.PowBigInt(expIntPart.getValue()) // if exponent is an integer we don't need to calculate d1**frac(d2) - if expFracPart.value.Sign() == 0 { + if expFracPart.getValue().Sign() == 0 { return intPartPow } @@ -836,10 +832,10 @@ func (d Decimal) PowWithPrecision(d2 Decimal, precision int32) (Decimal, error) return Decimal{}, fmt.Errorf("cannot represent imaginary value of x ** y, where x < 0 and y is non-integer decimal") } - intPartPow, _ := d.powBigIntWithPrecision(expIntPart.value, precision) + intPartPow, _ := d.powBigIntWithPrecision(expIntPart.getValue(), precision) // if exponent is an integer we don't need to calculate d1**frac(d2) - if expFracPart.value.Sign() == 0 { + if expFracPart.getValue().Sign() == 0 { return intPartPow, nil } @@ -1010,9 +1006,9 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { t = 0 } - k := New(1, t) // reduction factor - r := Decimal{new(big.Int).Set(d.value), d.exp - t} // reduced argument - p := int32(currentPrecision) + t + 2 // precision for calculating the sum + k := New(1, t) // reduction factor + r := Decimal{new(big.Int).Set(d.getValue()), d.exp - t} // reduced argument + p := int32(currentPrecision) + t + 2 // precision for calculating the sum // Determine n, the number of therms for calculating sum // use first Newton step (1.435p - 1.182) / log10(p/abs(r)) @@ -1257,12 +1253,9 @@ func (d Decimal) Ln(precision int32) (Decimal, error) { // NumDigits returns the number of digits of the decimal coefficient (d.Value) func (d Decimal) NumDigits() int { - if d.value == nil { - return 1 - } - - if d.value.IsInt64() { - i64 := d.value.Int64() + v := d.getValue() + if v.IsInt64() { + i64 := v.Int64() // restrict fast path to integers with exact conversion to float64 if i64 <= (1<<53) && i64 >= -(1<<53) { if i64 == 0 { @@ -1272,13 +1265,13 @@ func (d Decimal) NumDigits() int { } } - estimatedNumDigits := int(float64(d.value.BitLen()) / math.Log2(10)) + estimatedNumDigits := int(float64(v.BitLen()) / math.Log2(10)) // estimatedNumDigits (lg10) may be off by 1, need to verify digitsBigInt := big.NewInt(int64(estimatedNumDigits)) errorCorrectionUnit := digitsBigInt.Exp(tenInt, digitsBigInt, nil) - if d.value.CmpAbs(errorCorrectionUnit) >= 0 { + if v.CmpAbs(errorCorrectionUnit) >= 0 { return estimatedNumDigits + 1 } @@ -1294,7 +1287,7 @@ func (d Decimal) IsInteger() bool { // When the exponent is negative we have to check every number after the decimal place // If all of them are zeroes, we are sure that given decimal can be represented as an integer var r big.Int - q := new(big.Int).Set(d.value) + q := new(big.Int).Set(d.getValue()) for z := abs(d.exp); z > 0; z-- { q.QuoRem(q, tenInt, &r) if r.Cmp(zeroInt) != 0 { @@ -1318,16 +1311,13 @@ func abs(n int32) int32 { // 0 if d == d2 // +1 if d > d2 func (d Decimal) Cmp(d2 Decimal) int { - d.ensureInitialized() - d2.ensureInitialized() - if d.exp == d2.exp { - return d.value.Cmp(d2.value) + return d.getValue().Cmp(d2.getValue()) } rd, rd2 := RescalePair(d, d2) - return rd.value.Cmp(rd2.value) + return rd.getValue().Cmp(rd2.getValue()) } // Compare compares the numbers represented by d and d2 and returns: @@ -1377,10 +1367,7 @@ func (d Decimal) LessThanOrEqual(d2 Decimal) bool { // 0 if d == 0 // +1 if d > 0 func (d Decimal) Sign() int { - if d.value == nil { - return 0 - } - return d.value.Sign() + return d.getValue().Sign() } // IsPositive return @@ -1417,28 +1404,26 @@ func (d Decimal) Exponent() int32 { // Coefficient returns the coefficient of the decimal. It is scaled by 10^Exponent() func (d Decimal) Coefficient() *big.Int { - d.ensureInitialized() // we copy the coefficient so that mutating the result does not mutate the Decimal. - return new(big.Int).Set(d.value) + return new(big.Int).Set(d.getValue()) } // CoefficientInt64 returns the coefficient of the decimal as int64. It is scaled by 10^Exponent() // If coefficient cannot be represented in an int64, the result will be undefined. func (d Decimal) CoefficientInt64() int64 { - d.ensureInitialized() - return d.value.Int64() + return d.getValue().Int64() } // IntPart returns the integer component of the decimal. func (d Decimal) IntPart() int64 { scaledD := d.rescale(0) - return scaledD.value.Int64() + return scaledD.getValue().Int64() } // BigInt returns integer component of the decimal as a BigInt. func (d Decimal) BigInt() *big.Int { scaledD := d.rescale(0) - return scaledD.value + return scaledD.getValue() } // BigFloat returns decimal as BigFloat. @@ -1451,15 +1436,14 @@ func (d Decimal) BigFloat() *big.Float { // Rat returns a rational number representation of the decimal. func (d Decimal) Rat() *big.Rat { - d.ensureInitialized() if d.exp <= 0 { // NOTE(vadim): must negate after casting to prevent int32 overflow denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil) - return new(big.Rat).SetFrac(d.value, denom) + return new(big.Rat).SetFrac(d.getValue(), denom) } mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil) - num := new(big.Int).Mul(d.value, mul) + num := new(big.Int).Mul(d.getValue(), mul) return new(big.Rat).SetFrac(num, oneInt) } @@ -1588,8 +1572,8 @@ func (d Decimal) RoundCeil(places int32) Decimal { return d } - if d.value.Sign() > 0 { - rescaled.value.Add(rescaled.value, oneInt) + if d.getValue().Sign() > 0 { + rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) } return rescaled @@ -1613,8 +1597,8 @@ func (d Decimal) RoundFloor(places int32) Decimal { return d } - if d.value.Sign() < 0 { - rescaled.value.Sub(rescaled.value, oneInt) + if d.getValue().Sign() < 0 { + rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) } return rescaled @@ -1638,10 +1622,10 @@ func (d Decimal) RoundUp(places int32) Decimal { return d } - if d.value.Sign() > 0 { - rescaled.value.Add(rescaled.value, oneInt) - } else if d.value.Sign() < 0 { - rescaled.value.Sub(rescaled.value, oneInt) + if d.getValue().Sign() > 0 { + rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) + } else if d.getValue().Sign() < 0 { + rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) } return rescaled @@ -1687,11 +1671,11 @@ func (d Decimal) RoundBank(places int32) Decimal { remainder := d.Sub(round).Abs() half := New(5, -places-1) - if remainder.Cmp(half) == 0 && round.value.Bit(0) != 0 { - if round.value.Sign() < 0 { - round.value.Add(round.value, oneInt) + if remainder.Cmp(half) == 0 && round.getValue().Bit(0) != 0 { + if round.getValue().Sign() < 0 { + round.value = new(big.Int).Add(round.getValue(), oneInt) } else { - round.value.Sub(round.value, oneInt) + round.value = new(big.Int).Sub(round.getValue(), oneInt) } } @@ -1736,8 +1720,6 @@ func (d Decimal) RoundCash(interval uint8) Decimal { // Floor returns the nearest integer value less than or equal to d. func (d Decimal) Floor() Decimal { - d.ensureInitialized() - if d.exp >= 0 { return d } @@ -1747,14 +1729,12 @@ func (d Decimal) Floor() Decimal { // NOTE(vadim): must negate after casting to prevent int32 overflow exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) - z := new(big.Int).Div(d.value, exp) + z := new(big.Int).Div(d.getValue(), exp) return Decimal{value: z, exp: 0} } // Ceil returns the nearest integer value greater than or equal to d. func (d Decimal) Ceil() Decimal { - d.ensureInitialized() - if d.exp >= 0 { return d } @@ -1764,7 +1744,7 @@ func (d Decimal) Ceil() Decimal { // NOTE(vadim): must negate after casting to prevent int32 overflow exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) - z, m := new(big.Int).DivMod(d.value, exp, new(big.Int)) + z, m := new(big.Int).DivMod(d.getValue(), exp, new(big.Int)) if m.Cmp(zeroInt) != 0 { z.Add(z, oneInt) } @@ -1779,7 +1759,6 @@ func (d Decimal) Ceil() Decimal { // // decimal.NewFromString("123.456").Truncate(2).String() // "123.45" func (d Decimal) Truncate(precision int32) Decimal { - d.ensureInitialized() if precision >= 0 && -precision > d.exp { return d.rescale(-precision) } @@ -1836,7 +1815,7 @@ func (d *Decimal) UnmarshalBinary(data []byte) error { func (d Decimal) MarshalBinary() (data []byte, err error) { // exp is written first, but encode value first to know output size var valueData []byte - if valueData, err = d.value.GobEncode(); err != nil { + if valueData, err = d.getValue().GobEncode(); err != nil { return nil, err } @@ -1942,7 +1921,7 @@ func (d Decimal) StringScaled(exp int32) string { func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string { if d.exp == 0 { - return d.rescale(0).value.String() + return d.rescale(0).getValue().String() } if d.exp >= 0 { if avoidScientificNotation { @@ -1952,7 +1931,7 @@ func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string } } - abs := new(big.Int).Abs(d.value) + abs := new(big.Int).Abs(d.getValue()) str := abs.String() var intPart, fractionalPart string @@ -1985,7 +1964,7 @@ func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string number += "." + fractionalPart } - if d.value.Sign() < 0 { + if d.getValue().Sign() < 0 { return "-" + number } @@ -2000,7 +1979,7 @@ func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string // A zero, which has no significant digits, is simply serialized to "0". func (d Decimal) ScientificNotationString() string { exp := int(d.exp) - intStr := new(big.Int).Abs(d.value).String() + intStr := new(big.Int).Abs(d.getValue()).String() if intStr == "0" { return intStr } @@ -2017,12 +1996,6 @@ func (d Decimal) ScientificNotationString() string { return number } -func (d *Decimal) ensureInitialized() { - if d.value == nil { - d.value = new(big.Int) - } -} - // Min returns the smallest Decimal that was passed in the arguments. // // To call this function with an array, you must do: @@ -2076,9 +2049,6 @@ func Avg(first Decimal, rest ...Decimal) Decimal { // RescalePair rescales two decimals to common exponential value (minimal exp of both decimals) func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { - d1.ensureInitialized() - d2.ensureInitialized() - if d1.exp < d2.exp { return d1, d2.rescale(d1.exp) } else if d1.exp > d2.exp { diff --git a/decimal_go124_test.go b/decimal_go124_test.go new file mode 100644 index 0000000..13ee324 --- /dev/null +++ b/decimal_go124_test.go @@ -0,0 +1,58 @@ +//go:build go1.24 +// +build go1.24 + +package decimal + +import ( + "encoding/json" + "testing" +) + +// `omitzero` is supported by encoding/json starting in Go 1.24. +// Keep this test in a go1.24-gated file so older CI jobs skip it. +func TestJSONOmitZeroTag(t *testing.T) { + type Nested struct { + Amount Decimal `json:"amount,omitzero"` + } + + type Parent struct { + Nested Nested `json:"nested,omitzero"` + } + + tests := []struct { + name string + parent Parent + expected string + }{ + { + name: "Decimal{} empty value", + parent: Parent{ + Nested: Nested{ + Amount: Decimal{}, + }, + }, + expected: "{}", + }, + { + name: "Zero constant", + parent: Parent{ + Nested: Nested{ + Amount: Zero, + }, + }, + expected: "{}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := json.Marshal(tt.parent) + if err != nil { + t.Fatal(err) + } + if string(b) != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, string(b)) + } + }) + } +} diff --git a/decimal_test.go b/decimal_test.go index 3602d45..584132d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -576,7 +576,7 @@ func TestNewFromBigIntWithExponent(t *testing.T) { // add negatives for p, s := range tests { - if p.val.Cmp(Zero.value) > 0 { + if p.val.Cmp(zeroInt) > 0 { tests[Inp{p.val.Neg(p.val), p.exp}] = "-" + s } } @@ -1969,7 +1969,7 @@ func TestDecimal_QuoRem(t *testing.T) { t.Errorf("remainder too large: d=%v, d2= %v, prec=%d, q=%v, r=%v", d, d2, prec, q, r) } - if r.value.Sign()*d.value.Sign() < 0 { + if r.Sign()*d.Sign() < 0 { t.Errorf("signum of divisor and rest do not match: d=%v, d2= %v, prec=%d, q=%v, r=%v", d, d2, prec, q, r) } @@ -2034,7 +2034,7 @@ func TestDecimal_QuoRem2(t *testing.T) { d, d2, prec, q, r) } // rule 4: r and d have the same sign - if r.value.Sign()*d.value.Sign() < 0 { + if r.Sign()*d.Sign() < 0 { t.Errorf("signum of divisor and rest do not match, "+ "d=%v, d2=%v, prec=%d, q=%v, r=%v", d, d2, prec, q, r) @@ -2061,7 +2061,7 @@ func (d Decimal) DivOld(d2 Decimal, prec int) Decimal { } func sign(d Decimal) int { - return d.value.Sign() + return d.Sign() } // rules for rounded divide, rounded to integer @@ -3805,26 +3805,6 @@ func TestNewNullDecimal(t *testing.T) { } } -func ExampleNewFromFloat32() { - fmt.Println(NewFromFloat32(123.123123123123).String()) - fmt.Println(NewFromFloat32(.123123123123123).String()) - fmt.Println(NewFromFloat32(-1e13).String()) - // OUTPUT: - //123.12312 - //0.123123124 - //-10000000000000 -} - -func ExampleNewFromFloat() { - fmt.Println(NewFromFloat(123.123123123123).String()) - fmt.Println(NewFromFloat(.123123123123123).String()) - fmt.Println(NewFromFloat(-1e13).String()) - // OUTPUT: - //123.123123123123 - //0.123123123123123 - //-10000000000000 -} - func TestDecimal_String(t *testing.T) { type testData struct { input string @@ -3962,3 +3942,23 @@ func TestDecimal_ScientificNotation(t *testing.T) { } } } + +func ExampleNewFromFloat32() { + fmt.Println(NewFromFloat32(123.123123123123).String()) + fmt.Println(NewFromFloat32(.123123123123123).String()) + fmt.Println(NewFromFloat32(-1e13).String()) + // OUTPUT: + //123.12312 + //0.123123124 + //-10000000000000 +} + +func ExampleNewFromFloat() { + fmt.Println(NewFromFloat(123.123123123123).String()) + fmt.Println(NewFromFloat(.123123123123123).String()) + fmt.Println(NewFromFloat(-1e13).String()) + // OUTPUT: + //123.123123123123 + //0.123123123123123 + //-10000000000000 +} From 400829b4bf08b784ab60ce5598c3fafb3ce36e35 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 15 Mar 2026 23:07:00 +0100 Subject: [PATCH 06/52] chore: rename AvoidScientificNotation to UseScientificNotation (#416) --- decimal.go | 30 +++++++++++++++--------------- decimal_test.go | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/decimal.go b/decimal.go index 2ae2726..880d569 100644 --- a/decimal.go +++ b/decimal.go @@ -71,12 +71,12 @@ var MarshalJSONWithoutQuotes = false // Setting this value to false can be useful for APIs where exact decimal string representation matters. var TrimTrailingZeros = true -// AvoidScientificNotation specifies whether scientific notation should be used when decimal is turned +// UseScientificNotation specifies whether scientific notation should be used when a decimal is turned // into a string that has a "negative" precision. // // For example, 1200 rounded to the nearest 100 cannot accurately be shown as "1200" because the last two -// digits are unknown. With this set to false, that number would be expressed as "1.2E3" instead. -var AvoidScientificNotation = true +// digits are unknown. With this set to true, that number would be expressed as "1.2E3" instead. +var UseScientificNotation = false // ExpMaxIterations specifies the maximum number of iterations needed to calculate // precise natural exponent value using ExpHullAbrham method. @@ -1473,7 +1473,7 @@ func (d Decimal) InexactFloat64() float64 { // // -12.345 func (d Decimal) String() string { - return d.string(TrimTrailingZeros, AvoidScientificNotation) + return d.string(TrimTrailingZeros, UseScientificNotation) } // StringFixed returns a rounded fixed-point string with places digits after @@ -1489,10 +1489,10 @@ func (d Decimal) String() string { // NewFromFloat(5.45).StringFixed(3) // output: "5.450" // NewFromFloat(545).StringFixed(-1) // output: "540" // -// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. +// Regardless of the UseScientificNotation option, the returned string will never be in scientific notation. func (d Decimal) StringFixed(places int32) string { rounded := d.Round(places) - return rounded.string(false, true) + return rounded.string(false, false) } // StringFixedBank returns a banker rounded fixed-point string with places digits @@ -1508,19 +1508,19 @@ func (d Decimal) StringFixed(places int32) string { // NewFromFloat(5.45).StringFixedBank(3) // output: "5.450" // NewFromFloat(545).StringFixedBank(-1) // output: "540" // -// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. +// Regardless of the UseScientificNotation option, the returned string will never be in scientific notation. func (d Decimal) StringFixedBank(places int32) string { rounded := d.RoundBank(places) - return rounded.string(false, true) + return rounded.string(false, false) } // StringFixedCash returns a Swedish/Cash rounded fixed-point string. For // more details see the documentation at function RoundCash. // -// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation. +// Regardless of the UseScientificNotation option, the returned string will never be in scientific notation. func (d Decimal) StringFixedCash(interval uint8) string { rounded := d.RoundCash(interval) - return rounded.string(false, true) + return rounded.string(false, false) } // Round rounds the decimal to places decimal places. @@ -1529,7 +1529,7 @@ func (d Decimal) StringFixedCash(interval uint8) string { // Example: // // NewFromFloat(5.45).Round(1).String() // output: "5.5" -// NewFromFloat(545).Round(-1).String() // output: "550" (with AvoidScientificNotation, "5.5E2" otherwise) +// NewFromFloat(545).Round(-1).String() // output: "550" (with UseScientificNotation false, "5.5E2" if true) func (d Decimal) Round(places int32) Decimal { if d.exp == -places { return d @@ -1919,15 +1919,15 @@ func (d Decimal) StringScaled(exp int32) string { return d.rescale(exp).String() } -func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string { +func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { if d.exp == 0 { return d.rescale(0).getValue().String() } if d.exp >= 0 { - if avoidScientificNotation { - return d.rescale(0).value.String() - } else { + if useScientificNotation { return d.ScientificNotationString() + } else { + return d.rescale(0).value.String() } } diff --git a/decimal_test.go b/decimal_test.go index 584132d..25d95bc 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3868,9 +3868,9 @@ func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) { } defer func() { - AvoidScientificNotation = true + UseScientificNotation = false }() - AvoidScientificNotation = false + UseScientificNotation = true tests := []testData{ {"1.0E3", "1.0E3"}, // 1000 to the nearest hundred From 3090cc487febd78d6016c2859c66308328a40e99 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Mon, 29 Jun 2026 05:58:29 -0400 Subject: [PATCH 07/52] fix: NumDigits underreports for some exact powers of ten (#425) math.Log10(1eN) rounds down to N-1 for several N (15, 24, 28, ...) because the exact decimal can't survive a float64 round-trip. The fast path returned int(math.Log10(...)) + 1, so a coefficient like 1000000000000000 (16 digits) came back as 15. Count digits via int64 division for the IsInt64 branch. The slow big.Int path is unchanged. Regression tests cover 1e15 and -1e15 (both via plain literal and scientific-notation input) and 1<<53 (the old fast-path boundary). Fixes #420 Signed-off-by: Charlie Tonneslan --- decimal.go | 25 +++++++++++++++++++------ decimal_test.go | 8 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/decimal.go b/decimal.go index 880d569..608a12e 100644 --- a/decimal.go +++ b/decimal.go @@ -1256,13 +1256,26 @@ func (d Decimal) NumDigits() int { v := d.getValue() if v.IsInt64() { i64 := v.Int64() - // restrict fast path to integers with exact conversion to float64 - if i64 <= (1<<53) && i64 >= -(1<<53) { - if i64 == 0 { - return 1 - } - return int(math.Log10(math.Abs(float64(i64)))) + 1 + if i64 == 0 { + return 1 } + // Count digits via the underlying int64 instead of math.Log10, + // which rounds an exact 1eN like 1e15 down to 14.999... and would + // then under-report by one (see #420). Negation is safe because + // math.MinInt64 has IsInt64() true but its absolute value also + // needs counting; treat it explicitly. + if i64 == math.MinInt64 { + return 19 + } + if i64 < 0 { + i64 = -i64 + } + n := 0 + for i64 > 0 { + n++ + i64 /= 10 + } + return n } estimatedNumDigits := int(float64(v.BitLen()) / math.Log2(10)) diff --git a/decimal_test.go b/decimal_test.go index 25d95bc..161facf 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3157,6 +3157,14 @@ func TestDecimal_NumDigits(t *testing.T) { {"-5.2663117716", 11}, {"-26.1", 3}, {"", 1}, + // Regression for #420: float64 math.Log10 rounds 1eN down to + // N-1 for some N (e.g. log10(1e15) == 14.999...), so the old + // fast path under-reported by one for exact-power coefficients. + {"1000000000000000", 16}, + {"-1000000000000000", 16}, + {"1000000000000000E20", 16}, + {"9007199254740992", 16}, // 1<<53, the old fast-path boundary + {"-9007199254740992", 16}, } { d, _ := NewFromString(testCase.Dec) From ca4740823783f3bc026235a5ed3515aca626da92 Mon Sep 17 00:00:00 2001 From: natan streppel Date: Wed, 19 Aug 2026 18:07:36 -0300 Subject: [PATCH 08/52] fix: ExpTaylor race condition (#414) --- decimal.go | 28 +++++++++----- decimal_test.go | 97 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/decimal.go b/decimal.go index 608a12e..ee554d4 100644 --- a/decimal.go +++ b/decimal.go @@ -25,6 +25,7 @@ import ( "regexp" "strconv" "strings" + "sync" ) // DivisionPrecision is the number of decimal places in the result when it @@ -95,6 +96,7 @@ var tenInt = big.NewInt(10) var twentyInt = big.NewInt(20) var factorials = []Decimal{New(1, 0)} +var factorialsMutex sync.RWMutex // Decimal represents a fixed-point decimal. It is immutable. // number = value * 10 ^ exp @@ -1103,15 +1105,21 @@ func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { i++ // Calculate next factorial number or retrieve cached value + factorialsMutex.RLock() if len(factorials) >= int(i) && !factorials[i-1].IsZero() { factorial = factorials[i-1] + factorialsMutex.RUnlock() } else { - // To avoid any race conditions, firstly the zero value is appended to a slice to create - // a spot for newly calculated factorial. After that, the zero value is replaced by calculated - // factorial using the index notation. - factorial = factorials[i-2].Mul(New(i, 0)) - factorials = append(factorials, Zero) - factorials[i-1] = factorial + prevFactorial := factorials[i-2] + factorialsMutex.RUnlock() + factorial = prevFactorial.Mul(New(i, 0)) + factorialsMutex.Lock() + // Check again in case another goroutine already added it. + if len(factorials) < int(i) || factorials[i-1].IsZero() { + factorials = append(factorials, Zero) + factorials[i-1] = factorial + } + factorialsMutex.Unlock() } } @@ -2351,10 +2359,10 @@ var _tanP = [...]Decimal{ } var _tanQ = [...]Decimal{ NewFromFloat(1.00000000000000000000e+0), - NewFromFloat(1.36812963470692954678e+4), //0x40cab8a5eeb36572 - NewFromFloat(-1.32089234440210967447e+6), //0xc13427bc582abc96 - NewFromFloat(2.50083801823357915839e+7), //0x4177d98fc2ead8ef - NewFromFloat(-5.38695755929454629881e+7), //0xc189afe03cbe5a31 + NewFromFloat(1.36812963470692954678e+4), // 0x40cab8a5eeb36572 + NewFromFloat(-1.32089234440210967447e+6), // 0xc13427bc582abc96 + NewFromFloat(2.50083801823357915839e+7), // 0x4177d98fc2ead8ef + NewFromFloat(-5.38695755929454629881e+7), // 0xc189afe03cbe5a31 } // Tan returns the tangent of the radian argument x. diff --git a/decimal_test.go b/decimal_test.go index 161facf..48c1e21 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -12,6 +12,7 @@ import ( "regexp" "strconv" "strings" + "sync" "testing" "testing/quick" "time" @@ -648,7 +649,7 @@ func TestCopy(t *testing.T) { t.Error("expecting copy and origin to be equals, but they are not") } - //change value + // change value cpy = cpy.Add(New(1, 0)) if cpy.Cmp(origin) == 0 { @@ -3075,6 +3076,88 @@ func TestDecimal_ExpTaylor(t *testing.T) { } } +// TestDecimal_ExpTaylor_Concurrency tests the concurrency safety of the +// ExpTaylor method, particularly focusing on the shared state caching mechanism +// for factorial calculations. +func TestDecimal_ExpTaylor_Concurrency(t *testing.T) { + resetFactorials := func() { + factorialsMutex.Lock() + factorials = []Decimal{New(1, 0)} + factorialsMutex.Unlock() + } + t.Run("concurrent calculation works as expected", func(t *testing.T) { + resetFactorials() + + // Run multiple goroutines calculating exp concurrently + // This should trigger concurrent factorial calculations. + var wg sync.WaitGroup + numGoroutines := 50 + results := make([]Decimal, numGoroutines) + errors := make([]error, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + d := NewFromFloat(5.0) + result, err := d.ExpTaylor(10) + results[index] = result + errors[index] = err + }(i) + } + + wg.Wait() + + // Check all goroutines succeeded. + for i, err := range errors { + if err != nil { + t.Errorf("goroutine %d: ExpTaylor failed: %v", i, err) + } + } + + // All results should be identical. + expected := results[0] + for i, result := range results[1:] { + if !result.Equal(expected) { + t.Errorf("goroutine %d: result mismatch: expected %s, got %s", i+1, expected.String(), result.String()) + } + } + + // Verify factorials slice doesn't have duplicate Zero values + // (which would indicate the double-check locking failed). + factorialsMutex.RLock() + for i, f := range factorials { + if i > 0 && f.IsZero() { + t.Errorf("factorial at index %d is Zero, double-check locking may have failed", i) + } + } + factorialsMutex.RUnlock() + }) + + t.Run("race detection", func(t *testing.T) { + // NOTE: Factorials from previous subtests are intentionally reused here. + // This tests that concurrent access works correctly with a pre-populated cache, + // which is the real-world scenario. Run with `go test -race`. + + var wg sync.WaitGroup + numGoroutines := 100 + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(val float64) { + defer wg.Done() + d := NewFromFloat(val) + _, err := d.ExpTaylor(10) + if err != nil { + t.Errorf("ExpTaylor failed for value %f: %v", val, err) + } + }(float64(i%10) + 0.5) + } + + wg.Wait() + }) +} + func TestDecimal_Ln(t *testing.T) { for _, testCase := range []struct { Dec string @@ -3956,9 +4039,9 @@ func ExampleNewFromFloat32() { fmt.Println(NewFromFloat32(.123123123123123).String()) fmt.Println(NewFromFloat32(-1e13).String()) // OUTPUT: - //123.12312 - //0.123123124 - //-10000000000000 + // 123.12312 + // 0.123123124 + // -10000000000000 } func ExampleNewFromFloat() { @@ -3966,7 +4049,7 @@ func ExampleNewFromFloat() { fmt.Println(NewFromFloat(.123123123123123).String()) fmt.Println(NewFromFloat(-1e13).String()) // OUTPUT: - //123.123123123123 - //0.123123123123123 - //-10000000000000 + // 123.123123123123 + // 0.123123123123123 + // -10000000000000 } From e0621cba7b69c68bc2d1c6d5982d694cf4048d65 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 00:39:46 +0200 Subject: [PATCH 09/52] fix: correctly round Pow results and speed up Pow (#439) --- decimal.go | 686 +++++++++++++++++++++++++++++++----------- decimal_bench_test.go | 9 + decimal_test.go | 152 +++++++++- 3 files changed, 658 insertions(+), 189 deletions(-) diff --git a/decimal.go b/decimal.go index ee554d4..7b8c5cc 100644 --- a/decimal.go +++ b/decimal.go @@ -22,6 +22,7 @@ import ( "fmt" "math" "math/big" + "math/bits" "regexp" "strconv" "strings" @@ -44,9 +45,10 @@ import ( // d4.String() // output: "0.667" var DivisionPrecision = 16 -// PowPrecisionNegativeExponent specifies the maximum precision of the result (digits after decimal point) -// when calculating decimal power. Only used for cases where the exponent is a negative number. -// This constant applies to Pow, PowInt32 and PowBigInt methods, PowWithPrecision method is not constrained by it. +// PowPrecisionNegativeExponent specifies the number of digits after the decimal point in the results of +// Pow, PowInt32 and PowBigInt for negative or non-integer exponents. A result that would round to 0 keeps +// PowPrecisionNegativeExponent significant digits instead (at least one). +// PowWithPrecision is not affected, it takes the precision as an argument. // // Example: // @@ -694,13 +696,16 @@ func (d Decimal) Mod(d2 Decimal) Decimal { } // Pow returns d to the power of d2. -// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is +// rounded half away from zero to PowPrecisionNegativeExponent places after the decimal point. A result +// that would round to 0 keeps PowPrecisionNegativeExponent significant digits instead (at least one). // -// Pow returns 0 (zero-value of Decimal) instead of error for power operation edge cases, to handle those edge cases use PowWithPrecision +// Pow returns 0 (zero-value of Decimal) instead of error for power operation edge cases, to handle those edge cases use PowWithPrecision. // Edge cases not handled by Pow: // - 0 ** 0 => undefined value // - 0 ** y, where y < 0 => infinity // - x ** y, where x < 0 and y is non-integer decimal => imaginary value +// - x ** y, where the magnitude of the result or the precision is too large to represent // // Example: // @@ -712,83 +717,30 @@ func (d Decimal) Mod(d2 Decimal) Decimal { // d3 := decimal.NewFromFloat(5.0) // d4 := decimal.NewFromFloat(5.73) // res2 := d3.Pow(d4) -// res2.String() // output: "10118.08037125" +// res2.String() // output: "10118.0803715950193171" +// +// d5 := decimal.NewFromInt(10) +// d6 := decimal.NewFromInt(-18) +// res3 := d5.Pow(d6) +// res3.String() // output: "0.000000000000000001" func (d Decimal) Pow(d2 Decimal) Decimal { - baseSign := d.Sign() - expSign := d2.Sign() - - if baseSign == 0 { - if expSign == 0 { - return Decimal{} - } - if expSign == 1 { - return Decimal{zeroInt, 0} - } - if expSign == -1 { - return Decimal{} - } - } - - if expSign == 0 { - return Decimal{oneInt, 0} - } - - // TODO: optimize extraction of fractional part - one := Decimal{oneInt, 0} - expIntPart, expFracPart := d2.QuoRem(one, 0) - - if baseSign == -1 && !expFracPart.IsZero() { - return Decimal{} - } - - intPartPow, _ := d.PowBigInt(expIntPart.getValue()) - - // if exponent is an integer we don't need to calculate d1**frac(d2) - if expFracPart.getValue().Sign() == 0 { - return intPartPow - } - - // TODO: optimize NumDigits for more performant precision adjustment - digitsBase := d.NumDigits() - digitsExponent := d2.NumDigits() - - precision := digitsBase - - if digitsExponent > precision { - precision += digitsExponent - } - - precision += 6 - - // Calculate x ** frac(y), where - // x ** frac(y) = exp(ln(x ** frac(y)) = exp(ln(x) * frac(y)) - fracPartPow, err := d.Abs().Ln(-d.exp + int32(precision)) + res, err := d.PowWithPrecision(d2, int32(PowPrecisionNegativeExponent)) if err != nil { return Decimal{} } - - fracPartPow = fracPartPow.Mul(expFracPart) - - fracPartPow, err = fracPartPow.ExpTaylor(-d.exp + int32(precision)) - if err != nil { - return Decimal{} - } - - // Join integer and fractional part, - // base ** (expBase + expFrac) = base ** expBase * base ** expFrac - res := intPartPow.Mul(fracPartPow) - return res } // PowWithPrecision returns d to the power of d2. -// Precision parameter specifies minimum precision of the result (digits after decimal point). -// Returned decimal is not rounded to 'precision' places after decimal point. +// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is +// rounded half away from zero to precision places after the decimal point. A result that would round +// to 0 keeps precision significant digits instead (at least one). // // PowWithPrecision returns error when: // - 0 ** 0 => undefined value // - 0 ** y, where y < 0 => infinity // - x ** y, where x < 0 and y is non-integer decimal => imaginary value +// - x ** y, where the magnitude of the result or the precision is too large to represent // // Example: // @@ -800,85 +752,40 @@ func (d Decimal) Pow(d2 Decimal) Decimal { // d3 := decimal.NewFromFloat(5.0) // d4 := decimal.NewFromFloat(5.73) // res2, err := d3.PowWithPrecision(d4, 5) -// res2.String() // output: "10118.080371595015625" +// res2.String() // output: "10118.08037" // // d5 := decimal.NewFromFloat(-3.0) // d6 := decimal.NewFromFloat(-6.0) // res3, err := d5.PowWithPrecision(d6, 10) // res3.String() // output: "0.0013717421" func (d Decimal) PowWithPrecision(d2 Decimal, precision int32) (Decimal, error) { - baseSign := d.Sign() - expSign := d2.Sign() - - if baseSign == 0 { - if expSign == 0 { + if d.IsZero() { + if d2.IsZero() { return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") } - if expSign == 1 { - return Decimal{zeroInt, 0}, nil - } - if expSign == -1 { + if d2.Sign() < 0 { return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") } + return Decimal{zeroInt, 0}, nil } - if expSign == 0 { - return Decimal{oneInt, 0}, nil + if d2.IsInteger() { + return d.powBigIntWithPrecision(d2.BigInt(), precision) } - // TODO: optimize extraction of fractional part - one := Decimal{oneInt, 0} - expIntPart, expFracPart := d2.QuoRem(one, 0) - - if baseSign == -1 && !expFracPart.IsZero() { + if d.Sign() < 0 { return Decimal{}, fmt.Errorf("cannot represent imaginary value of x ** y, where x < 0 and y is non-integer decimal") } - intPartPow, _ := d.powBigIntWithPrecision(expIntPart.getValue(), precision) - - // if exponent is an integer we don't need to calculate d1**frac(d2) - if expFracPart.getValue().Sign() == 0 { - return intPartPow, nil - } - - // TODO: optimize NumDigits for more performant precision adjustment - digitsBase := d.NumDigits() - digitsExponent := d2.NumDigits() - - if int32(digitsBase) > precision { - precision = int32(digitsBase) - } - if int32(digitsExponent) > precision { - precision += int32(digitsExponent) - } - // increase precision by 10 to compensate for errors in further calculations - precision += 10 - - // Calculate x ** frac(y), where - // x ** frac(y) = exp(ln(x ** frac(y)) = exp(ln(x) * frac(y)) - fracPartPow, err := d.Abs().Ln(precision) - if err != nil { - return Decimal{}, err - } - - fracPartPow = fracPartPow.Mul(expFracPart) - - fracPartPow, err = fracPartPow.ExpTaylor(precision) - if err != nil { - return Decimal{}, err - } - - // Join integer and fractional part, - // base ** (expBase + expFrac) = base ** expBase * base ** expFrac - res := intPartPow.Mul(fracPartPow) - - return res, nil + return d.powFrac(d2, precision) } // PowInt32 returns d to the power of exp, where exp is int32. -// Only returns error when d and exp is 0, thus result is undefined. +// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. // -// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places +// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant +// digits instead (at least one). // // Example: // @@ -888,37 +795,15 @@ func (d Decimal) PowWithPrecision(d2 Decimal, precision int32) (Decimal, error) // d2, err := decimal.NewFromFloat(3.13).PowInt32(5) // d2.String() // output: "300.4150512793" func (d Decimal) PowInt32(exp int32) (Decimal, error) { - if d.IsZero() && exp == 0 { - return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") - } - - isExpNeg := exp < 0 - exp = abs(exp) - - n, result := d, New(1, 0) - - for exp > 0 { - if exp%2 == 1 { - result = result.Mul(n) - } - exp /= 2 - - if exp > 0 { - n = n.Mul(n) - } - } - - if isExpNeg { - return New(1, 0).DivRound(result, int32(PowPrecisionNegativeExponent)), nil - } - - return result, nil + return d.PowBigInt(big.NewInt(int64(exp))) } // PowBigInt returns d to the power of exp, where exp is big.Int. -// Only returns error when d and exp is 0, thus result is undefined. +// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. // -// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places +// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant +// digits instead (at least one). // // Example: // @@ -931,36 +816,487 @@ func (d Decimal) PowBigInt(exp *big.Int) (Decimal, error) { return d.powBigIntWithPrecision(exp, int32(PowPrecisionNegativeExponent)) } +var errPowOutOfRange = fmt.Errorf("cannot represent result of power operation, its magnitude or precision is too large") + +// powExactBits is the largest estimated cost of computing d^n exactly for negative n. +// Bigger powers are approximated with truncated intermediates, which is cheaper. +const powExactBits = 3000 + func (d Decimal) powBigIntWithPrecision(exp *big.Int, precision int32) (Decimal, error) { - if d.IsZero() && exp.Sign() == 0 { + switch { + case exp.Sign() > 0: + return d.powExact(exp) + case d.IsZero() && exp.Sign() == 0: return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") + case exp.Sign() == 0: + return Decimal{oneInt, 0}, nil + case d.IsZero(): + return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") + } + return d.powNegInt(new(big.Int).Neg(exp), precision) +} + +// powExact returns d^n for n > 0. +func (d Decimal) powExact(n *big.Int) (Decimal, error) { + exp := new(big.Int).Mul(n, big.NewInt(int64(d.exp))) + if !exp.IsInt64() || exp.Int64() > math.MaxInt32 || exp.Int64() < math.MinInt32 { + return Decimal{}, errPowOutOfRange + } + return Decimal{new(big.Int).Exp(d.getValue(), n, nil), int32(exp.Int64())}, nil +} + +// powNegInt returns d^-n for n > 0, rounded as described in PowWithPrecision. +func (d Decimal) powNegInt(n *big.Int, precision int32) (Decimal, error) { + if d.absIsOne() { + // only the parity of n matters + n = big.NewInt(2 - int64(n.Bit(0))) } - tmpExp := new(big.Int).Set(exp) - isExpNeg := exp.Sign() < 0 - - if isExpNeg { - tmpExp.Abs(tmpExp) - } - - n, result := d, New(1, 0) - - for tmpExp.Sign() > 0 { - if tmpExp.Bit(0) == 1 { - result = result.Mul(n) - } - tmpExp.Rsh(tmpExp, 1) - - if tmpExp.Sign() > 0 { - n = n.Mul(n) + // small powers are cheap to compute exactly, and |log10(d^-n)| <= cost keeps them in range + if n.IsInt64() && n.Int64() <= powExactBits { + cost := n.Int64() * (int64(d.getValue().BitLen()) + abs64(int64(d.exp))) + if cost <= powExactBits && powInRange(float64(cost), precision) { + return d.powNegIntExact(n, precision) } } - if isExpNeg { - return New(1, 0).DivRound(result, precision), nil + nf, _ := new(big.Float).SetInt(n).Float64() + lg := -nf * d.log10Abs() + if !powInRange(lg, precision) { + return Decimal{}, errPowOutOfRange } - return result, nil + // the digit shifts of the truncated c^n must fit into int64 + if nd := int64(d.NumDigits()); n.IsInt64() && n.Int64() <= math.MaxInt64/(4*nd) { + c := new(big.Int).Abs(d.getValue()) + w := powSigDigits(lg, precision) + int64(len(n.String())) + 6 + // each try adds 20 digits, failing 5 times means an exact tie or a value extremely close to one + for i := 0; i < 5; i, w = i+1, w+20 { + if res, ok := powNegIntApprox(c, int64(d.exp), n, w, precision); ok { + if d.Sign() < 0 && n.Bit(0) == 1 { + res.value.Neg(res.value) + } + return res, nil + } + } + } + + // same as for non-integer exponents, which handles any n and exact ties + res, err := d.Abs().powFrac(Decimal{value: new(big.Int).Neg(n)}, precision) + if err == nil && d.Sign() < 0 && n.Bit(0) == 1 { + res.value.Neg(res.value) + } + return res, err +} + +// powNegIntExact returns d^-n for n > 0, dividing by the exact d^n. +func (d Decimal) powNegIntExact(n *big.Int, precision int32) (Decimal, error) { + x, err := d.powExact(n) + if err != nil { + return Decimal{}, err + } + res := New(1, 0).DivRound(x, precision) + if res.IsZero() { + // first significant digit of 1/x is at place NumDigits+exp, one earlier when x is a power of ten + nd := int32(x.NumDigits()) + places := int32(powMinSig(precision)) - 1 + nd + x.exp + if x.getValue().CmpAbs(pow10(int64(nd)-1)) == 0 { + places-- + } + res = New(1, 0).DivRound(x, places) + } + return res, nil +} + +// powNegIntApprox rounds |1 / (c^n * 10^(e*n))| as described in PowWithPrecision. c^n is approximated +// from below by m*10^s, truncating m to w digits after every multiplication. Each truncation shrinks +// the result by a factor of at most 1-10^(1-w), and the errors add up to less than 5n*10^(1-w), +// lo below allows ten times that. +func powNegIntApprox(c *big.Int, e int64, n *big.Int, w int64, precision int32) (Decimal, bool) { + cw := new(big.Int).Set(c) + cs, exact := truncDigits(cw, w) + + m, s := big.NewInt(1), int64(0) + for i := n.BitLen() - 1; i >= 0; i-- { + m.Mul(m, m) + ds, ex := truncDigits(m, w) + s, exact = 2*s+ds, exact && ex + if n.Bit(i) == 1 { + m.Mul(m, cw) + ds, ex = truncDigits(m, w) + s, exact = s+cs+ds, exact && ex + } + } + + // 10^scale / (m * 10^(s+e*n)) = 10^z / m + z := int64(Decimal{value: m}.NumDigits()) + w + 2 + scale := z + s + e*n.Int64() + q, rem := new(big.Int).QuoRem(pow10(z), m, new(big.Int)) + lo, hi := q, q + if !exact || rem.Sign() != 0 { + hi = new(big.Int).Add(q, oneInt) + } + if !exact { + lo = new(big.Int).Mul(q, big.NewInt(5)) + lo.Mul(lo, n).Quo(lo, pow10(w-2)) + lo.Sub(q, lo).Sub(lo, oneInt) + } + return roundBounds(lo, hi, scale, precision) +} + +// truncDigits truncates m > 0 to at least w digits, returning the number of dropped digits +// and whether they were all zeros. +func truncDigits(m *big.Int, w int64) (int64, bool) { + // m >= 2^(BitLen-1), so m has at least floor((BitLen-1)*log10(2))+1 digits + drop := int64(float64(m.BitLen()-1)*math.Log10(2)-1e-9) + 1 - w + if drop <= 0 { + return 0, true + } + var r big.Int + m.QuoRem(m, pow10(drop), &r) + return drop, r.Sign() == 0 +} + +// powFrac returns d^y for d > 0 and non-integer (or negative integer) y, rounded as described in +// PowWithPrecision. It evaluates d^y = 10^q * e^s, where y*ln(d) = q*ln(10) + s and 0 <= s < ln(10), +// in binary fixed-point arithmetic, adding working digits until the error bounds decide the rounding. +func (d Decimal) powFrac(y Decimal, precision int32) (Decimal, error) { + if d.absIsOne() { + // 1^y = 1, also for y beyond float64 range + if !powInRange(0, precision) { + return Decimal{}, errPowOutOfRange + } + k := int64(abs(precision)) + 2 + res, _ := roundBounds(pow10(k), pow10(k), k, precision) + return res, nil + } + + yf := y.InexactFloat64() + lg := yf * d.log10Abs() + if !powInRange(lg, precision) { + return Decimal{}, errPowOutOfRange + } + + k := int64(d.NumDigits()) - 1 + int64(d.exp) + // covers the working error amplified by |y| and |k|, see powFracFixed + extra := int64(math.Log10(math.Abs(yf)*(math.Abs(float64(k))+64)+math.Abs(lg)+1)) + 4 + // 9 guard digits make a retry unlikely, each retry adds 20 more + f := powSigDigits(lg, precision) + 9 + exactTie := false + for i := 0; ; i, f = i+1, f+20 { + if res, ok := powFracFixed(d, y, k, f, extra, precision, exactTie); ok { + return res, nil + } + // only a terminating result can sit exactly on a rounding boundary, which no working + // precision resolves, then the upper bound rounds it away from zero + if i == 8 { + exactTie = powIsTerminating(d, y) + } + } +} + +// powIsTerminating reports whether d^y is a terminating decimal, for d > 0. +func powIsTerminating(d, y Decimal) bool { + p, q := powRatio(y) + num, den := powRatio(d) + if !q.IsInt64() { + return false + } + // d^(p/q) is rational only when num and den are perfect q-th powers + rn, ok := iroot(num, q.Int64()) + if !ok { + return false + } + rd, ok := iroot(den, q.Int64()) + if !ok { + return false + } + // (rn/rd)^p terminates when its denominator has no prime factors other than 2 and 5 + if p.Sign() < 0 { + rd = rn + } + rd = new(big.Int).Set(rd) + for _, f := range []*big.Int{twoInt, fiveInt} { + for r := new(big.Int); rd.Cmp(oneInt) > 0 && r.Rem(rd, f).Sign() == 0; { + rd.Quo(rd, f) + } + } + return rd.Cmp(oneInt) == 0 +} + +// powRatio returns d as a fraction p/q in lowest terms, q > 0. +func powRatio(d Decimal) (*big.Int, *big.Int) { + if d.exp >= 0 { + return new(big.Int).Mul(d.getValue(), pow10(int64(d.exp))), big.NewInt(1) + } + p, q := new(big.Int).Set(d.getValue()), new(big.Int).Set(pow10(-int64(d.exp))) + g := new(big.Int).GCD(nil, nil, new(big.Int).Abs(p), q) + return p.Quo(p, g), q.Quo(q, g) +} + +// iroot returns the integer k-th root of x >= 1 and whether it is exact. +func iroot(x *big.Int, k int64) (*big.Int, bool) { + if k == 1 || x.Cmp(oneInt) == 0 { + return x, true + } + if int64(x.BitLen()) <= k { + // 1 < x < 2^k has no integer k-th root + return nil, false + } + // Newton's method from above converges to the floor of the root + kb, km1 := big.NewInt(k), big.NewInt(k-1) + r := new(big.Int).Lsh(oneInt, uint((int64(x.BitLen())+k-1)/k)) + t, u := new(big.Int), new(big.Int) + for { + t.Quo(x, t.Exp(r, km1, nil)) + t.Add(t, u.Mul(r, km1)).Quo(t, kb) + if t.Cmp(r) >= 0 { + break + } + r.Set(t) + } + return r, t.Exp(r, kb, nil).Cmp(x) == 0 +} + +// powFracFixed computes d^y (see powFrac) as an interval of e^s * 10^f with f+extra working digits +// and rounds it. With force the upper bound is used as the exact value. +func powFracFixed(d, y Decimal, k, f, extra int64, precision int32, force bool) (Decimal, bool) { + b := uint(float64(f+extra)*math.Log2(10)) + 1 + + // d = m * 10^k, 1 <= m < 10 + m := new(big.Int).Lsh(d.getValue(), b) + m.Quo(m, pow10(int64(d.NumDigits())-1)) + t, lnErr := lnFixed(m, b) + ln10, ln10Err := ln10Fixed(b) + t.Add(t, new(big.Int).Mul(big.NewInt(k), ln10)) + tErr := big.NewInt(lnErr + 1 + abs64(k)*ln10Err) + + // t = y * ln(d) + t.Mul(t, y.getValue()) + if y.exp >= 0 { + t.Mul(t, pow10(int64(y.exp))) + } else { + t.Quo(t, pow10(-int64(y.exp))) + } + tErr.Mul(tErr, y.Abs().Ceil().getValue()).Add(tErr, oneInt) + + q, s := new(big.Int).DivMod(t, ln10, new(big.Int)) + sErr := new(big.Int).Abs(q) + sErr.Mul(sErr, big.NewInt(ln10Err)).Add(sErr, tErr) + + // e^s < 11, so an error in s grows at most 11 times + r, rErr := expFixed(s, b) + sErr.Mul(sErr, big.NewInt(11)).Add(sErr, big.NewInt(rErr+1)) + + // v * 10^(f-q) = e^s * 10^f + p := pow10(f) + hi := new(big.Int).Add(r, sErr) + hi.Mul(hi, p).Rsh(hi, b).Add(hi, oneInt) + lo := hi + if !force { + lo = new(big.Int).Sub(r, sErr) + lo.Mul(lo, p).Rsh(lo, b) + if lo.Sign() < 0 { + lo.SetInt64(0) + } + } + return roundBounds(lo, hi, f-q.Int64(), precision) +} + +// lnFixed returns ln(m / 2^b) * 2^b for m / 2^b in [1, 10], with its error bound in ulps. +func lnFixed(m *big.Int, b uint) (*big.Int, int64) { + // a = math.Log(m) is accurate to ~1e-15, so ln(m) = a + ln(1+z), where z = m*e^-a - 1 is tiny + // and the series ln(1+z) = z - z^2/2 + z^3/3 - ... gains ~50 bits per term. + top, sh := new(big.Int), m.BitLen()-60 + if sh > 0 { + top.Rsh(m, uint(sh)) + } else { + top.Lsh(m, uint(-sh)) + } + a := big.NewInt(int64(math.Ldexp(math.Log(math.Ldexp(float64(top.Uint64()), sh-int(b))), 52))) + if b >= 52 { + a.Lsh(a, b-52) + } else { + a.Rsh(a, 52-b) + } + + e, eErr := expFixed(new(big.Int).Neg(a), b) + z := e.Mul(e, m).Rsh(e, b) + z.Sub(z, new(big.Int).Lsh(oneInt, b)) + + sum, p, term, iv := new(big.Int).Set(z), new(big.Int).Set(z), new(big.Int), new(big.Int) + terms := int64(1) + for i := int64(2); ; i++ { + p.Mul(p, z).Rsh(p, b) + if p.Sign() == 0 { + break + } + term.Quo(p, iv.SetInt64(i)) + if i%2 == 0 { + sum.Sub(sum, term) + } else { + sum.Add(sum, term) + } + terms++ + } + // m <= 10 amplifies the error of e^-a at most 10 times + return sum.Add(sum, a), 10*eErr + 2*terms + 3 +} + +// expFixed returns e^(x / 2^b) * 2^b for |x / 2^b| <= 2.4, with its error bound in ulps. +func expFixed(x *big.Int, b uint) (*big.Int, int64) { + // e^x = (e^(x/2^j))^(2^j): Taylor series of the reduced argument, then j squarings. + // Guard bits cover the error of up to ~g Taylor terms, amplified up to 11*2^j times by squaring. + j := uint(math.Sqrt(float64(b))) + 1 + g := b + j + uint(bits.Len(33*(b+j+64)+363)) + 2 + + xr := new(big.Int).Lsh(x, g-b) + xr.Rsh(xr, j) + sum, term, iv := new(big.Int).Lsh(oneInt, g), new(big.Int).Lsh(oneInt, g), new(big.Int) + for i := int64(1); ; i++ { + term.Mul(term, xr).Rsh(term, g) + term.Quo(term, iv.SetInt64(i)) + if term.Sign() == 0 { + break + } + sum.Add(sum, term) + } + for ; j > 0; j-- { + sum.Mul(sum, sum).Rsh(sum, g) + } + return sum.Rsh(sum, g-b), 2 +} + +var ln10Cache struct { + once sync.Once + v *big.Int +} + +const ln10CacheBits = 2048 + +// ln10Fixed returns ln(10) * 2^b, with its error bound in ulps. +func ln10Fixed(b uint) (*big.Int, int64) { + if b <= ln10CacheBits { + ln10Cache.once.Do(func() { ln10Cache.v, _ = ln10FromDigits(ln10CacheBits) }) + return new(big.Int).Rsh(ln10Cache.v, ln10CacheBits-b), 3 + } + if v, ok := ln10FromDigits(b); ok { + return v, 2 + } + return lnFixed(new(big.Int).Lsh(big.NewInt(10), b), b) +} + +// ln10FromDigits returns ln(10) * 2^b computed from strLn10, if it has enough digits. +func ln10FromDigits(b uint) (*big.Int, bool) { + digits := int(float64(b)*math.Log10(2)) + 3 + if digits+2 > len(strLn10) { + return nil, false + } + v, _ := new(big.Int).SetString(strLn10[:1]+strLn10[2:digits+2], 10) + v.Lsh(v, b) + return v.Quo(v, pow10(int64(digits))), true +} + +// roundBounds rounds v, known only by lo <= v*10^scale <= hi (lo >= 0), half away from zero to +// precision places after the decimal point, or to max(precision, 1) significant digits when that +// would be 0. It reports false when lo and hi do not round to the same value. +func roundBounds(lo, hi *big.Int, scale int64, precision int32) (Decimal, bool) { + g := scale - int64(precision) + if g < 1 { + return Decimal{}, false + } + dhi := int64(Decimal{value: hi}.NumDigits()) + // with fewer than g digits hi rounds to 0, skip building 10^g for tiny results + if g <= dhi { + if q := roundHalfUp(hi, g); q.Sign() != 0 { + return Decimal{q, -precision}, q.Cmp(roundHalfUp(lo, g)) == 0 + } + } + + sig := powMinSig(precision) + g = dhi - sig + if g < 2 { + return Decimal{}, false + } + q := roundHalfUp(hi, g) + res := Decimal{q, int32(g - scale)} + if int64(Decimal{value: lo}.NumDigits()) == dhi { + return res, q.Cmp(roundHalfUp(lo, g)) == 0 + } + // lo < 10^(dhi-1) <= hi, decided only when both round to that power of ten + return res, q.Cmp(pow10(sig-1)) == 0 && roundHalfUp(lo, g-1).Cmp(pow10(sig)) == 0 +} + +// roundHalfUp returns x / 10^g rounded half away from zero, for x >= 0 and g >= 1. +func roundHalfUp(x *big.Int, g int64) *big.Int { + p := pow10(g) + q, r := new(big.Int).QuoRem(x, p, new(big.Int)) + if r.Lsh(r, 1).Cmp(p) >= 0 { + q.Add(q, oneInt) + } + return q +} + +// powSigDigits returns the number of significant digits of a result of magnitude 10^lg rounded to +// precision places, at least powMinSig(precision). +func powSigDigits(lg float64, precision int32) int64 { + if n := int64(math.Floor(lg)) + 1 + int64(precision); n > powMinSig(precision) { + return n + } + return powMinSig(precision) +} + +// powMinSig returns the significant digits kept by results that would round to zero. +func powMinSig(precision int32) int64 { + if precision < 1 { + return 1 + } + return int64(precision) +} + +// powInRange reports whether a result of magnitude 10^lg rounded to precision places has an exponent +// that fits into int32. +func powInRange(lg float64, precision int32) bool { + return math.Abs(lg)+math.Abs(float64(precision)) < math.MaxInt32/2 +} + +// absIsOne reports whether |d| = 1. +func (d Decimal) absIsOne() bool { + nd := int64(d.NumDigits()) + return nd-1+int64(d.exp) == 0 && d.getValue().CmpAbs(pow10(nd-1)) == 0 +} + +// log10Abs approximates log10(|d|) for d != 0. +func (d Decimal) log10Abs() float64 { + var m big.Float + e2 := new(big.Float).SetInt(d.getValue()).MantExp(&m) + mf, _ := m.Float64() + return math.Log10(math.Abs(mf)) + float64(e2)*math.Log10(2) + float64(d.exp) +} + +// pow10Table holds 10^0 ... 10^127, its values must not be modified. +var pow10Table = func() []*big.Int { + t := make([]*big.Int, 128) + t[0] = big.NewInt(1) + for i := 1; i < len(t); i++ { + t[i] = new(big.Int).Mul(t[i-1], tenInt) + } + return t +}() + +// pow10 returns 10^n for n >= 0, the result must not be modified. +func pow10(n int64) *big.Int { + if n < int64(len(pow10Table)) { + return pow10Table[n] + } + return new(big.Int).Exp(tenInt, big.NewInt(n), nil) +} + +func abs64(n int64) int64 { + if n < 0 { + return -n + } + return n } // ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 4e46251..834040e 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -249,6 +249,15 @@ func BenchmarkDecimal_PowBigInt(b *testing.B) { } } +func BenchmarkDecimal_PowNegativeExponent(b *testing.B) { + d1 := RequireFromString("1.0041666666666667") + d2 := RequireFromString("-360") + + for i := 0; i < b.N; i++ { + d1.Pow(d2) + } +} + func BenchmarkDecimal_NewFromString(b *testing.B) { count := 72 prices := make([]string, 0, count) diff --git a/decimal_test.go b/decimal_test.go index 48c1e21..d81520b 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2698,19 +2698,41 @@ func TestDecimal_Pow(t *testing.T) { {"4.0", "2.0", "16.0"}, {"4.0", "-2.0", "0.0625"}, {"629.25", "5.0", "98654323103449.5673828125"}, - {"5.0", "5.73", "10118.08037159375"}, - {"962.0", "3.2791", "6055212360.0000044205714144"}, - {"5.69169126", "5.18515912", "8242.26344757948412597909547972726268869189399260047793106028930864"}, - {"13.1337", "3.5196719618391835", "8636.856220644773844815693636723928750940666269885"}, - {"67762386.283696923", "4.85917691669163916681738", "112761146905370140621385730157437443321.91755738117317148674362233906499698561022574811238435007575701773212242750262081945556470501"}, + {"5.0", "5.73", "10118.0803715950193171"}, + {"962.0", "3.2791", "6055212360.0000044065516031"}, + {"5.69169126", "5.18515912", "8242.263447579484126"}, + {"13.1337", "3.5196719618391835", "8636.8562206447738448"}, + {"67762386.283696923", "4.85917691669163916681738", "112761146905370140621385730157437443321.9175573811731715"}, {"-3.0", "6.0", "729"}, {"-13.757", "5.0", "-492740.983929899460557"}, {"3.0", "-6.0", "0.0013717421124829"}, {"13.757", "-5.0", "0.000002029463821"}, - {"66.12", "-7.61313", "0.000000000000013854086588876805036"}, - {"6696871.12", "-2.61313", "0.000000000000000001455988684546983"}, + {"66.12", "-7.61313", "0.0000000000000139"}, + {"6696871.12", "-2.61313", "0.000000000000000001455825273411375"}, {"-3.0", "-6.0", "0.0013717421124829"}, {"-13.757", "-5.0", "-0.000002029463821"}, + {"-2", "-3", "-0.125"}, + {"0.5", "-20", "1048576"}, + {"2", "-17", "0.0000076293945313"}, + {"10", "-18", "0.000000000000000001"}, + {"100", "-17", "0.0000000000000000000000000000000001"}, + {"12345", "-5", "0.000000000000000000003487743424943423"}, + {"1000", "-6.5", "0.00000000000000000003162277660168379"}, + {"1.0041666666666667", "-360", "0.2238265956413493"}, + {"1.0041666666666667", "360.5", "4.4770424391814548"}, + {"0.01", "-7.585", "1479108388168207.4221993463468552"}, + {"1.5", "-100000", "7.483321982518254e-17610"}, + {"-1.5", "-1001", "-5.403183104351711e-177"}, + {"-1.5", "-1000", "8.104774656527567e-177"}, + {"1.0000001", "-1000000000", "0.00000000000000000000000000000000000000000003720094576445977"}, + {"1.0000001", "1000000000.5", "26881038356701055136223322520735843418287670.4854123820834493"}, + {"100.0000000000000000", "0.5", "10"}, + {"4", "2.5", "32"}, + {"2", "1.1", "2.1435469250725863"}, + {"2", "0.5", "1.414213562373095"}, + {"1.28", "0.0833333333333333", "1.0207847284895002"}, + {"0.0208333333333333", "0.33", "0.2787342852450195"}, + {"1", "1" + strings.Repeat("0", 400) + ".5", "1"}, } { base, _ := NewFromString(testCase.Base) exp, _ := NewFromString(testCase.Exponent) @@ -2743,19 +2765,31 @@ func TestDecimal_PowWithPrecision(t *testing.T) { {"4.0", "-2.0", 2, "0.06"}, {"4.0", "-2.0", 4, "0.0625"}, {"629.25", "5.0", 6, "98654323103449.5673828125"}, - {"5.0", "5.73", 20, "10118.080371595019317118681359884375"}, - {"962.0", "3.2791", 15, "6055212360.000004406551603058195732"}, - {"5.69169126", "5.18515912", 4, "8242.26344757948412587366859330429895955552280978668983459852256"}, - {"13.1337", "3.5196719618391835", 8, "8636.85622064477384481569363672392591908386390769375"}, - {"67762386.283696923", "4.85917691669163916681738", 10, "112761146905370140621385730157437443321.917557381173174638304347353880676293576708009282115993465286373470882947470198597518762"}, + {"5.0", "5.73", 20, "10118.08037159501931711868"}, + {"962.0", "3.2791", 15, "6055212360.000004406551603"}, + {"5.69169126", "5.18515912", 4, "8242.2634"}, + {"13.1337", "3.5196719618391835", 8, "8636.85622064"}, + {"67762386.283696923", "4.85917691669163916681738", 10, "112761146905370140621385730157437443321.9175573812"}, {"-3.0", "6.0", 2, "729"}, {"-13.757", "5.0", 4, "-492740.983929899460557"}, {"3.0", "-6.0", 10, "0.0013717421"}, {"13.757", "-5.0", 20, "0.00000202946382098037"}, - {"66.12", "-7.61313", 20, "0.00000000000001385381563049821591633907104023700216"}, - {"6696871.12", "-2.61313", 24, "0.0000000000000000014558252733872790626400278983397459207418"}, + {"66.12", "-7.61313", 20, "0.00000000000001385382"}, + {"6696871.12", "-2.61313", 24, "0.000000000000000001455825"}, {"-3.0", "-6.0", 8, "0.00137174"}, {"-13.757", "-5.0", 16, "-0.000002029463821"}, + {"10", "-18", 5, "0.000000000000000001"}, + {"5.63379", "-6.438", 2, "0.000015"}, + {"1000", "10.5", 5, "31622776601683793319988935444327.18534"}, + {"123.456", "7.89", 10, "31771028258180977.3090686597"}, + {"0.3", "-2.5", 16, "20.2860206483394857"}, + {"6.25", "0.5", 0, "3"}, + {"6.24" + strings.Repeat("9", 200), "0.5", 0, "2"}, + {"1.0041666666666667", "-360", -1, "0.2"}, + {"0.0625", "0.5", 1, "0.3"}, + {"0.00000625", "0.5", 1, "0.003"}, + {"2", "0.5", -1, "1"}, + {"123456", "0.5", -2, "400"}, } { base, _ := NewFromString(testCase.Base) exp, _ := NewFromString(testCase.Exponent) @@ -2859,6 +2893,11 @@ func TestDecimal_PowInt32(t *testing.T) { {"-13.757", 5, "-492740.983929899460557"}, {"3.0", -6, "0.0013717421124829"}, {"-13.757", -5, "-0.000002029463821"}, + {"10", -18, "0.000000000000000001"}, + {"12345", -5, "0.000000000000000000003487743424943423"}, + {"-1.5", -1001, "-5.403183104351711e-177"}, + {"1.5", math.MinInt32, "4.189793427303781e-378153100"}, + {"3e50000000", -1, "3.333333333333333e-50000001"}, } { base, _ := NewFromString(testCase.Decimal) expected, _ := NewFromString(testCase.Expected) @@ -2871,6 +2910,86 @@ func TestDecimal_PowInt32(t *testing.T) { } } +func TestDecimal_Pow_InexactResultExponent(t *testing.T) { + for _, testCase := range []struct { + Base string + Exponent string + }{ + {"3", "-6"}, + {"2", "0.5"}, + {"5.2", "6.3"}, + } { + result := RequireFromString(testCase.Base).Pow(RequireFromString(testCase.Exponent)) + if result.Exponent() != -int32(PowPrecisionNegativeExponent) { + t.Errorf("expected exponent %d, got %d, for %s^%s", -PowPrecisionNegativeExponent, result.Exponent(), testCase.Base, testCase.Exponent) + } + } +} + +func TestDecimal_Pow_RoundedToZeroResultExponent(t *testing.T) { + for _, testCase := range []struct { + Exponent int32 + Expected int32 + }{ + {-18, -33}, + {-1018, -1033}, + } { + result, _ := New(10, 0).PowInt32(testCase.Exponent) + if result.Exponent() != testCase.Expected { + t.Errorf("expected exponent %d, got %d, for 10**%d", testCase.Expected, result.Exponent(), testCase.Exponent) + } + } +} + +func TestDecimal_PowWithPrecision_HighPrecision(t *testing.T) { + two := New(2, 0) + for _, precision := range []int32{700, 3500} { + result, _ := two.PowWithPrecision(RequireFromString("0.5"), precision) + half := New(5, -precision-1) + lo, hi := result.Sub(half), result.Add(half) + if result.Exponent() != -precision || lo.Mul(lo).Cmp(two) >= 0 || hi.Mul(hi).Cmp(two) <= 0 { + t.Errorf("2^0.5 is not correctly rounded to %d places", precision) + } + } +} + +func TestDecimal_Pow_OutOfRange(t *testing.T) { + for _, testCase := range []struct { + Base string + Exponent string + }{ + {"10", "3000000000.5"}, + {"1.5", "-1000000000000"}, + {"0.5", "-4000000000.5"}, + {"0.1", "3000000000"}, + } { + base, exp := RequireFromString(testCase.Base), RequireFromString(testCase.Exponent) + + if _, err := base.PowWithPrecision(exp, 16); err == nil { + t.Errorf("expected out of range error for %s^%s", testCase.Base, testCase.Exponent) + } + if result := base.Pow(exp); !result.IsZero() { + t.Errorf("expected zero value for %s^%s, got %s", testCase.Base, testCase.Exponent, result) + } + } + + if _, err := RequireFromString("1.5").PowBigInt(big.NewInt(-1000000000000)); err == nil { + t.Errorf("expected out of range error for 1.5**-1000000000000") + } + if _, err := New(1, -2).PowInt32(1<<30 + 1); err == nil { + t.Errorf("expected out of range error for 0.01**1073741825") + } +} + +func TestDecimal_PowInt32_ZeroNegativeExponent(t *testing.T) { + if _, err := Zero.PowInt32(-2); err == nil { + t.Errorf("expected error, cannot represent infinity value of 0 ** y, where y < 0") + } + if _, err := Zero.PowBigInt(big.NewInt(-2)); err == nil { + t.Errorf("expected error, cannot represent infinity value of 0 ** y, where y < 0") + } +} + func TestDecimal_PowInt32_UndefinedResult(t *testing.T) { base := RequireFromString("0") @@ -2897,6 +3016,11 @@ func TestDecimal_PowBigInt(t *testing.T) { {"-13.757", big.NewInt(5), "-492740.983929899460557"}, {"3.0", big.NewInt(-6), "0.0013717421124829"}, {"-13.757", big.NewInt(-5), "-0.000002029463821"}, + {"10", big.NewInt(-18), "0.000000000000000001"}, + {"12345", big.NewInt(-5), "0.000000000000000000003487743424943423"}, + {"-1.5", big.NewInt(-1001), "-5.403183104351711e-177"}, + {"1.0000000000000000000000000000001", big.NewInt(-100000000000000000), "0.99999999999999"}, + {"-1", new(big.Int).Neg(new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil)), "1"}, } { base, _ := NewFromString(testCase.Decimal) expected, _ := NewFromString(testCase.Expected) From 684fe9e21d9e87ac60985f15693d9481ccbec4af Mon Sep 17 00:00:00 2001 From: "phuoc14.dev" Date: Sun, 4 Oct 2026 14:29:49 +0700 Subject: [PATCH 10/52] fix: Float64 hangs on decimals with huge exponents (#438) * fix: Float64 hangs on decimals with huge exponents Float64 went through Rat(), which builds 10^|exp| as a big.Int. With an exponent like -1<<30 that never finishes, even when the coefficient is 0. Zero and values whose magnitude is far outside the float64 range now return 0 or +/-Inf directly, matching what big.Rat.Float64 would give. Fixes #226 * docs: explain the Float64 magnitude bounds --- decimal.go | 17 +++++++++++++++++ decimal_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/decimal.go b/decimal.go index 7b8c5cc..7df0570 100644 --- a/decimal.go +++ b/decimal.go @@ -1808,6 +1808,23 @@ func (d Decimal) Rat() *big.Rat { // whether f represents d exactly. // For more details, see the documentation for big.Rat.Float64 func (d Decimal) Float64() (f float64, exact bool) { + // Rat() materializes 10^|exp|, which takes forever for huge exponents. + // Values that far outside the float64 range don't need it: they always + // round to zero or overflow to infinity. + sign := d.Sign() + if sign == 0 { + return 0, true + } + // |d| lies in [10^(magnitude-1), 10^magnitude). float64 spans roughly + // 1e-324..1e308, so ±400 is safely outside it with margin to spare. + magnitude := int64(d.NumDigits()) + int64(d.exp) + if magnitude < -400 { + return math.Copysign(0, float64(sign)), false + } + if magnitude > 400 { + return math.Inf(sign), false + } + return d.Rat().Float64() } diff --git a/decimal_test.go b/decimal_test.go index d81520b..ced5c92 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -307,6 +307,56 @@ func TestFloat64(t *testing.T) { } } +func TestFloat64OutOfRange(t *testing.T) { + type tc struct { + d Decimal + f float64 + exact bool + } + tests := []tc{ + // near the float64 limits, still converted the normal way + {New(1, 308), 1e308, false}, + {New(-1, 308), -1e308, false}, + {New(1, 309), math.Inf(1), false}, + {New(-1, 309), math.Inf(-1), false}, + {New(5, -324), 5e-324, false}, + {New(1, -325), 0, false}, + {New(-1, -325), math.Copysign(0, -1), false}, + {New(123456789, -340), 1.23456789e-332, false}, + {New(0, -1<<30), 0, true}, + {New(0, 1<<30), 0, true}, + // far outside the float64 range (issue #226) + {New(100, -1<<30), 0, false}, + {New(-100, -1<<30), math.Copysign(0, -1), false}, + {New(100, 1<<30), math.Inf(1), false}, + {New(-100, 1<<30), math.Inf(-1), false}, + {New(1, math.MinInt32), 0, false}, + {New(1, math.MaxInt32), math.Inf(1), false}, + {NewFromBigInt(new(big.Int).Exp(tenInt, big.NewInt(400), nil), -1<<30), 0, false}, + } + + for _, test := range tests { + done := make(chan struct{}) + var f float64 + var exact bool + go func() { + f, exact = test.d.Float64() + close(done) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatalf("Float64() of %de%d did not return within 5s", test.d.Coefficient(), test.d.Exponent()) + } + + if math.Float64bits(f) != math.Float64bits(test.f) || exact != test.exact { + t.Errorf("%de%d: expected (%v, %v), got (%v, %v)", + test.d.Coefficient(), test.d.Exponent(), test.f, test.exact, f, exact) + } + } +} + func TestNewFromStringErrs(t *testing.T) { tests := []string{ "", From 71ca20d5944f1a79b7f799d091941821de15cdf5 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 09:37:51 +0200 Subject: [PATCH 11/52] fix: limit exponents accepted by NewFromString and UnmarshalBinary (#440) * fix: limit exponents accepted by NewFromString and UnmarshalBinary * fix: rename limit to MaxDecodeExponent and lower its default to 10000 --- decimal.go | 22 ++++++++++++++++- decimal_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/decimal.go b/decimal.go index 7df0570..c219177 100644 --- a/decimal.go +++ b/decimal.go @@ -85,6 +85,19 @@ var UseScientificNotation = false // precise natural exponent value using ExpHullAbrham method. var ExpMaxIterations = 1000 +// MaxDecodeExponent is the largest exponent magnitude that NewFromString and UnmarshalBinary accept, +// and so UnmarshalJSON, UnmarshalText, Scan, GobDecode, DecodeSpanner and their NullDecimal variants too. +// Operations like String, Add and Float64 take time and memory that grow with the exponent, so without +// this limit a short input like "1e-2000000000" can stall the process or exhaust its memory. +// The default covers the float64 and IEEE 754 decimal128 exponent ranges. +// +// Values created with New, NewFromBigInt or arithmetic are not checked, so they may not decode back from +// their own String or MarshalBinary output. The limit does not bound the cost of Pow or ExpTaylor +// arguments or of very long inputs; validate those separately. +// +// Set it once at program start, before any decoding. Raise it only for trusted input. +var MaxDecodeExponent = 10000 + // Zero constant, to make computations faster. // Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. var Zero = Decimal{} @@ -272,6 +285,9 @@ func NewFromString(value string) (Decimal, error) { // NOTE(vadim): I doubt a string could realistically be this long return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput) } + if exp > int64(MaxDecodeExponent) || exp < -int64(MaxDecodeExponent) { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent %d exceeds MaxDecodeExponent (%d)", originalInput, exp, MaxDecodeExponent) + } return Decimal{ value: dValue, @@ -2174,7 +2190,11 @@ func (d *Decimal) UnmarshalBinary(data []byte) error { } // Extract the exponent - d.exp = int32(binary.BigEndian.Uint32(data[:4])) + exp := int32(binary.BigEndian.Uint32(data[:4])) + if int64(exp) > int64(MaxDecodeExponent) || int64(exp) < -int64(MaxDecodeExponent) { + return fmt.Errorf("error decoding binary: exponent %d exceeds MaxDecodeExponent (%d)", exp, MaxDecodeExponent) + } + d.exp = exp // Extract the value d.value = new(big.Int) diff --git a/decimal_test.go b/decimal_test.go index ced5c92..5a9731e 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -405,6 +405,36 @@ func TestNewFromStringErrs(t *testing.T) { } } +func TestMaxDecodeExponent(t *testing.T) { + limit := int32(MaxDecodeExponent) + for _, c := range []struct { + exp int32 + ok bool + }{ + {limit, true}, + {-limit, true}, + {limit + 1, false}, + {-limit - 1, false}, + {math.MaxInt32, false}, + {math.MinInt32, false}, + } { + if _, err := NewFromString(fmt.Sprintf("1e%d", c.exp)); (err == nil) != c.ok { + t.Errorf("NewFromString(1e%d): got error %v, want ok=%v", c.exp, err, c.ok) + } + + b, _ := New(1, c.exp).MarshalBinary() + d := New(7, 0) + err := d.UnmarshalBinary(b) + if (err == nil) != c.ok || (!c.ok && d.Exponent() != 0) { + t.Errorf("UnmarshalBinary(exp %d): got error %v and exp %d, want ok=%v", c.exp, err, d.Exponent(), c.ok) + } + } + + if _, err := NewFromString(fmt.Sprintf("0.1e%d", -limit)); err == nil { + t.Errorf("NewFromString(0.1e%d) accepted exponent %d", -limit, -limit-1) + } +} + func TestNewFromStringDeepEquals(t *testing.T) { type StrCmp struct { str1 string @@ -2771,7 +2801,6 @@ func TestDecimal_Pow(t *testing.T) { {"1.0041666666666667", "-360", "0.2238265956413493"}, {"1.0041666666666667", "360.5", "4.4770424391814548"}, {"0.01", "-7.585", "1479108388168207.4221993463468552"}, - {"1.5", "-100000", "7.483321982518254e-17610"}, {"-1.5", "-1001", "-5.403183104351711e-177"}, {"-1.5", "-1000", "8.104774656527567e-177"}, {"1.0000001", "-1000000000", "0.00000000000000000000000000000000000000000003720094576445977"}, @@ -2784,9 +2813,9 @@ func TestDecimal_Pow(t *testing.T) { {"0.0208333333333333", "0.33", "0.2787342852450195"}, {"1", "1" + strings.Repeat("0", 400) + ".5", "1"}, } { - base, _ := NewFromString(testCase.Base) - exp, _ := NewFromString(testCase.Exponent) - expected, _ := NewFromString(testCase.Expected) + base := RequireFromString(testCase.Base) + exp := RequireFromString(testCase.Exponent) + expected := RequireFromString(testCase.Expected) result := base.Pow(exp) @@ -2794,6 +2823,11 @@ func TestDecimal_Pow(t *testing.T) { t.Errorf("expected %s, got %s, for %s^%s", testCase.Expected, result.String(), testCase.Base, testCase.Exponent) } } + + // The expected exponent is beyond MaxDecodeExponent, so it can't be parsed from a string. + if got := New(15, -1).Pow(New(-100000, 0)); got.Cmp(New(7483321982518254, -17625)) != 0 { + t.Errorf("expected 7.483321982518254e-17610, got %s, for 1.5^-100000", got) + } } func TestDecimal_PowWithPrecision(t *testing.T) { @@ -2946,11 +2980,9 @@ func TestDecimal_PowInt32(t *testing.T) { {"10", -18, "0.000000000000000001"}, {"12345", -5, "0.000000000000000000003487743424943423"}, {"-1.5", -1001, "-5.403183104351711e-177"}, - {"1.5", math.MinInt32, "4.189793427303781e-378153100"}, - {"3e50000000", -1, "3.333333333333333e-50000001"}, } { - base, _ := NewFromString(testCase.Decimal) - expected, _ := NewFromString(testCase.Expected) + base := RequireFromString(testCase.Decimal) + expected := RequireFromString(testCase.Expected) result, _ := base.PowInt32(testCase.Exponent) @@ -2958,6 +2990,23 @@ func TestDecimal_PowInt32(t *testing.T) { t.Errorf("expected %s, got %s, for %s**%d", testCase.Expected, result.String(), testCase.Decimal, testCase.Exponent) } } + + // Exponents beyond MaxDecodeExponent can't be parsed, and String() on them is too slow for error messages. + for _, testCase := range []struct { + Decimal Decimal + Exponent int32 + Expected Decimal + }{ + {New(15, -1), math.MinInt32, New(4189793427303781, -378153115)}, + {New(3, 50000000), -1, New(3333333333333333, -50000016)}, + } { + result, err := testCase.Decimal.PowInt32(testCase.Exponent) + if err != nil || result.Cmp(testCase.Expected) != 0 { + t.Errorf("expected %de%d, got %de%d (err %v), for %de%d**%d", + testCase.Expected.Coefficient(), testCase.Expected.Exponent(), result.Coefficient(), result.Exponent(), err, + testCase.Decimal.Coefficient(), testCase.Decimal.Exponent(), testCase.Exponent) + } + } } func TestDecimal_Pow_InexactResultExponent(t *testing.T) { From fdefb63ebca39beefde9d4cb0419f8ee45801ddf Mon Sep 17 00:00:00 2001 From: Amit Mishra <96818986+amitmishra11@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:17:36 +0530 Subject: [PATCH 12/52] fix: normalize exponent in RoundUp/RoundDown/RoundFloor/RoundCeil and support negative precision in Truncate (#426) RoundUp, RoundDown, RoundFloor, and RoundCeil all returned the original decimal (with its original exponent) when the value was already exact at the requested precision. This caused the result to carry a trailing exponent that did not match the requested places. For example, (100.0).RoundUp(0) returned a Decimal with exp=-1 instead of exp=0, so StringFixedBank(-d.Exponent()) would produce "100.0" rather than "100". Fix: when the rescaled value equals the original, return the rescaled form (which has the canonical exponent) instead of the original. Truncate previously rejected negative precision values due to a precision >= 0 guard, silently returning the input unchanged. The rescale function already uses integer division (Quo, truncating toward zero), so removing the guard is sufficient to make negative precision work correctly. For example, (5432).Truncate(-2) now returns 5400 instead of 5432. Fixes #390 Fixes #406 Co-authored-by: Amit Mishra --- decimal.go | 18 ++++++---- decimal_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/decimal.go b/decimal.go index c219177..30c3d77 100644 --- a/decimal.go +++ b/decimal.go @@ -1959,7 +1959,7 @@ func (d Decimal) RoundCeil(places int32) Decimal { rescaled := d.rescale(-places) if d.Equal(rescaled) { - return d + return rescaled } if d.getValue().Sign() > 0 { @@ -1984,7 +1984,7 @@ func (d Decimal) RoundFloor(places int32) Decimal { rescaled := d.rescale(-places) if d.Equal(rescaled) { - return d + return rescaled } if d.getValue().Sign() < 0 { @@ -2009,7 +2009,7 @@ func (d Decimal) RoundUp(places int32) Decimal { rescaled := d.rescale(-places) if d.Equal(rescaled) { - return d + return rescaled } if d.getValue().Sign() > 0 { @@ -2036,7 +2036,7 @@ func (d Decimal) RoundDown(places int32) Decimal { rescaled := d.rescale(-places) if d.Equal(rescaled) { - return d + return rescaled } return rescaled } @@ -2143,13 +2143,17 @@ func (d Decimal) Ceil() Decimal { // Truncate truncates off digits from the number, without rounding. // -// NOTE: precision is the last digit that will not be truncated (must be >= 0). +// If precision >= 0, it specifies the number of decimal places to keep. +// If precision < 0, it truncates the integer part to the nearest 10^(-precision) +// towards zero. // // Example: // -// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" +// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" +// decimal.NewFromString("5432").Truncate(-2).String() // "5400" +// decimal.NewFromString("-5432").Truncate(-2).String() // "-5400" func (d Decimal) Truncate(precision int32) Decimal { - if precision >= 0 && -precision > d.exp { + if -precision > d.exp { return d.rescale(-precision) } return d diff --git a/decimal_test.go b/decimal_test.go index 5a9731e..ad32db6 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4276,3 +4276,99 @@ func ExampleNewFromFloat() { // 0.123123123123123 // -10000000000000 } + +// TestTruncateNegativePrecision verifies that Truncate correctly handles +// negative precision values, truncating the integer part towards zero. +func TestTruncateNegativePrecision(t *testing.T) { + type testCase struct { + input string + places int32 + want string + wantExp int32 + } + tests := []testCase{ + // negative precision: truncate integer part + {"5432", -2, "5400", 2}, + {"-5432", -2, "-5400", 2}, + {"5499", -2, "5400", 2}, + {"5500", -2, "5500", 2}, + {"5501", -2, "5500", 2}, + {"999", -3, "0", 3}, + {"1000", -3, "1000", 3}, + {"1001", -3, "1000", 3}, + {"-999", -3, "0", 3}, + {"-1000", -3, "-1000", 3}, + // positive precision still works as before + {"123.456", 2, "123.45", -2}, + {"123.456", 0, "123", 0}, + {"123.456", 5, "123.456", -3}, + } + + for _, tc := range tests { + d := RequireFromString(tc.input) + got := d.Truncate(tc.places) + if got.String() != tc.want { + t.Errorf("(%s).Truncate(%d): got %s, want %s", tc.input, tc.places, got.String(), tc.want) + } + if got.exp != tc.wantExp { + t.Errorf("(%s).Truncate(%d): got exponent %d, want %d", tc.input, tc.places, got.exp, tc.wantExp) + } + } +} + +// TestRoundingExponentNormalization verifies that RoundUp, RoundDown, +// RoundCeil, and RoundFloor return a result with the exponent normalized +// to the requested number of places, even when the value is already exact. +func TestRoundingExponentNormalization(t *testing.T) { + type testCase struct { + fn string + input string + places int32 + wantStr string + wantExp int32 + } + + tests := []testCase{ + // RoundUp + {"RoundUp", "100.0", 0, "100", 0}, + {"RoundUp", "100.00", 0, "100", 0}, + {"RoundUp", "3.14", 2, "3.14", -2}, + {"RoundUp", "500", -2, "500", 2}, + // RoundDown + {"RoundDown", "100.0", 0, "100", 0}, + {"RoundDown", "100.00", 0, "100", 0}, + {"RoundDown", "3.14", 2, "3.14", -2}, + {"RoundDown", "500", -2, "500", 2}, + // RoundFloor + {"RoundFloor", "100.0", 0, "100", 0}, + {"RoundFloor", "100.00", 0, "100", 0}, + {"RoundFloor", "-100.0", 0, "-100", 0}, + {"RoundFloor", "500", -2, "500", 2}, + // RoundCeil + {"RoundCeil", "100.0", 0, "100", 0}, + {"RoundCeil", "100.00", 0, "100", 0}, + {"RoundCeil", "-100.0", 0, "-100", 0}, + {"RoundCeil", "500", -2, "500", 2}, + } + + for _, tc := range tests { + d := RequireFromString(tc.input) + var got Decimal + switch tc.fn { + case "RoundUp": + got = d.RoundUp(tc.places) + case "RoundDown": + got = d.RoundDown(tc.places) + case "RoundFloor": + got = d.RoundFloor(tc.places) + case "RoundCeil": + got = d.RoundCeil(tc.places) + } + if got.String() != tc.wantStr { + t.Errorf("(%s).%s(%d): got %s, want %s", tc.input, tc.fn, tc.places, got.String(), tc.wantStr) + } + if got.exp != tc.wantExp { + t.Errorf("(%s).%s(%d): got exponent %d, want %d", tc.input, tc.fn, tc.places, got.exp, tc.wantExp) + } + } +} From 0911c5f1b57c365452d89f759ca2ec6d0a2c9fe2 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 09:53:38 +0200 Subject: [PATCH 13/52] fix: directed rounding always returns the requested exponent (#441) --- decimal.go | 16 ++++------------ decimal_test.go | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/decimal.go b/decimal.go index 30c3d77..c59ef6a 100644 --- a/decimal.go +++ b/decimal.go @@ -1954,7 +1954,7 @@ func (d Decimal) Round(places int32) Decimal { // NewFromFloat(-1.454).RoundCeil(1).String() // output: "-1.4" func (d Decimal) RoundCeil(places int32) Decimal { if d.exp >= -places { - return d + return d.rescale(-places) } rescaled := d.rescale(-places) @@ -1979,7 +1979,7 @@ func (d Decimal) RoundCeil(places int32) Decimal { // NewFromFloat(-1.454).RoundFloor(1).String() // output: "-1.5" func (d Decimal) RoundFloor(places int32) Decimal { if d.exp >= -places { - return d + return d.rescale(-places) } rescaled := d.rescale(-places) @@ -2004,7 +2004,7 @@ func (d Decimal) RoundFloor(places int32) Decimal { // NewFromFloat(-1.454).RoundUp(1).String() // output: "-1.5" func (d Decimal) RoundUp(places int32) Decimal { if d.exp >= -places { - return d + return d.rescale(-places) } rescaled := d.rescale(-places) @@ -2030,15 +2030,7 @@ func (d Decimal) RoundUp(places int32) Decimal { // NewFromFloat(1.1001).RoundDown(2).String() // output: "1.1" // NewFromFloat(-1.454).RoundDown(1).String() // output: "-1.4" func (d Decimal) RoundDown(places int32) Decimal { - if d.exp >= -places { - return d - } - - rescaled := d.rescale(-places) - if d.Equal(rescaled) { - return rescaled - } - return rescaled + return d.rescale(-places) } // RoundBank rounds the decimal to places decimal places. diff --git a/decimal_test.go b/decimal_test.go index ad32db6..b34808d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4281,10 +4281,10 @@ func ExampleNewFromFloat() { // negative precision values, truncating the integer part towards zero. func TestTruncateNegativePrecision(t *testing.T) { type testCase struct { - input string - places int32 - want string - wantExp int32 + input string + places int32 + want string + wantExp int32 } tests := []testCase{ // negative precision: truncate integer part @@ -4334,21 +4334,30 @@ func TestRoundingExponentNormalization(t *testing.T) { {"RoundUp", "100.00", 0, "100", 0}, {"RoundUp", "3.14", 2, "3.14", -2}, {"RoundUp", "500", -2, "500", 2}, + {"RoundUp", "100", 2, "100", -2}, + {"RoundUp", "5E3", -2, "5000", 2}, + {"RoundUp", "0.01230000000000000000000000000000", 4, "0.0123", -4}, // RoundDown {"RoundDown", "100.0", 0, "100", 0}, {"RoundDown", "100.00", 0, "100", 0}, {"RoundDown", "3.14", 2, "3.14", -2}, {"RoundDown", "500", -2, "500", 2}, + {"RoundDown", "100", 2, "100", -2}, + {"RoundDown", "5E3", -2, "5000", 2}, // RoundFloor {"RoundFloor", "100.0", 0, "100", 0}, {"RoundFloor", "100.00", 0, "100", 0}, {"RoundFloor", "-100.0", 0, "-100", 0}, {"RoundFloor", "500", -2, "500", 2}, + {"RoundFloor", "-100", 2, "-100", -2}, + {"RoundFloor", "5E3", -2, "5000", 2}, // RoundCeil {"RoundCeil", "100.0", 0, "100", 0}, {"RoundCeil", "100.00", 0, "100", 0}, {"RoundCeil", "-100.0", 0, "-100", 0}, {"RoundCeil", "500", -2, "500", 2}, + {"RoundCeil", "-100", 2, "-100", -2}, + {"RoundCeil", "5E3", -2, "5000", 2}, } for _, tc := range tests { From f6cc580f80e1378730007f041079d94ec4c61311 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 10:25:56 +0200 Subject: [PATCH 14/52] docs: explain that NewFromFloat can change large whole numbers (#442) --- decimal.go | 15 +++++++++++---- decimal_test.go | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/decimal.go b/decimal.go index c59ef6a..8f8f4a3 100644 --- a/decimal.go +++ b/decimal.go @@ -335,11 +335,16 @@ func RequireFromString(value string) Decimal { // NewFromFloat converts a float64 to Decimal. // -// The converted number will contain the number of significant digits that can be -// represented in a float with reliable roundtrip. +// The result is the shortest decimal that converts back to the same float64, +// the same number that strconv.FormatFloat(value, 'f', -1, 64) prints. // This is typically 15 digits, but may be more in some cases. // See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. // +// This is not always the exact value stored in the float. Whole numbers larger +// than 2^53 can change: float64(1<<62) is exactly 4611686018427387904, but +// NewFromFloat returns 4611686018427388000. To convert whole numbers exactly, +// use NewFromFloatWithExponent(value, 0). +// // For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. // // NOTE: this will panic on NaN, +/-inf @@ -352,11 +357,13 @@ func NewFromFloat(value float64) Decimal { // NewFromFloat32 converts a float32 to Decimal. // -// The converted number will contain the number of significant digits that can be -// represented in a float with reliable roundtrip. +// The result is the shortest decimal that converts back to the same float32. // This is typically 6-8 digits depending on the input. // See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. // +// As with NewFromFloat, whole numbers larger than 2^24 can change. To convert +// them exactly, use NewFromFloatWithExponent(float64(value), 0). +// // For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. // // NOTE: this will panic on NaN, +/-inf diff --git a/decimal_test.go b/decimal_test.go index b34808d..c5c05a1 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4271,10 +4271,14 @@ func ExampleNewFromFloat() { fmt.Println(NewFromFloat(123.123123123123).String()) fmt.Println(NewFromFloat(.123123123123123).String()) fmt.Println(NewFromFloat(-1e13).String()) + fmt.Println(NewFromFloat(1 << 62).String()) + fmt.Println(NewFromFloatWithExponent(1<<62, 0).String()) // OUTPUT: // 123.123123123123 // 0.123123123123123 // -10000000000000 + // 4611686018427388000 + // 4611686018427387904 } // TestTruncateNegativePrecision verifies that Truncate correctly handles From a37fbe42bef8da93f347fb498f13afe195e20521 Mon Sep 17 00:00:00 2001 From: Sash Date: Sun, 4 Oct 2026 16:31:52 +0100 Subject: [PATCH 15/52] fix: panic when Shift overflows the exponent (#437) * fix: panic when Shift overflows the exponent Shift added the exponents as int32, so shifting 5 * 10^2147483647 up by one came back as 5 * 10^-2147483648. Mul already panics when an exponent does not fit. * Address the Shift overflow review. The wrap comment was wrong, and the panic cases now use didPanic. --- decimal.go | 7 ++++++- decimal_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index 8f8f4a3..984ebd0 100644 --- a/decimal.go +++ b/decimal.go @@ -623,10 +623,15 @@ func (d Decimal) Mul(d2 Decimal) Decimal { // It shifts left when shift is positive and right if shift is negative. // In simpler terms, the given value for shift is added to the exponent // of the decimal. +// Shift panics if the resulting exponent does not fit in an int32. func (d Decimal) Shift(shift int32) Decimal { + exp := int64(d.exp) + int64(shift) + if exp > math.MaxInt32 || exp < math.MinInt32 { + panic(fmt.Sprintf("exponent %v overflows an int32!", exp)) + } return Decimal{ value: new(big.Int).Set(d.getValue()), - exp: d.exp + shift, + exp: int32(exp), } } diff --git a/decimal_test.go b/decimal_test.go index c5c05a1..d5ecc9d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -1924,6 +1924,21 @@ func TestDecimal_Shift(t *testing.T) { } } +func TestDecimal_ShiftExponentOverflow(t *testing.T) { + // The sum still fits, so the exponent is unchanged by the overflow check. + got := New(5, math.MaxInt32-1).Shift(1) + if !got.Equal(New(5, math.MaxInt32)) { + t.Fatalf("shift onto MaxInt32: exp %d", got.Exponent()) + } + + if !didPanic(func() { New(5, math.MaxInt32).Shift(1) }) { + t.Fatalf("should have gotten an overflow panic") + } + if !didPanic(func() { New(5, math.MinInt32).Shift(-1) }) { + t.Fatalf("should have gotten an overflow panic") + } +} + func TestDecimal_Div(t *testing.T) { type Inp struct { a string From e78fe0e8ac449cd462b6fa5203592bbf877e94ce Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 19:56:28 +0200 Subject: [PATCH 16/52] perf: use cached powers of ten in rescale, Floor, Ceil and Rat (#444) --- decimal.go | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/decimal.go b/decimal.go index 984ebd0..382c0ae 100644 --- a/decimal.go +++ b/decimal.go @@ -545,13 +545,11 @@ func (d Decimal) rescale(exp int32) Decimal { // NOTE(vadim): must convert exps to float64 before - to prevent overflow diff := math.Abs(float64(exp) - float64(d.exp)) - value := new(big.Int).Set(d.getValue()) - - expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil) + value := new(big.Int) if exp > d.exp { - value = value.Quo(value, expScale) - } else if exp < d.exp { - value = value.Mul(value, expScale) + value.Quo(d.getValue(), pow10(int64(diff))) + } else { + value.Mul(d.getValue(), pow10(int64(diff))) } return Decimal{ @@ -1823,12 +1821,10 @@ func (d Decimal) BigFloat() *big.Float { func (d Decimal) Rat() *big.Rat { if d.exp <= 0 { // NOTE(vadim): must negate after casting to prevent int32 overflow - denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil) - return new(big.Rat).SetFrac(d.getValue(), denom) + return new(big.Rat).SetFrac(d.getValue(), pow10(-int64(d.exp))) } - mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil) - num := new(big.Int).Mul(d.getValue(), mul) + num := new(big.Int).Mul(d.getValue(), pow10(int64(d.exp))) return new(big.Rat).SetFrac(num, oneInt) } @@ -2118,12 +2114,8 @@ func (d Decimal) Floor() Decimal { return d } - exp := big.NewInt(10) - // NOTE(vadim): must negate after casting to prevent int32 overflow - exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) - - z := new(big.Int).Div(d.getValue(), exp) + z := new(big.Int).Div(d.getValue(), pow10(-int64(d.exp))) return Decimal{value: z, exp: 0} } @@ -2133,12 +2125,8 @@ func (d Decimal) Ceil() Decimal { return d } - exp := big.NewInt(10) - // NOTE(vadim): must negate after casting to prevent int32 overflow - exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) - - z, m := new(big.Int).DivMod(d.getValue(), exp, new(big.Int)) + z, m := new(big.Int).DivMod(d.getValue(), pow10(-int64(d.exp)), new(big.Int)) if m.Cmp(zeroInt) != 0 { z.Add(z, oneInt) } From 8a6d7582fa7eef135fd8343c98470a98b80a7b68 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 21:06:15 +0200 Subject: [PATCH 17/52] perf: fewer allocations in Add, Sub, Cmp, Sum and Shift (#445) * perf: fewer allocations in Add, Sub, Cmp, Sum and Shift * fix: keep the zero representation of Shift and Sum results * fix: Shift of zero keeps the zero representation --- decimal.go | 80 ++++++++++++++++++++++++++++++++++++++++--------- decimal_test.go | 11 +++++++ 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/decimal.go b/decimal.go index 382c0ae..ab32c91 100644 --- a/decimal.go +++ b/decimal.go @@ -572,23 +572,41 @@ func (d Decimal) Abs() Decimal { // Add returns d + d2. func (d Decimal) Add(d2 Decimal) Decimal { - rd, rd2 := RescalePair(d, d2) + // rescale returns a new value, so the sum can be stored in it + if d.exp < d2.exp { + r := d2.rescale(d.exp) + r.value.Add(d.getValue(), r.value) + return r + } else if d.exp > d2.exp { + r := d.rescale(d2.exp) + r.value.Add(r.value, d2.getValue()) + return r + } - d3Value := new(big.Int).Add(rd.getValue(), rd2.getValue()) + d3Value := new(big.Int).Add(d.getValue(), d2.getValue()) return Decimal{ value: d3Value, - exp: rd.exp, + exp: d.exp, } } // Sub returns d - d2. func (d Decimal) Sub(d2 Decimal) Decimal { - rd, rd2 := RescalePair(d, d2) + // rescale returns a new value, so the difference can be stored in it + if d.exp < d2.exp { + r := d2.rescale(d.exp) + r.value.Sub(d.getValue(), r.value) + return r + } else if d.exp > d2.exp { + r := d.rescale(d2.exp) + r.value.Sub(r.value, d2.getValue()) + return r + } - d3Value := new(big.Int).Sub(rd.getValue(), rd2.getValue()) + d3Value := new(big.Int).Sub(d.getValue(), d2.getValue()) return Decimal{ value: d3Value, - exp: rd.exp, + exp: d.exp, } } @@ -627,8 +645,13 @@ func (d Decimal) Shift(shift int32) Decimal { if exp > math.MaxInt32 || exp < math.MinInt32 { panic(fmt.Sprintf("exponent %v overflows an int32!", exp)) } + value := d.getValue() + if value.Sign() == 0 { + // a copy of zero has a nil word slice, which reflect.DeepEqual tells from an empty one + value = zeroInt + } return Decimal{ - value: new(big.Int).Set(d.getValue()), + value: value, exp: int32(exp), } } @@ -1697,10 +1720,20 @@ func (d Decimal) Cmp(d2 Decimal) int { if d.exp == d2.exp { return d.getValue().Cmp(d2.getValue()) } + if s, s2 := d.Sign(), d2.Sign(); s != s2 { + if s > s2 { + return 1 + } + return -1 + } else if s == 0 { + return 0 + } - rd, rd2 := RescalePair(d, d2) - - return rd.getValue().Cmp(rd2.getValue()) + var scaled big.Int + if d.exp < d2.exp { + return d.getValue().Cmp(scaled.Mul(d2.getValue(), pow10(int64(d2.exp)-int64(d.exp)))) + } + return scaled.Mul(d.getValue(), pow10(int64(d.exp)-int64(d2.exp))).Cmp(d2.getValue()) } // Compare compares the numbers represented by d and d2 and returns: @@ -2422,12 +2455,31 @@ func Max(first Decimal, rest ...Decimal) Decimal { // Sum returns the combined total of the provided first and rest Decimals func Sum(first Decimal, rest ...Decimal) Decimal { - total := first - for _, item := range rest { - total = total.Add(item) + if len(rest) == 0 { + return first } - return total + // Add returns a new value, so the following items can be added to it in place + total := first.Add(rest[0]) + last := len(rest) - 1 + if last == 0 { + return total + } + var scaled big.Int + for _, item := range rest[1:last] { + switch { + case item.exp < total.exp: + total = total.rescale(item.exp) + total.value.Add(total.value, item.getValue()) + case item.exp > total.exp: + total.value.Add(total.value, scaled.Mul(item.getValue(), pow10(int64(item.exp)-int64(total.exp)))) + default: + total.value.Add(total.value, item.getValue()) + } + } + + // the last Add builds the result the same way as adding one item at a time does + return total.Add(rest[last]) } // Avg returns the average value of the provided first and rest Decimals diff --git a/decimal_test.go b/decimal_test.go index d5ecc9d..e2515a4 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3923,6 +3923,17 @@ func TestSum(t *testing.T) { if !sum.Equal(New(45, 0)) { t.Errorf("Failed to calculate sum, expected %s got %s", New(45, 0), sum) } + + // reflect.DeepEqual, used by testify's assert.Equal, also compares the big.Int internals + if s := Sum(New(5, -1), New(-5, -1), Zero); !reflect.DeepEqual(s, New(0, -1)) { + t.Errorf("Sum(0.5, -0.5, 0) = %#v, want the same representation as New(0, -1)", s) + } + if s := Zero.Shift(2); !reflect.DeepEqual(s, New(0, 2)) { + t.Errorf("Zero.Shift(2) = %#v, want the same representation as New(0, 2)", s) + } + if s := New(7, 0).Sub(New(7, 0)).Shift(2); !reflect.DeepEqual(s, New(0, 2)) { + t.Errorf("(7 - 7).Shift(2) = %#v, want the same representation as New(0, 2)", s) + } } func TestAvg(t *testing.T) { From 51fe3c95dc53c82f8f0515db00408f3be3d037c5 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 21:06:56 +0200 Subject: [PATCH 18/52] fix: keep the zero representation of rescale results (#451) --- decimal.go | 5 +++++ decimal_test.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/decimal.go b/decimal.go index ab32c91..975cb92 100644 --- a/decimal.go +++ b/decimal.go @@ -548,6 +548,11 @@ func (d Decimal) rescale(exp int32) Decimal { value := new(big.Int) if exp > d.exp { value.Quo(d.getValue(), pow10(int64(diff))) + if value.Sign() == 0 && d.getValue().Sign() != 0 { + // reflect.DeepEqual tells an empty word slice from nil, keep the empty one that + // dividing a copy of d.value in place used to leave + value.SetBits([]big.Word{}) + } } else { value.Mul(d.getValue(), pow10(int64(diff))) } diff --git a/decimal_test.go b/decimal_test.go index e2515a4..212e11a 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4346,6 +4346,21 @@ func TestTruncateNegativePrecision(t *testing.T) { } } +func TestRescaleToZeroRepresentation(t *testing.T) { + // reflect.DeepEqual, used by testify's assert.Equal, also compares the big.Int internals. + // Rounding a nonzero value to zero keeps the same form for any number of dropped digits. + want := New(7, 0).Sub(New(7, 0)) + for _, k := range []int32{1, 19, 20, 40} { + d := New(5, -k) + if got := d.RoundDown(0); !reflect.DeepEqual(got, want) { + t.Errorf("5e-%d RoundDown(0) = %#v, want the same representation as %#v", k, got, want) + } + if got := d.Truncate(0); !reflect.DeepEqual(got, want) { + t.Errorf("5e-%d Truncate(0) = %#v, want the same representation as %#v", k, got, want) + } + } +} + // TestRoundingExponentNormalization verifies that RoundUp, RoundDown, // RoundCeil, and RoundFloor return a result with the exponent normalized // to the requested number of places, even when the value is already exact. From f1d1c0012bd4aa882a035d95a1019c9859e8f0ae Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 4 Oct 2026 21:07:30 +0200 Subject: [PATCH 19/52] perf: int64 fast paths in NumDigits, IntPart, IsInteger and Float64 (#448) --- decimal.go | 69 +++++++++++++++++++++++++++++++++++-------------- decimal_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/decimal.go b/decimal.go index 975cb92..e193049 100644 --- a/decimal.go +++ b/decimal.go @@ -1338,6 +1338,28 @@ var pow10Table = func() []*big.Int { return t }() +// pow10Uint64 holds 10^0 ... 10^19. +var pow10Uint64 = func() (t [20]uint64) { + t[0] = 1 + for i := 1; i < len(t); i++ { + t[i] = t[i-1] * 10 + } + return t +}() + +// float64Pow10 holds 10^0 ... 10^22, the powers of ten that float64 represents exactly. +var float64Pow10 = [...]float64{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, + 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22} + +// pow5Int64 holds 5^0 ... 5^22. +var pow5Int64 = func() (t [23]int64) { + t[0] = 1 + for i := 1; i < len(t); i++ { + t[i] = t[i-1] * 5 + } + return t +}() + // pow10 returns 10^n for n >= 0, the result must not be modified. func pow10(n int64) *big.Int { if n < int64(len(pow10Table)) { @@ -1653,25 +1675,18 @@ func (d Decimal) Ln(precision int32) (Decimal, error) { func (d Decimal) NumDigits() int { v := d.getValue() if v.IsInt64() { - i64 := v.Int64() - if i64 == 0 { + u := uint64(v.Int64()) + if u == 0 { return 1 } - // Count digits via the underlying int64 instead of math.Log10, - // which rounds an exact 1eN like 1e15 down to 14.999... and would - // then under-report by one (see #420). Negation is safe because - // math.MinInt64 has IsInt64() true but its absolute value also - // needs counting; treat it explicitly. - if i64 == math.MinInt64 { - return 19 + if v.Sign() < 0 { + u = -u } - if i64 < 0 { - i64 = -i64 - } - n := 0 - for i64 > 0 { + // Not math.Log10, which rounds an exact 1eN like 1e15 down (see #420). + // bits.Len64(u)*1233>>12 is floor(log10(2^bits)), the digit count or one less. + n := bits.Len64(u) * 1233 >> 12 + if u >= pow10Uint64[n] { n++ - i64 /= 10 } return n } @@ -1679,10 +1694,7 @@ func (d Decimal) NumDigits() int { estimatedNumDigits := int(float64(v.BitLen()) / math.Log2(10)) // estimatedNumDigits (lg10) may be off by 1, need to verify - digitsBigInt := big.NewInt(int64(estimatedNumDigits)) - errorCorrectionUnit := digitsBigInt.Exp(tenInt, digitsBigInt, nil) - - if v.CmpAbs(errorCorrectionUnit) >= 0 { + if v.CmpAbs(pow10(int64(estimatedNumDigits))) >= 0 { return estimatedNumDigits + 1 } @@ -1695,10 +1707,17 @@ func (d Decimal) IsInteger() bool { if d.exp >= 0 { return true } + v := d.getValue() + if v.Sign() == 0 { + return true + } + if d.exp >= -18 && v.IsInt64() { + return v.Int64()%int64(pow10Uint64[-d.exp]) == 0 + } // When the exponent is negative we have to check every number after the decimal place // If all of them are zeroes, we are sure that given decimal can be represented as an integer var r big.Int - q := new(big.Int).Set(d.getValue()) + q := new(big.Int).Set(v) for z := abs(d.exp); z > 0; z-- { q.QuoRem(q, tenInt, &r) if r.Cmp(zeroInt) != 0 { @@ -1837,6 +1856,9 @@ func (d Decimal) CoefficientInt64() int64 { // IntPart returns the integer component of the decimal. func (d Decimal) IntPart() int64 { + if v := d.getValue(); d.exp <= 0 && d.exp >= -18 && v.IsInt64() { + return v.Int64() / int64(pow10Uint64[-d.exp]) + } scaledD := d.rescale(0) return scaledD.getValue().Int64() } @@ -1877,6 +1899,13 @@ func (d Decimal) Float64() (f float64, exact bool) { if sign == 0 { return 0, true } + // Both operands are exact here, so the division is correctly rounded like Rat().Float64(), + // and the result is exact when the 5^k part of 10^k divides the coefficient. + if v := d.getValue(); d.exp <= 0 && d.exp >= -22 && v.IsInt64() { + if i := v.Int64(); i >= -1<<53 && i <= 1<<53 { + return float64(i) / float64Pow10[-d.exp], i%pow5Int64[-d.exp] == 0 + } + } // |d| lies in [10^(magnitude-1), 10^magnitude). float64 spans roughly // 1e-324..1e308, so ±400 is safely outside it with margin to spare. magnitude := int64(d.NumDigits()) + int64(d.exp) diff --git a/decimal_test.go b/decimal_test.go index 212e11a..e896a7d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3494,6 +3494,45 @@ func TestDecimal_NumDigits(t *testing.T) { t.Errorf("expected %d digits for decimal %s, got %d", testCase.ExpectedNumDigits, testCase.Dec, nums) } } + + // every power of ten and of two in int64, and their neighbours + values := []int64{math.MaxInt64, math.MinInt64} + for p := int64(1); p <= math.MaxInt64/10; p *= 10 { + values = append(values, p-1, p, p+1, -p+1, -p, -p-1) + } + for j := uint(0); j < 63; j++ { + p := int64(1) << j + values = append(values, p-1, p, p+1, -p+1, -p, -p-1) + } + for _, v := range values { + want := len(strconv.FormatInt(v, 10)) + if v < 0 { + want-- + } + if got := New(v, 0).NumDigits(); got != want { + t.Errorf("expected %d digits for %d, got %d", want, v, got) + } + } +} + +func TestDecimal_Float64MatchesRat(t *testing.T) { + // coefficients and exponents in the range of the Float64 fast path + rng := rand.New(rand.NewSource(7)) + for i := 0; i < 100000; i++ { + v := rng.Int63n(1<<54+1) - 1<<53 + if i%3 == 0 { + v /= 1e10 // shorter coefficients, so more exact results + } + d := New(v, -int32(rng.Intn(23))) + f, exact := d.Float64() + wantF, wantExact := d.Rat().Float64() + if v == 0 { + wantF, wantExact = 0, true + } + if math.Float64bits(f) != math.Float64bits(wantF) || exact != wantExact { + t.Fatalf("%s.Float64() = %v, %v; want %v, %v", d, f, exact, wantF, wantExact) + } + } } func TestDecimal_Sign(t *testing.T) { From 71ebcfdcdf6d4a5e1751294ae667666273b03fe3 Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Mon, 5 Oct 2026 04:13:05 -0400 Subject: [PATCH 20/52] Fix the scale of the small-input exponential result (#433) --- decimal.go | 2 +- decimal_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index e193049..7255f50 100644 --- a/decimal.go +++ b/decimal.go @@ -1410,7 +1410,7 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { // Return 1 if abs(d) small enough; this also avoids later over/underflow overflowThreshold2 := New(9, -int32(currentPrecision)-1) if d.Abs().Cmp(overflowThreshold2) <= 0 { - return Decimal{oneInt, d.exp}, nil + return Decimal{oneInt, 0}, nil } // t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1 diff --git a/decimal_test.go b/decimal_test.go index e896a7d..7dc481e 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3191,6 +3191,15 @@ func TestDecimal_ExpHullAbrham(t *testing.T) { OverallPrecision uint32 ExpectedDec string }{ + {"0.000001", 4, "1"}, + {"-0.000001", 4, "1"}, + {"0.00000100", 4, "1"}, + {"-0.00000100", 4, "1"}, + {"0.00009", 4, "1"}, + {"0.000000000001", 8, "1"}, + {"-0.000000000001", 8, "1"}, + {"0.001", 4, "1.001"}, + {"-0.001", 4, "0.999"}, {"0", 1, "1"}, {"0.00", 5, "1"}, {"0.5", 5, "1.6487"}, From 84990cbfac3cfdada111489558da172cc40ce593 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 11:57:10 +0200 Subject: [PATCH 21/52] perf: faster String, MarshalJSON and NewFromString; reject a sign after the decimal point (#447) --- decimal.go | 100 ++++++++++++++++++++++++++++++++---------------- decimal_test.go | 34 ++++++++++++++++ 2 files changed, 101 insertions(+), 33 deletions(-) diff --git a/decimal.go b/decimal.go index 7255f50..78207ec 100644 --- a/decimal.go +++ b/decimal.go @@ -216,7 +216,6 @@ func NewFromBigRat(value *big.Rat, precision int32) Decimal { // d3, err := NewFromString("1.47000") func NewFromString(value string) (Decimal, error) { originalInput := value - var intString string var exp int64 // Check if number is using scientific notation and find dots @@ -251,29 +250,30 @@ func NewFromString(value string) (Decimal, error) { exp = expInt } - if pIndex == -1 { - // There is no decimal point, we can just parse the original string as - // an int - intString = value - } else { - if pIndex+1 < len(value) { - intString = value[:pIndex] + value[pIndex+1:] - } else { - intString = value[:pIndex] + numLen := len(value) + if pIndex != -1 { + if pIndex+1 < len(value) && (value[pIndex+1] == '-' || value[pIndex+1] == '+') { + // ParseInt and SetString would accept the sign once the point is removed, as in ".-5" + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) } + numLen-- expInt := -len(value[pIndex+1:]) exp += int64(expInt) } var dValue *big.Int - // strconv.ParseInt is faster than new(big.Int).SetString so this is just a shortcut for strings we know won't overflow - if len(intString) <= 18 { - parsed64, err := strconv.ParseInt(intString, 10, 64) - if err != nil { + // parsing in an int64 is faster than new(big.Int).SetString so this is just a shortcut for strings we know won't overflow + if numLen <= 18 { + parsed64, ok := parseInt64SkipIndex(value, pIndex) + if !ok { return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) } dValue = big.NewInt(parsed64) } else { + intString := value + if pIndex != -1 { + intString = value[:pIndex] + value[pIndex+1:] + } dValue = new(big.Int) _, ok := dValue.SetString(intString, 10) if !ok { @@ -295,6 +295,32 @@ func NewFromString(value string) (Decimal, error) { }, nil } +// parseInt64SkipIndex parses s without the byte at index skip as strconv.ParseInt(s, 10, 64) would, +// s must have at most 18 other bytes so that the result can't overflow. +func parseInt64SkipIndex(s string, skip int) (int64, bool) { + i, neg := 0, false + if len(s) > 0 && skip != 0 && (s[0] == '+' || s[0] == '-') { + i, neg = 1, s[0] == '-' + } + var n int64 + digits := 0 + for ; i < len(s); i++ { + if i == skip { + continue + } + c := s[i] + if c < '0' || c > '9' { + return 0, false + } + n = n*10 + int64(c-'0') + digits++ + } + if neg { + n = -n + } + return n, digits > 0 +} + // NewFromFormattedString returns a new Decimal from a formatted string representation. // The second argument - replRegexp, is a regular expression that is used to find characters that should be // removed from given decimal string representation. All matched characters will be replaced with an empty string. @@ -2234,13 +2260,14 @@ func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { // MarshalJSON implements the json.Marshaler interface. func (d Decimal) MarshalJSON() ([]byte, error) { - var str string + str := d.String() if MarshalJSONWithoutQuotes { - str = d.String() - } else { - str = "\"" + d.String() + "\"" + return []byte(str), nil } - return []byte(str), nil + b := make([]byte, 0, len(str)+2) + b = append(b, '"') + b = append(b, str...) + return append(b, '"'), nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation @@ -2378,7 +2405,7 @@ func (d Decimal) StringScaled(exp int32) string { func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { if d.exp == 0 { - return d.rescale(0).getValue().String() + return d.getValue().String() } if d.exp >= 0 { if useScientificNotation { @@ -2388,10 +2415,13 @@ func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { } } - abs := new(big.Int).Abs(d.getValue()) - str := abs.String() + str := d.getValue().String() + sign := "" + if str[0] == '-' { + sign, str = "-", str[1:] + } - var intPart, fractionalPart string + var intPart, leadingZeros, fractionalPart string // NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN // and you are on a 32-bit machine. Won't fix this super-edge case. @@ -2403,7 +2433,12 @@ func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { intPart = "0" num0s := -dExpInt - len(str) - fractionalPart = strings.Repeat("0", num0s) + str + if num0s <= len(zeros) { + leadingZeros = zeros[:num0s] + } else { + leadingZeros = strings.Repeat("0", num0s) + } + fractionalPart = str } if trimTrailingZeros { @@ -2414,20 +2449,19 @@ func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { } } fractionalPart = fractionalPart[:i+1] + if fractionalPart == "" { + leadingZeros = "" + } } - number := intPart - if len(fractionalPart) > 0 { - number += "." + fractionalPart + if len(leadingZeros)+len(fractionalPart) > 0 { + return sign + intPart + "." + leadingZeros + fractionalPart } - - if d.getValue().Sign() < 0 { - return "-" + number - } - - return number + return sign + intPart } +const zeros = "0000000000000000000000000000000000000000000000000000000000000000" + // ScientificNotationString serializes the decimal into standard scientific notation. // // The notation is normalized to have one non-zero digit followed by a decimal point and diff --git a/decimal_test.go b/decimal_test.go index 7dc481e..31f4893 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -394,6 +394,9 @@ func TestNewFromStringErrs(t *testing.T) { "51,850.00", "20_000_000.00", "$20_000_000.00", + ".-5", + ".+5", + ".-1234567890123456789012", } for _, s := range tests { @@ -405,6 +408,37 @@ func TestNewFromStringErrs(t *testing.T) { } } +func TestParseInt64SkipIndex(t *testing.T) { + // every string of up to 5 bytes from this alphabet, with the decimal point at each position + const alphabet = "059-+.a " + var check func(s string) + check = func(s string) { + for skip := -1; skip < len(s); skip++ { + if skip >= 0 && s[skip] != '.' { + continue + } + if skip == 0 && len(s) > 1 && (s[1] == '-' || s[1] == '+') { + continue // NewFromString rejects a sign after the point before parsing + } + withoutPoint := s + if skip >= 0 { + withoutPoint = s[:skip] + s[skip+1:] + } + want, err := strconv.ParseInt(withoutPoint, 10, 64) + got, ok := parseInt64SkipIndex(s, skip) + if ok != (err == nil) || got != want { + t.Fatalf("parseInt64SkipIndex(%q, %d) = %d, %v; ParseInt(%q) = %d, %v", s, skip, got, ok, withoutPoint, want, err) + } + } + if len(s) < 5 { + for i := range alphabet { + check(s + alphabet[i:i+1]) + } + } + } + check("") +} + func TestMaxDecodeExponent(t *testing.T) { limit := int32(MaxDecodeExponent) for _, c := range []struct { From cc8953c6b309c7031dd45e91891ced6fbb0581da Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 11:57:34 +0200 Subject: [PATCH 22/52] perf: build trig constants once (#449) --- decimal.go | 88 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/decimal.go b/decimal.go index 78207ec..646277d 100644 --- a/decimal.go +++ b/decimal.go @@ -2682,28 +2682,43 @@ func (d NullDecimal) EncodeSpanner() (interface{}, error) { // Trig functions +var ( + // Pi/4 split into three parts + pi4A = NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000 + pi4B = NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000 + pi4C = NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170 + m4PI = NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + atanP0 = NewFromFloat(-8.750608600031904122785e-01) + atanP1 = NewFromFloat(-1.615753718733365076637e+01) + atanP2 = NewFromFloat(-7.500855792314704667340e+01) + atanP3 = NewFromFloat(-1.228866684490136173410e+02) + atanP4 = NewFromFloat(-6.485021904942025371773e+01) + atanQ0 = NewFromFloat(2.485846490142306297962e+01) + atanQ1 = NewFromFloat(1.650270098316988542046e+02) + atanQ2 = NewFromFloat(4.328810604912902668951e+02) + atanQ3 = NewFromFloat(4.853903996359136964868e+02) + atanQ4 = NewFromFloat(1.945506571482613964425e+02) + + atanMorebits = NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits + tan3pio8 = NewFromFloat(2.41421356237309504880) // tan(3*pi/8) + piFloat = NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) +) + // Atan returns the arctangent, in radians, of x. func (d Decimal) Atan() Decimal { - if d.Equal(NewFromFloat(0.0)) { + if d.IsZero() { return d } - if d.GreaterThan(NewFromFloat(0.0)) { + if d.IsPositive() { return d.satan() } return d.Neg().satan().Neg() } func (d Decimal) xatan() Decimal { - P0 := NewFromFloat(-8.750608600031904122785e-01) - P1 := NewFromFloat(-1.615753718733365076637e+01) - P2 := NewFromFloat(-7.500855792314704667340e+01) - P3 := NewFromFloat(-1.228866684490136173410e+02) - P4 := NewFromFloat(-6.485021904942025371773e+01) - Q0 := NewFromFloat(2.485846490142306297962e+01) - Q1 := NewFromFloat(1.650270098316988542046e+02) - Q2 := NewFromFloat(4.328810604912902668951e+02) - Q3 := NewFromFloat(4.853903996359136964868e+02) - Q4 := NewFromFloat(1.945506571482613964425e+02) + P0, P1, P2, P3, P4 := atanP0, atanP1, atanP2, atanP3, atanP4 + Q0, Q1, Q2, Q3, Q4 := atanQ0, atanQ1, atanQ2, atanQ3, atanQ4 z := d.Mul(d) b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z) b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4) @@ -2715,17 +2730,15 @@ func (d Decimal) xatan() Decimal { // satan reduces its argument (known to be positive) // to the range [0, 0.66] and calls xatan. func (d Decimal) satan() Decimal { - Morebits := NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits - Tan3pio8 := NewFromFloat(2.41421356237309504880) // tan(3*pi/8) - pi := NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) + Morebits, Tan3pio8, pi := atanMorebits, tan3pio8, piFloat - if d.LessThanOrEqual(NewFromFloat(0.66)) { + if d.LessThanOrEqual(New(66, -2)) { return d.xatan() } if d.GreaterThan(Tan3pio8) { - return pi.Div(NewFromFloat(2.0)).Sub(NewFromFloat(1.0).Div(d).xatan()).Add(Morebits) + return pi.Div(New(2, 0)).Sub(New(1, 0).Div(d).xatan()).Add(Morebits) } - return pi.Div(NewFromFloat(4.0)).Add((d.Sub(NewFromFloat(1.0)).Div(d.Add(NewFromFloat(1.0)))).xatan()).Add(NewFromFloat(0.5).Mul(Morebits)) + return pi.Div(New(4, 0)).Add((d.Sub(New(1, 0)).Div(d.Add(New(1, 0)))).xatan()).Add(New(5, -1).Mul(Morebits)) } // sin coefficients @@ -2740,17 +2753,14 @@ var _sin = [...]Decimal{ // Sin returns the sine of the radian argument x. func (d Decimal) Sin() Decimal { - PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts - PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, - PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, - M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI - if d.Equal(NewFromFloat(0.0)) { + if d.IsZero() { return d } // make argument positive but save the sign sign := false - if d.LessThan(NewFromFloat(0.0)) { + if d.IsNegative() { d = d.Neg() sign = true } @@ -2761,7 +2771,7 @@ func (d Decimal) Sin() Decimal { // map zeros to origin if j&1 == 1 { j++ - y = y.Add(NewFromFloat(1.0)) + y = y.Add(New(1, 0)) } j &= 7 // octant modulo 2Pi radians (360 degrees) // reflect in x axis @@ -2774,7 +2784,7 @@ func (d Decimal) Sin() Decimal { if j == 1 || j == 2 { w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) - y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) } else { y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) } @@ -2797,14 +2807,11 @@ var _cos = [...]Decimal{ // Cos returns the cosine of the radian argument x. func (d Decimal) Cos() Decimal { - PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts - PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, - PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, - M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI // make argument positive sign := false - if d.LessThan(NewFromFloat(0.0)) { + if d.IsNegative() { d = d.Neg() } @@ -2814,7 +2821,7 @@ func (d Decimal) Cos() Decimal { // map zeros to origin if j&1 == 1 { j++ - y = y.Add(NewFromFloat(1.0)) + y = y.Add(New(1, 0)) } j &= 7 // octant modulo 2Pi radians (360 degrees) // reflect in x axis @@ -2833,7 +2840,7 @@ func (d Decimal) Cos() Decimal { y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) } else { w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) - y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) } if sign { y = y.Neg() @@ -2857,18 +2864,15 @@ var _tanQ = [...]Decimal{ // Tan returns the tangent of the radian argument x. func (d Decimal) Tan() Decimal { - PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts - PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, - PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, - M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI - if d.Equal(NewFromFloat(0.0)) { + if d.IsZero() { return d } // make argument positive but save the sign sign := false - if d.LessThan(NewFromFloat(0.0)) { + if d.IsNegative() { d = d.Neg() sign = true } @@ -2879,13 +2883,13 @@ func (d Decimal) Tan() Decimal { // map zeros to origin if j&1 == 1 { j++ - y = y.Add(NewFromFloat(1.0)) + y = y.Add(New(1, 0)) } z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic zz := z.Mul(z) - if zz.GreaterThan(NewFromFloat(1e-14)) { + if zz.GreaterThan(New(1, -14)) { w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2])) x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4]) y = z.Add(z.Mul(w.Div(x))) @@ -2893,7 +2897,7 @@ func (d Decimal) Tan() Decimal { y = z } if j&2 == 2 { - y = NewFromFloat(-1.0).Div(y) + y = New(-1, 0).Div(y) } if sign { y = y.Neg() From 9b6d1186ce89d33fe37ae8f2785d83a4966f9ec1 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 11:57:46 +0200 Subject: [PATCH 23/52] perf: raise the ExpHullAbrham sum to its power by squaring (#450) --- decimal.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/decimal.go b/decimal.go index 646277d..9fc2dc6 100644 --- a/decimal.go +++ b/decimal.go @@ -1471,11 +1471,13 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { sum = sum.Add(one) } + // res = sum^ki, the same value ki repeated multiplications would give ki := k.IntPart() - res := New(1, 0) - for i := ki; i > 0; i-- { - res = res.Mul(sum) + expInt64 := int64(sum.exp) * ki + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) } + res := Decimal{new(big.Int).Exp(sum.getValue(), big.NewInt(ki), nil), int32(expInt64)} resNumDigits := int32(res.NumDigits()) From 811854c508d5eaf855afb2267e364498a985b2d0 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 11:58:08 +0200 Subject: [PATCH 24/52] perf: faster QuoRem, DivRound, Round, RoundBank, RoundUp, RoundCeil and RoundFloor (#446) * perf: faster QuoRem, DivRound, Round, RoundBank, RoundUp, RoundCeil and RoundFloor * fix: keep the zero representation of RoundCeil and RoundFloor results --- decimal.go | 126 +++++++++++++++++++++++++----------------------- decimal_test.go | 14 ++++++ 2 files changed, 80 insertions(+), 60 deletions(-) diff --git a/decimal.go b/decimal.go index 9fc2dc6..b30331a 100644 --- a/decimal.go +++ b/decimal.go @@ -702,6 +702,11 @@ func (d Decimal) Div(d2 Decimal) Decimal { // // Note that precision<0 is allowed as input. func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { + return d.quoRem(d2, precision, nil) +} + +// quoRem is QuoRem that also sets bb, if not nil, to the divisor in the units of the remainder. +func (d Decimal) quoRem(d2 Decimal, precision int32, bb *big.Int) (Decimal, Decimal) { if d2.getValue().Sign() == 0 { panic("decimal division by 0") } @@ -710,29 +715,28 @@ func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { if e > math.MaxInt32 || e < math.MinInt32 { panic("overflow in decimal QuoRem") } - var aa, bb, expo big.Int + if bb == nil { + bb = new(big.Int) + } + var aa big.Int var scalerest int32 // d = a 10^ea // d2 = b 10^eb if e < 0 { aa = *d.getValue() - expo.SetInt64(-e) - bb.Exp(tenInt, &expo, nil) - bb.Mul(d2.getValue(), &bb) + bb.Mul(d2.getValue(), pow10(-e)) scalerest = d.exp // now aa = a // bb = b 10^(scale + eb - ea) } else { - expo.SetInt64(e) - aa.Exp(tenInt, &expo, nil) - aa.Mul(d.getValue(), &aa) - bb = *d2.getValue() + aa.Mul(d.getValue(), pow10(e)) + *bb = *d2.getValue() scalerest = scale + d2.exp // now aa = a ^ (ea - eb - scale) // bb = b } var q, r big.Int - q.QuoRem(&aa, &bb, &r) + q.QuoRem(&aa, bb, &r) dq := Decimal{value: &q, exp: scale} dr := Decimal{value: &r, exp: scalerest} return dq, dr @@ -746,27 +750,24 @@ func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { // // Note that precision<0 is allowed as input. func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { - // QuoRem already checks initialization - q, r := d.QuoRem(d2, precision) - // the actual rounding decision is based on comparing r*10^precision and d2/2 - // instead compare 2 r 10 ^precision and d2 + // quoRem already checks initialization + var bb big.Int + q, r := d.quoRem(d2, precision, &bb) + // the actual rounding decision is based on comparing r*10^precision and d2/2, + // which is comparing 2*|r.value| and |bb| var rv2 big.Int rv2.Abs(r.getValue()) rv2.Lsh(&rv2, 1) - // now rv2 = abs(r.value) * 2 - r2 := Decimal{value: &rv2, exp: r.exp + precision} - // r2 is now 2 * r * 10 ^ precision - var c = r2.Cmp(d2.Abs()) - - if c < 0 { + if rv2.CmpAbs(&bb) < 0 { return q } if d.getValue().Sign()*d2.getValue().Sign() < 0 { - return q.Sub(New(1, -precision)) + q.value.Sub(q.value, oneInt) + } else { + q.value.Add(q.value, oneInt) } - - return q.Add(New(1, -precision)) + return q } // Mod returns d % d2. @@ -2037,12 +2038,9 @@ func (d Decimal) Round(places int32) Decimal { ret.value.Add(ret.value, fiveInt) } - // floor for positive numbers, ceil for negative numbers - _, m := ret.value.DivMod(ret.value, tenInt, new(big.Int)) + // truncate towards zero + ret.value.Quo(ret.value, tenInt) ret.exp++ - if ret.value.Sign() < 0 && m.Cmp(zeroInt) != 0 { - ret.value.Add(ret.value, oneInt) - } return ret } @@ -2060,16 +2058,19 @@ func (d Decimal) RoundCeil(places int32) Decimal { return d.rescale(-places) } - rescaled := d.rescale(-places) - if d.Equal(rescaled) { - return rescaled + // q is d truncated to places, r is not 0 when digits were dropped and has the sign of d + var r big.Int + q := new(big.Int) + q.QuoRem(d.getValue(), pow10(-int64(places)-int64(d.exp)), &r) + if r.Sign() > 0 { + q.Add(q, oneInt) + } else if q.Sign() == 0 && r.Sign() != 0 { + // reflect.DeepEqual tells an empty word slice from nil, keep the empty one that + // rounding a nonzero value to zero used to leave + q.SetBits([]big.Word{}) } - if d.getValue().Sign() > 0 { - rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) - } - - return rescaled + return Decimal{value: q, exp: -places} } // RoundFloor rounds the decimal towards -infinity. @@ -2085,16 +2086,19 @@ func (d Decimal) RoundFloor(places int32) Decimal { return d.rescale(-places) } - rescaled := d.rescale(-places) - if d.Equal(rescaled) { - return rescaled + // q is d truncated to places, r is not 0 when digits were dropped and has the sign of d + var r big.Int + q := new(big.Int) + q.QuoRem(d.getValue(), pow10(-int64(places)-int64(d.exp)), &r) + if r.Sign() < 0 { + q.Sub(q, oneInt) + } else if q.Sign() == 0 && r.Sign() != 0 { + // reflect.DeepEqual tells an empty word slice from nil, keep the empty one that + // rounding a nonzero value to zero used to leave + q.SetBits([]big.Word{}) } - if d.getValue().Sign() < 0 { - rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) - } - - return rescaled + return Decimal{value: q, exp: -places} } // RoundUp rounds the decimal away from zero. @@ -2110,18 +2114,17 @@ func (d Decimal) RoundUp(places int32) Decimal { return d.rescale(-places) } - rescaled := d.rescale(-places) - if d.Equal(rescaled) { - return rescaled + // q is d truncated to places, r is not 0 when digits were dropped and has the sign of d + var r big.Int + q := new(big.Int) + q.QuoRem(d.getValue(), pow10(-int64(places)-int64(d.exp)), &r) + if r.Sign() > 0 { + q.Add(q, oneInt) + } else if r.Sign() < 0 { + q.Sub(q, oneInt) } - if d.getValue().Sign() > 0 { - rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) - } else if d.getValue().Sign() < 0 { - rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) - } - - return rescaled + return Decimal{value: q, exp: -places} } // RoundDown rounds the decimal towards zero. @@ -2153,14 +2156,17 @@ func (d Decimal) RoundDown(places int32) Decimal { func (d Decimal) RoundBank(places int32) Decimal { round := d.Round(places) - remainder := d.Sub(round).Abs() - half := New(5, -places-1) - if remainder.Cmp(half) == 0 && round.getValue().Bit(0) != 0 { - if round.getValue().Sign() < 0 { - round.value = new(big.Int).Add(round.getValue(), oneInt) - } else { - round.value = new(big.Int).Sub(round.getValue(), oneInt) + // it is a tie when twice the k dropped digits equal 10^k + if k := -int64(places) - int64(d.exp); k > 0 && round.getValue().Bit(0) != 0 { + var dropped big.Int + dropped.Rem(d.getValue(), pow10(k)) + if dropped.Abs(&dropped).Lsh(&dropped, 1).Cmp(pow10(k)) == 0 { + if round.getValue().Sign() < 0 { + round.value = new(big.Int).Add(round.getValue(), oneInt) + } else { + round.value = new(big.Int).Sub(round.getValue(), oneInt) + } } } diff --git a/decimal_test.go b/decimal_test.go index 31f4893..b209d1b 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4446,6 +4446,20 @@ func TestRescaleToZeroRepresentation(t *testing.T) { // TestRoundingExponentNormalization verifies that RoundUp, RoundDown, // RoundCeil, and RoundFloor return a result with the exponent normalized // to the requested number of places, even when the value is already exact. +func TestRoundCeilFloorToZeroRepresentation(t *testing.T) { + // reflect.DeepEqual, used by testify's assert.Equal, also compares the big.Int internals. + // Rounding a nonzero value to zero keeps the same form for any number of dropped digits. + want := New(7, 0).Sub(New(7, 0)) + for _, k := range []int32{1, 19, 20, 40} { + if got := New(-5, -k).RoundCeil(0); !reflect.DeepEqual(got, want) { + t.Errorf("-5e-%d RoundCeil(0) = %#v, want the same representation as %#v", k, got, want) + } + if got := New(5, -k).RoundFloor(0); !reflect.DeepEqual(got, want) { + t.Errorf("5e-%d RoundFloor(0) = %#v, want the same representation as %#v", k, got, want) + } + } +} + func TestRoundingExponentNormalization(t *testing.T) { type testCase struct { fn string From aed8543466e85124e50ef52648894a2d1b6c1cd7 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 11:58:31 +0200 Subject: [PATCH 25/52] fix: RoundCash with a low DivisionPrecision and NullDecimal.Valid after errors (#443) --- decimal.go | 18 ++++++++++-------- decimal_test.go | 27 +++++++++++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/decimal.go b/decimal.go index b30331a..fd17797 100644 --- a/decimal.go +++ b/decimal.go @@ -2205,8 +2205,7 @@ func (d Decimal) RoundCash(interval uint8) Decimal { value: iVal, } - // TODO: optimize those calculations to reduce the high allocations (~29 allocs). - return d.Mul(dVal).Round(0).Div(dVal).Truncate(2) + return d.Mul(dVal).Round(0).DivRound(dVal, 2) } // Floor returns the nearest integer value less than or equal to d. @@ -2605,8 +2604,9 @@ func (d *NullDecimal) Scan(value interface{}) error { d.Valid = false return nil } - d.Valid = true - return d.Decimal.Scan(value) + err := d.Decimal.Scan(value) + d.Valid = err == nil + return err } // Value implements the driver.Valuer interface for database serialization. @@ -2623,8 +2623,9 @@ func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { d.Valid = false return nil } - d.Valid = true - return d.Decimal.UnmarshalJSON(decimalBytes) + err := d.Decimal.UnmarshalJSON(decimalBytes) + d.Valid = err == nil + return err } // MarshalJSON implements the json.Marshaler interface. @@ -2675,9 +2676,10 @@ func (d *NullDecimal) DecodeSpanner(value interface{}) error { } value = *t } - d.Valid = true - return d.Decimal.Scan(value) + err := d.Decimal.Scan(value) + d.Valid = err == nil + return err } // EncodeSpanner encodes a Decimal into a Spanner value diff --git a/decimal_test.go b/decimal_test.go index b209d1b..bf82891 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -931,6 +931,17 @@ func TestNullDecimalBadJSON(t *testing.T) { if err == nil { t.Errorf("expected error, got %+v", doc) } + if doc.Amount.Valid { + t.Errorf("%s: expected Valid to be false after an error", testCase) + } + } + + var nd NullDecimal + if err := nd.Scan("nope"); err == nil || nd.Valid { + t.Errorf("Scan(\"nope\"): expected an error and Valid false, got %v and %v", err, nd.Valid) + } + if err := nd.DecodeSpanner("nope"); err == nil || nd.Valid { + t.Errorf("DecodeSpanner(\"nope\"): expected an error and Valid false, got %v and %v", err, nd.Valid) } } @@ -2308,13 +2319,17 @@ func TestDecimal_RoundCash(t *testing.T) { {"3.93", 100, "4.00"}, {"393", 100, "393"}, } - for i, test := range tests { - d, _ := NewFromString(test.d) - haveRounded := d.RoundCash(test.interval) - result, _ := NewFromString(test.result) + defer func(p int) { DivisionPrecision = p }(DivisionPrecision) + for _, precision := range []int{16, 0} { + DivisionPrecision = precision + for i, test := range tests { + d, _ := NewFromString(test.d) + haveRounded := d.RoundCash(test.interval) + result, _ := NewFromString(test.result) - if !haveRounded.Equal(result) { - t.Errorf("Index %d: Cash rounding for %q interval %d want %q, have %q", i, test.d, test.interval, test.result, haveRounded) + if !haveRounded.Equal(result) { + t.Errorf("Index %d, DivisionPrecision %d: Cash rounding for %q interval %d want %q, have %q", i, precision, test.d, test.interval, test.result, haveRounded) + } } } } From fc257b4a95d135982828135d99f17b7f6075c13f Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 13:05:34 +0200 Subject: [PATCH 26/52] chore: split decimal.go into math.go and encoding.go (#452) decimal.go had grown to about 2,900 lines. This moves declarations, byte for byte with their comments, into: - math.go: power, exponential, logarithm and trigonometric functions and their helpers (Pow, PowWithPrecision, PowInt32, PowBigInt, ExpHullAbrham, ExpTaylor, Ln, Sin, Cos, Tan, Atan) - encoding.go: JSON, text, binary, gob, SQL and Spanner methods, and NullDecimal - const.go: internal constants and lookup tables (zeroInt to twentyInt, pow10Table, pow10Uint64, float64Pow10, pow5Int64, pow10, zeros) Nothing else changes. The go doc -all output is the same. To follow the history of moved code: git blame -C git log -L :ExpTaylor:decimal.go aed8543 git show --color-moved=zebra --color-moved-ws=no --- const.go | 51 ++ decimal.go | 1432 --------------------------------------------------- encoding.go | 275 ++++++++++ math.go | 1121 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1447 insertions(+), 1432 deletions(-) create mode 100644 encoding.go create mode 100644 math.go diff --git a/const.go b/const.go index e5d6fa8..4538cab 100644 --- a/const.go +++ b/const.go @@ -1,6 +1,7 @@ package decimal import ( + "math/big" "strings" ) @@ -61,3 +62,53 @@ func (c constApproximation) withPrecision(precision int32) Decimal { return c.approximations[i] } + +var zeroInt = big.NewInt(0) +var oneInt = big.NewInt(1) +var twoInt = big.NewInt(2) +var fourInt = big.NewInt(4) +var fiveInt = big.NewInt(5) +var tenInt = big.NewInt(10) +var twentyInt = big.NewInt(20) + +// pow10Table holds 10^0 ... 10^127, its values must not be modified. +var pow10Table = func() []*big.Int { + t := make([]*big.Int, 128) + t[0] = big.NewInt(1) + for i := 1; i < len(t); i++ { + t[i] = new(big.Int).Mul(t[i-1], tenInt) + } + return t +}() + +// pow10Uint64 holds 10^0 ... 10^19. +var pow10Uint64 = func() (t [20]uint64) { + t[0] = 1 + for i := 1; i < len(t); i++ { + t[i] = t[i-1] * 10 + } + return t +}() + +// float64Pow10 holds 10^0 ... 10^22, the powers of ten that float64 represents exactly. +var float64Pow10 = [...]float64{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, + 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22} + +// pow5Int64 holds 5^0 ... 5^22. +var pow5Int64 = func() (t [23]int64) { + t[0] = 1 + for i := 1; i < len(t); i++ { + t[i] = t[i-1] * 5 + } + return t +}() + +// pow10 returns 10^n for n >= 0, the result must not be modified. +func pow10(n int64) *big.Int { + if n < int64(len(pow10Table)) { + return pow10Table[n] + } + return new(big.Int).Exp(tenInt, big.NewInt(n), nil) +} + +const zeros = "0000000000000000000000000000000000000000000000000000000000000000" diff --git a/decimal.go b/decimal.go index fd17797..4980cdd 100644 --- a/decimal.go +++ b/decimal.go @@ -17,8 +17,6 @@ package decimal import ( - "database/sql/driver" - "encoding/binary" "fmt" "math" "math/big" @@ -26,7 +24,6 @@ import ( "regexp" "strconv" "strings" - "sync" ) // DivisionPrecision is the number of decimal places in the result when it @@ -102,17 +99,6 @@ var MaxDecodeExponent = 10000 // Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. var Zero = Decimal{} -var zeroInt = big.NewInt(0) -var oneInt = big.NewInt(1) -var twoInt = big.NewInt(2) -var fourInt = big.NewInt(4) -var fiveInt = big.NewInt(5) -var tenInt = big.NewInt(10) -var twentyInt = big.NewInt(20) - -var factorials = []Decimal{New(1, 0)} -var factorialsMutex sync.RWMutex - // Decimal represents a fixed-point decimal. It is immutable. // number = value * 10 ^ exp type Decimal struct { @@ -776,930 +762,6 @@ func (d Decimal) Mod(d2 Decimal) Decimal { return r } -// Pow returns d to the power of d2. -// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is -// rounded half away from zero to PowPrecisionNegativeExponent places after the decimal point. A result -// that would round to 0 keeps PowPrecisionNegativeExponent significant digits instead (at least one). -// -// Pow returns 0 (zero-value of Decimal) instead of error for power operation edge cases, to handle those edge cases use PowWithPrecision. -// Edge cases not handled by Pow: -// - 0 ** 0 => undefined value -// - 0 ** y, where y < 0 => infinity -// - x ** y, where x < 0 and y is non-integer decimal => imaginary value -// - x ** y, where the magnitude of the result or the precision is too large to represent -// -// Example: -// -// d1 := decimal.NewFromFloat(4.0) -// d2 := decimal.NewFromFloat(4.0) -// res1 := d1.Pow(d2) -// res1.String() // output: "256" -// -// d3 := decimal.NewFromFloat(5.0) -// d4 := decimal.NewFromFloat(5.73) -// res2 := d3.Pow(d4) -// res2.String() // output: "10118.0803715950193171" -// -// d5 := decimal.NewFromInt(10) -// d6 := decimal.NewFromInt(-18) -// res3 := d5.Pow(d6) -// res3.String() // output: "0.000000000000000001" -func (d Decimal) Pow(d2 Decimal) Decimal { - res, err := d.PowWithPrecision(d2, int32(PowPrecisionNegativeExponent)) - if err != nil { - return Decimal{} - } - return res -} - -// PowWithPrecision returns d to the power of d2. -// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is -// rounded half away from zero to precision places after the decimal point. A result that would round -// to 0 keeps precision significant digits instead (at least one). -// -// PowWithPrecision returns error when: -// - 0 ** 0 => undefined value -// - 0 ** y, where y < 0 => infinity -// - x ** y, where x < 0 and y is non-integer decimal => imaginary value -// - x ** y, where the magnitude of the result or the precision is too large to represent -// -// Example: -// -// d1 := decimal.NewFromFloat(4.0) -// d2 := decimal.NewFromFloat(4.0) -// res1, err := d1.PowWithPrecision(d2, 2) -// res1.String() // output: "256" -// -// d3 := decimal.NewFromFloat(5.0) -// d4 := decimal.NewFromFloat(5.73) -// res2, err := d3.PowWithPrecision(d4, 5) -// res2.String() // output: "10118.08037" -// -// d5 := decimal.NewFromFloat(-3.0) -// d6 := decimal.NewFromFloat(-6.0) -// res3, err := d5.PowWithPrecision(d6, 10) -// res3.String() // output: "0.0013717421" -func (d Decimal) PowWithPrecision(d2 Decimal, precision int32) (Decimal, error) { - if d.IsZero() { - if d2.IsZero() { - return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") - } - if d2.Sign() < 0 { - return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") - } - return Decimal{zeroInt, 0}, nil - } - - if d2.IsInteger() { - return d.powBigIntWithPrecision(d2.BigInt(), precision) - } - - if d.Sign() < 0 { - return Decimal{}, fmt.Errorf("cannot represent imaginary value of x ** y, where x < 0 and y is non-integer decimal") - } - - return d.powFrac(d2, precision) -} - -// PowInt32 returns d to the power of exp, where exp is int32. -// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. -// -// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places -// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant -// digits instead (at least one). -// -// Example: -// -// d1, err := decimal.NewFromFloat(4.0).PowInt32(4) -// d1.String() // output: "256" -// -// d2, err := decimal.NewFromFloat(3.13).PowInt32(5) -// d2.String() // output: "300.4150512793" -func (d Decimal) PowInt32(exp int32) (Decimal, error) { - return d.PowBigInt(big.NewInt(int64(exp))) -} - -// PowBigInt returns d to the power of exp, where exp is big.Int. -// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. -// -// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places -// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant -// digits instead (at least one). -// -// Example: -// -// d1, err := decimal.NewFromFloat(3.0).PowBigInt(big.NewInt(3)) -// d1.String() // output: "27" -// -// d2, err := decimal.NewFromFloat(629.25).PowBigInt(big.NewInt(5)) -// d2.String() // output: "98654323103449.5673828125" -func (d Decimal) PowBigInt(exp *big.Int) (Decimal, error) { - return d.powBigIntWithPrecision(exp, int32(PowPrecisionNegativeExponent)) -} - -var errPowOutOfRange = fmt.Errorf("cannot represent result of power operation, its magnitude or precision is too large") - -// powExactBits is the largest estimated cost of computing d^n exactly for negative n. -// Bigger powers are approximated with truncated intermediates, which is cheaper. -const powExactBits = 3000 - -func (d Decimal) powBigIntWithPrecision(exp *big.Int, precision int32) (Decimal, error) { - switch { - case exp.Sign() > 0: - return d.powExact(exp) - case d.IsZero() && exp.Sign() == 0: - return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") - case exp.Sign() == 0: - return Decimal{oneInt, 0}, nil - case d.IsZero(): - return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") - } - return d.powNegInt(new(big.Int).Neg(exp), precision) -} - -// powExact returns d^n for n > 0. -func (d Decimal) powExact(n *big.Int) (Decimal, error) { - exp := new(big.Int).Mul(n, big.NewInt(int64(d.exp))) - if !exp.IsInt64() || exp.Int64() > math.MaxInt32 || exp.Int64() < math.MinInt32 { - return Decimal{}, errPowOutOfRange - } - return Decimal{new(big.Int).Exp(d.getValue(), n, nil), int32(exp.Int64())}, nil -} - -// powNegInt returns d^-n for n > 0, rounded as described in PowWithPrecision. -func (d Decimal) powNegInt(n *big.Int, precision int32) (Decimal, error) { - if d.absIsOne() { - // only the parity of n matters - n = big.NewInt(2 - int64(n.Bit(0))) - } - - // small powers are cheap to compute exactly, and |log10(d^-n)| <= cost keeps them in range - if n.IsInt64() && n.Int64() <= powExactBits { - cost := n.Int64() * (int64(d.getValue().BitLen()) + abs64(int64(d.exp))) - if cost <= powExactBits && powInRange(float64(cost), precision) { - return d.powNegIntExact(n, precision) - } - } - - nf, _ := new(big.Float).SetInt(n).Float64() - lg := -nf * d.log10Abs() - if !powInRange(lg, precision) { - return Decimal{}, errPowOutOfRange - } - - // the digit shifts of the truncated c^n must fit into int64 - if nd := int64(d.NumDigits()); n.IsInt64() && n.Int64() <= math.MaxInt64/(4*nd) { - c := new(big.Int).Abs(d.getValue()) - w := powSigDigits(lg, precision) + int64(len(n.String())) + 6 - // each try adds 20 digits, failing 5 times means an exact tie or a value extremely close to one - for i := 0; i < 5; i, w = i+1, w+20 { - if res, ok := powNegIntApprox(c, int64(d.exp), n, w, precision); ok { - if d.Sign() < 0 && n.Bit(0) == 1 { - res.value.Neg(res.value) - } - return res, nil - } - } - } - - // same as for non-integer exponents, which handles any n and exact ties - res, err := d.Abs().powFrac(Decimal{value: new(big.Int).Neg(n)}, precision) - if err == nil && d.Sign() < 0 && n.Bit(0) == 1 { - res.value.Neg(res.value) - } - return res, err -} - -// powNegIntExact returns d^-n for n > 0, dividing by the exact d^n. -func (d Decimal) powNegIntExact(n *big.Int, precision int32) (Decimal, error) { - x, err := d.powExact(n) - if err != nil { - return Decimal{}, err - } - res := New(1, 0).DivRound(x, precision) - if res.IsZero() { - // first significant digit of 1/x is at place NumDigits+exp, one earlier when x is a power of ten - nd := int32(x.NumDigits()) - places := int32(powMinSig(precision)) - 1 + nd + x.exp - if x.getValue().CmpAbs(pow10(int64(nd)-1)) == 0 { - places-- - } - res = New(1, 0).DivRound(x, places) - } - return res, nil -} - -// powNegIntApprox rounds |1 / (c^n * 10^(e*n))| as described in PowWithPrecision. c^n is approximated -// from below by m*10^s, truncating m to w digits after every multiplication. Each truncation shrinks -// the result by a factor of at most 1-10^(1-w), and the errors add up to less than 5n*10^(1-w), -// lo below allows ten times that. -func powNegIntApprox(c *big.Int, e int64, n *big.Int, w int64, precision int32) (Decimal, bool) { - cw := new(big.Int).Set(c) - cs, exact := truncDigits(cw, w) - - m, s := big.NewInt(1), int64(0) - for i := n.BitLen() - 1; i >= 0; i-- { - m.Mul(m, m) - ds, ex := truncDigits(m, w) - s, exact = 2*s+ds, exact && ex - if n.Bit(i) == 1 { - m.Mul(m, cw) - ds, ex = truncDigits(m, w) - s, exact = s+cs+ds, exact && ex - } - } - - // 10^scale / (m * 10^(s+e*n)) = 10^z / m - z := int64(Decimal{value: m}.NumDigits()) + w + 2 - scale := z + s + e*n.Int64() - q, rem := new(big.Int).QuoRem(pow10(z), m, new(big.Int)) - lo, hi := q, q - if !exact || rem.Sign() != 0 { - hi = new(big.Int).Add(q, oneInt) - } - if !exact { - lo = new(big.Int).Mul(q, big.NewInt(5)) - lo.Mul(lo, n).Quo(lo, pow10(w-2)) - lo.Sub(q, lo).Sub(lo, oneInt) - } - return roundBounds(lo, hi, scale, precision) -} - -// truncDigits truncates m > 0 to at least w digits, returning the number of dropped digits -// and whether they were all zeros. -func truncDigits(m *big.Int, w int64) (int64, bool) { - // m >= 2^(BitLen-1), so m has at least floor((BitLen-1)*log10(2))+1 digits - drop := int64(float64(m.BitLen()-1)*math.Log10(2)-1e-9) + 1 - w - if drop <= 0 { - return 0, true - } - var r big.Int - m.QuoRem(m, pow10(drop), &r) - return drop, r.Sign() == 0 -} - -// powFrac returns d^y for d > 0 and non-integer (or negative integer) y, rounded as described in -// PowWithPrecision. It evaluates d^y = 10^q * e^s, where y*ln(d) = q*ln(10) + s and 0 <= s < ln(10), -// in binary fixed-point arithmetic, adding working digits until the error bounds decide the rounding. -func (d Decimal) powFrac(y Decimal, precision int32) (Decimal, error) { - if d.absIsOne() { - // 1^y = 1, also for y beyond float64 range - if !powInRange(0, precision) { - return Decimal{}, errPowOutOfRange - } - k := int64(abs(precision)) + 2 - res, _ := roundBounds(pow10(k), pow10(k), k, precision) - return res, nil - } - - yf := y.InexactFloat64() - lg := yf * d.log10Abs() - if !powInRange(lg, precision) { - return Decimal{}, errPowOutOfRange - } - - k := int64(d.NumDigits()) - 1 + int64(d.exp) - // covers the working error amplified by |y| and |k|, see powFracFixed - extra := int64(math.Log10(math.Abs(yf)*(math.Abs(float64(k))+64)+math.Abs(lg)+1)) + 4 - // 9 guard digits make a retry unlikely, each retry adds 20 more - f := powSigDigits(lg, precision) + 9 - exactTie := false - for i := 0; ; i, f = i+1, f+20 { - if res, ok := powFracFixed(d, y, k, f, extra, precision, exactTie); ok { - return res, nil - } - // only a terminating result can sit exactly on a rounding boundary, which no working - // precision resolves, then the upper bound rounds it away from zero - if i == 8 { - exactTie = powIsTerminating(d, y) - } - } -} - -// powIsTerminating reports whether d^y is a terminating decimal, for d > 0. -func powIsTerminating(d, y Decimal) bool { - p, q := powRatio(y) - num, den := powRatio(d) - if !q.IsInt64() { - return false - } - // d^(p/q) is rational only when num and den are perfect q-th powers - rn, ok := iroot(num, q.Int64()) - if !ok { - return false - } - rd, ok := iroot(den, q.Int64()) - if !ok { - return false - } - // (rn/rd)^p terminates when its denominator has no prime factors other than 2 and 5 - if p.Sign() < 0 { - rd = rn - } - rd = new(big.Int).Set(rd) - for _, f := range []*big.Int{twoInt, fiveInt} { - for r := new(big.Int); rd.Cmp(oneInt) > 0 && r.Rem(rd, f).Sign() == 0; { - rd.Quo(rd, f) - } - } - return rd.Cmp(oneInt) == 0 -} - -// powRatio returns d as a fraction p/q in lowest terms, q > 0. -func powRatio(d Decimal) (*big.Int, *big.Int) { - if d.exp >= 0 { - return new(big.Int).Mul(d.getValue(), pow10(int64(d.exp))), big.NewInt(1) - } - p, q := new(big.Int).Set(d.getValue()), new(big.Int).Set(pow10(-int64(d.exp))) - g := new(big.Int).GCD(nil, nil, new(big.Int).Abs(p), q) - return p.Quo(p, g), q.Quo(q, g) -} - -// iroot returns the integer k-th root of x >= 1 and whether it is exact. -func iroot(x *big.Int, k int64) (*big.Int, bool) { - if k == 1 || x.Cmp(oneInt) == 0 { - return x, true - } - if int64(x.BitLen()) <= k { - // 1 < x < 2^k has no integer k-th root - return nil, false - } - // Newton's method from above converges to the floor of the root - kb, km1 := big.NewInt(k), big.NewInt(k-1) - r := new(big.Int).Lsh(oneInt, uint((int64(x.BitLen())+k-1)/k)) - t, u := new(big.Int), new(big.Int) - for { - t.Quo(x, t.Exp(r, km1, nil)) - t.Add(t, u.Mul(r, km1)).Quo(t, kb) - if t.Cmp(r) >= 0 { - break - } - r.Set(t) - } - return r, t.Exp(r, kb, nil).Cmp(x) == 0 -} - -// powFracFixed computes d^y (see powFrac) as an interval of e^s * 10^f with f+extra working digits -// and rounds it. With force the upper bound is used as the exact value. -func powFracFixed(d, y Decimal, k, f, extra int64, precision int32, force bool) (Decimal, bool) { - b := uint(float64(f+extra)*math.Log2(10)) + 1 - - // d = m * 10^k, 1 <= m < 10 - m := new(big.Int).Lsh(d.getValue(), b) - m.Quo(m, pow10(int64(d.NumDigits())-1)) - t, lnErr := lnFixed(m, b) - ln10, ln10Err := ln10Fixed(b) - t.Add(t, new(big.Int).Mul(big.NewInt(k), ln10)) - tErr := big.NewInt(lnErr + 1 + abs64(k)*ln10Err) - - // t = y * ln(d) - t.Mul(t, y.getValue()) - if y.exp >= 0 { - t.Mul(t, pow10(int64(y.exp))) - } else { - t.Quo(t, pow10(-int64(y.exp))) - } - tErr.Mul(tErr, y.Abs().Ceil().getValue()).Add(tErr, oneInt) - - q, s := new(big.Int).DivMod(t, ln10, new(big.Int)) - sErr := new(big.Int).Abs(q) - sErr.Mul(sErr, big.NewInt(ln10Err)).Add(sErr, tErr) - - // e^s < 11, so an error in s grows at most 11 times - r, rErr := expFixed(s, b) - sErr.Mul(sErr, big.NewInt(11)).Add(sErr, big.NewInt(rErr+1)) - - // v * 10^(f-q) = e^s * 10^f - p := pow10(f) - hi := new(big.Int).Add(r, sErr) - hi.Mul(hi, p).Rsh(hi, b).Add(hi, oneInt) - lo := hi - if !force { - lo = new(big.Int).Sub(r, sErr) - lo.Mul(lo, p).Rsh(lo, b) - if lo.Sign() < 0 { - lo.SetInt64(0) - } - } - return roundBounds(lo, hi, f-q.Int64(), precision) -} - -// lnFixed returns ln(m / 2^b) * 2^b for m / 2^b in [1, 10], with its error bound in ulps. -func lnFixed(m *big.Int, b uint) (*big.Int, int64) { - // a = math.Log(m) is accurate to ~1e-15, so ln(m) = a + ln(1+z), where z = m*e^-a - 1 is tiny - // and the series ln(1+z) = z - z^2/2 + z^3/3 - ... gains ~50 bits per term. - top, sh := new(big.Int), m.BitLen()-60 - if sh > 0 { - top.Rsh(m, uint(sh)) - } else { - top.Lsh(m, uint(-sh)) - } - a := big.NewInt(int64(math.Ldexp(math.Log(math.Ldexp(float64(top.Uint64()), sh-int(b))), 52))) - if b >= 52 { - a.Lsh(a, b-52) - } else { - a.Rsh(a, 52-b) - } - - e, eErr := expFixed(new(big.Int).Neg(a), b) - z := e.Mul(e, m).Rsh(e, b) - z.Sub(z, new(big.Int).Lsh(oneInt, b)) - - sum, p, term, iv := new(big.Int).Set(z), new(big.Int).Set(z), new(big.Int), new(big.Int) - terms := int64(1) - for i := int64(2); ; i++ { - p.Mul(p, z).Rsh(p, b) - if p.Sign() == 0 { - break - } - term.Quo(p, iv.SetInt64(i)) - if i%2 == 0 { - sum.Sub(sum, term) - } else { - sum.Add(sum, term) - } - terms++ - } - // m <= 10 amplifies the error of e^-a at most 10 times - return sum.Add(sum, a), 10*eErr + 2*terms + 3 -} - -// expFixed returns e^(x / 2^b) * 2^b for |x / 2^b| <= 2.4, with its error bound in ulps. -func expFixed(x *big.Int, b uint) (*big.Int, int64) { - // e^x = (e^(x/2^j))^(2^j): Taylor series of the reduced argument, then j squarings. - // Guard bits cover the error of up to ~g Taylor terms, amplified up to 11*2^j times by squaring. - j := uint(math.Sqrt(float64(b))) + 1 - g := b + j + uint(bits.Len(33*(b+j+64)+363)) + 2 - - xr := new(big.Int).Lsh(x, g-b) - xr.Rsh(xr, j) - sum, term, iv := new(big.Int).Lsh(oneInt, g), new(big.Int).Lsh(oneInt, g), new(big.Int) - for i := int64(1); ; i++ { - term.Mul(term, xr).Rsh(term, g) - term.Quo(term, iv.SetInt64(i)) - if term.Sign() == 0 { - break - } - sum.Add(sum, term) - } - for ; j > 0; j-- { - sum.Mul(sum, sum).Rsh(sum, g) - } - return sum.Rsh(sum, g-b), 2 -} - -var ln10Cache struct { - once sync.Once - v *big.Int -} - -const ln10CacheBits = 2048 - -// ln10Fixed returns ln(10) * 2^b, with its error bound in ulps. -func ln10Fixed(b uint) (*big.Int, int64) { - if b <= ln10CacheBits { - ln10Cache.once.Do(func() { ln10Cache.v, _ = ln10FromDigits(ln10CacheBits) }) - return new(big.Int).Rsh(ln10Cache.v, ln10CacheBits-b), 3 - } - if v, ok := ln10FromDigits(b); ok { - return v, 2 - } - return lnFixed(new(big.Int).Lsh(big.NewInt(10), b), b) -} - -// ln10FromDigits returns ln(10) * 2^b computed from strLn10, if it has enough digits. -func ln10FromDigits(b uint) (*big.Int, bool) { - digits := int(float64(b)*math.Log10(2)) + 3 - if digits+2 > len(strLn10) { - return nil, false - } - v, _ := new(big.Int).SetString(strLn10[:1]+strLn10[2:digits+2], 10) - v.Lsh(v, b) - return v.Quo(v, pow10(int64(digits))), true -} - -// roundBounds rounds v, known only by lo <= v*10^scale <= hi (lo >= 0), half away from zero to -// precision places after the decimal point, or to max(precision, 1) significant digits when that -// would be 0. It reports false when lo and hi do not round to the same value. -func roundBounds(lo, hi *big.Int, scale int64, precision int32) (Decimal, bool) { - g := scale - int64(precision) - if g < 1 { - return Decimal{}, false - } - dhi := int64(Decimal{value: hi}.NumDigits()) - // with fewer than g digits hi rounds to 0, skip building 10^g for tiny results - if g <= dhi { - if q := roundHalfUp(hi, g); q.Sign() != 0 { - return Decimal{q, -precision}, q.Cmp(roundHalfUp(lo, g)) == 0 - } - } - - sig := powMinSig(precision) - g = dhi - sig - if g < 2 { - return Decimal{}, false - } - q := roundHalfUp(hi, g) - res := Decimal{q, int32(g - scale)} - if int64(Decimal{value: lo}.NumDigits()) == dhi { - return res, q.Cmp(roundHalfUp(lo, g)) == 0 - } - // lo < 10^(dhi-1) <= hi, decided only when both round to that power of ten - return res, q.Cmp(pow10(sig-1)) == 0 && roundHalfUp(lo, g-1).Cmp(pow10(sig)) == 0 -} - -// roundHalfUp returns x / 10^g rounded half away from zero, for x >= 0 and g >= 1. -func roundHalfUp(x *big.Int, g int64) *big.Int { - p := pow10(g) - q, r := new(big.Int).QuoRem(x, p, new(big.Int)) - if r.Lsh(r, 1).Cmp(p) >= 0 { - q.Add(q, oneInt) - } - return q -} - -// powSigDigits returns the number of significant digits of a result of magnitude 10^lg rounded to -// precision places, at least powMinSig(precision). -func powSigDigits(lg float64, precision int32) int64 { - if n := int64(math.Floor(lg)) + 1 + int64(precision); n > powMinSig(precision) { - return n - } - return powMinSig(precision) -} - -// powMinSig returns the significant digits kept by results that would round to zero. -func powMinSig(precision int32) int64 { - if precision < 1 { - return 1 - } - return int64(precision) -} - -// powInRange reports whether a result of magnitude 10^lg rounded to precision places has an exponent -// that fits into int32. -func powInRange(lg float64, precision int32) bool { - return math.Abs(lg)+math.Abs(float64(precision)) < math.MaxInt32/2 -} - -// absIsOne reports whether |d| = 1. -func (d Decimal) absIsOne() bool { - nd := int64(d.NumDigits()) - return nd-1+int64(d.exp) == 0 && d.getValue().CmpAbs(pow10(nd-1)) == 0 -} - -// log10Abs approximates log10(|d|) for d != 0. -func (d Decimal) log10Abs() float64 { - var m big.Float - e2 := new(big.Float).SetInt(d.getValue()).MantExp(&m) - mf, _ := m.Float64() - return math.Log10(math.Abs(mf)) + float64(e2)*math.Log10(2) + float64(d.exp) -} - -// pow10Table holds 10^0 ... 10^127, its values must not be modified. -var pow10Table = func() []*big.Int { - t := make([]*big.Int, 128) - t[0] = big.NewInt(1) - for i := 1; i < len(t); i++ { - t[i] = new(big.Int).Mul(t[i-1], tenInt) - } - return t -}() - -// pow10Uint64 holds 10^0 ... 10^19. -var pow10Uint64 = func() (t [20]uint64) { - t[0] = 1 - for i := 1; i < len(t); i++ { - t[i] = t[i-1] * 10 - } - return t -}() - -// float64Pow10 holds 10^0 ... 10^22, the powers of ten that float64 represents exactly. -var float64Pow10 = [...]float64{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, - 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22} - -// pow5Int64 holds 5^0 ... 5^22. -var pow5Int64 = func() (t [23]int64) { - t[0] = 1 - for i := 1; i < len(t); i++ { - t[i] = t[i-1] * 5 - } - return t -}() - -// pow10 returns 10^n for n >= 0, the result must not be modified. -func pow10(n int64) *big.Int { - if n < int64(len(pow10Table)) { - return pow10Table[n] - } - return new(big.Int).Exp(tenInt, big.NewInt(n), nil) -} - -func abs64(n int64) int64 { - if n < 0 { - return -n - } - return n -} - -// ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. -// OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). -// -// ExpHullAbrham is faster than ExpTaylor for small precision values, but it is much slower for large precision values. -// -// Example: -// -// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000" -// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284" -func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { - // Algorithm based on Variable precision exponential function. - // ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham. - if d.IsZero() { - return Decimal{oneInt, 0}, nil - } - - currentPrecision := overallPrecision - - // Algorithm does not work if currentPrecision * 23 < |x|. - // Precision is automatically increased in such cases, so the value can be calculated precisely. - // If newly calculated precision is higher than ExpMaxIterations the currentPrecision will not be changed. - f := d.Abs().InexactFloat64() - if ncp := f / 23; ncp > float64(currentPrecision) && ncp < float64(ExpMaxIterations) { - currentPrecision = uint32(math.Ceil(ncp)) - } - - // fail if abs(d) beyond an over/underflow threshold - overflowThreshold := New(23*int64(currentPrecision), 0) - if d.Abs().Cmp(overflowThreshold) > 0 { - return Decimal{}, fmt.Errorf("over/underflow threshold, exp(x) cannot be calculated precisely") - } - - // Return 1 if abs(d) small enough; this also avoids later over/underflow - overflowThreshold2 := New(9, -int32(currentPrecision)-1) - if d.Abs().Cmp(overflowThreshold2) <= 0 { - return Decimal{oneInt, 0}, nil - } - - // t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1 - t := d.exp + int32(d.NumDigits()) // Add d.NumDigits because the paper assumes that d.value [0.1, 1) - - if t < 0 { - t = 0 - } - - k := New(1, t) // reduction factor - r := Decimal{new(big.Int).Set(d.getValue()), d.exp - t} // reduced argument - p := int32(currentPrecision) + t + 2 // precision for calculating the sum - - // Determine n, the number of therms for calculating sum - // use first Newton step (1.435p - 1.182) / log10(p/abs(r)) - // for solving appropriate equation, along with directed - // roundings and simple rational bound for log10(p/abs(r)) - rf := r.Abs().InexactFloat64() - pf := float64(p) - nf := math.Ceil((1.453*pf - 1.182) / math.Log10(pf/rf)) - if nf > float64(ExpMaxIterations) || math.IsNaN(nf) { - return Decimal{}, fmt.Errorf("exact value cannot be calculated in <=ExpMaxIterations iterations") - } - n := int64(nf) - - tmp := New(0, 0) - sum := New(1, 0) - one := New(1, 0) - for i := n - 1; i > 0; i-- { - tmp.value.SetInt64(i) - sum = sum.Mul(r.DivRound(tmp, p)) - sum = sum.Add(one) - } - - // res = sum^ki, the same value ki repeated multiplications would give - ki := k.IntPart() - expInt64 := int64(sum.exp) * ki - if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { - panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) - } - res := Decimal{new(big.Int).Exp(sum.getValue(), big.NewInt(ki), nil), int32(expInt64)} - - resNumDigits := int32(res.NumDigits()) - - var roundDigits int32 - if resNumDigits > abs(res.exp) { - roundDigits = int32(currentPrecision) - resNumDigits - res.exp - } else { - roundDigits = int32(currentPrecision) - } - - res = res.Round(roundDigits) - - return res, nil -} - -// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion. -// Precision argument specifies how precise the result must be (number of digits after decimal point). -// Negative precision is allowed. -// -// ExpTaylor is much faster for large precision values than ExpHullAbrham. -// -// Example: -// -// d, err := NewFromFloat(26.1).ExpTaylor(2).String() -// d.String() // output: "216314672147.06" -// -// NewFromFloat(26.1).ExpTaylor(20).String() -// d.String() // output: "216314672147.05767284062928674083" -// -// NewFromFloat(26.1).ExpTaylor(-10).String() -// d.String() // output: "220000000000" -func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { - // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only - if d.IsZero() { - return Decimal{oneInt, 0}.Round(precision), nil - } - - var epsilon Decimal - var divPrecision int32 - if precision < 0 { - epsilon = New(1, -1) - divPrecision = 8 - } else { - epsilon = New(1, -precision-1) - divPrecision = precision + 1 - } - - decAbs := d.Abs() - pow := d.Abs() - factorial := New(1, 0) - - result := New(1, 0) - - for i := int64(1); ; { - step := pow.DivRound(factorial, divPrecision) - result = result.Add(step) - - // Stop Taylor series when current step is smaller than epsilon - if step.Cmp(epsilon) < 0 { - break - } - - pow = pow.Mul(decAbs) - - i++ - - // Calculate next factorial number or retrieve cached value - factorialsMutex.RLock() - if len(factorials) >= int(i) && !factorials[i-1].IsZero() { - factorial = factorials[i-1] - factorialsMutex.RUnlock() - } else { - prevFactorial := factorials[i-2] - factorialsMutex.RUnlock() - factorial = prevFactorial.Mul(New(i, 0)) - factorialsMutex.Lock() - // Check again in case another goroutine already added it. - if len(factorials) < int(i) || factorials[i-1].IsZero() { - factorials = append(factorials, Zero) - factorials[i-1] = factorial - } - factorialsMutex.Unlock() - } - } - - if d.Sign() < 0 { - result = New(1, 0).DivRound(result, precision+1) - } - - result = result.Round(precision) - return result, nil -} - -// Ln calculates natural logarithm of d. -// Precision argument specifies how precise the result must be (number of digits after decimal point). -// Negative precision is allowed. -// -// Example: -// -// d1, err := NewFromFloat(13.3).Ln(2) -// d1.String() // output: "2.59" -// -// d2, err := NewFromFloat(579.161).Ln(10) -// d2.String() // output: "6.3615805046" -func (d Decimal) Ln(precision int32) (Decimal, error) { - // Algorithm based on The Use of Iteration Methods for Approximating the Natural Logarithm, - // James F. Epperson, The American Mathematical Monthly, Vol. 96, No. 9, November 1989, pp. 831-835. - if d.IsNegative() { - return Decimal{}, fmt.Errorf("cannot calculate natural logarithm for negative decimals") - } - - if d.IsZero() { - return Decimal{}, fmt.Errorf("cannot represent natural logarithm of 0, result: -infinity") - } - - calcPrecision := precision + 2 - z := d.Copy() - - var comp1, comp3, comp2, comp4, reduceAdjust Decimal - comp1 = z.Sub(Decimal{oneInt, 0}) - comp3 = Decimal{oneInt, -1} - - // for decimal in range [0.9, 1.1] where ln(d) is close to 0 - usePowerSeries := false - - if comp1.Abs().Cmp(comp3) <= 0 { - usePowerSeries = true - } else { - // reduce input decimal to range [0.1, 1) - expDelta := int32(z.NumDigits()) + z.exp - z.exp -= expDelta - - // Input decimal was reduced by factor of 10^expDelta, thus we will need to add - // ln(10^expDelta) = expDelta * ln(10) - // to the result to compensate that - ln10 := ln10.withPrecision(calcPrecision) - reduceAdjust = NewFromInt32(expDelta) - reduceAdjust = reduceAdjust.Mul(ln10) - - comp1 = z.Sub(Decimal{oneInt, 0}) - - if comp1.Abs().Cmp(comp3) <= 0 { - usePowerSeries = true - } else { - // initial estimate using floats - zFloat := z.InexactFloat64() - comp1 = NewFromFloat(math.Log(zFloat)) - } - } - - epsilon := Decimal{oneInt, -calcPrecision} - - if usePowerSeries { - // Power Series - https://en.wikipedia.org/wiki/Logarithm#Power_series - // Calculating n-th term of formula: ln(z+1) = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] - // until the difference between current and next term is smaller than epsilon. - // Coverage quite fast for decimals close to 1.0 - - // z + 2 - comp2 = comp1.Add(Decimal{twoInt, 0}) - // z / (z + 2) - comp3 = comp1.DivRound(comp2, calcPrecision) - // 2 * (z / (z + 2)) - comp1 = comp3.Add(comp3) - comp2 = comp1.Copy() - - for n := 1; ; n++ { - // 2 * (z / (z+2))^(2n+1) - comp2 = comp2.Mul(comp3).Mul(comp3) - - // 1 / (2n+1) * 2 * (z / (z+2))^(2n+1) - comp4 = NewFromInt(int64(2*n + 1)) - comp4 = comp2.DivRound(comp4, calcPrecision) - - // comp1 = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] - comp1 = comp1.Add(comp4) - - if comp4.Abs().Cmp(epsilon) <= 0 { - break - } - } - } else { - // Halley's Iteration. - // Calculating n-th term of formula: a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z), - // until the difference between current and next term is smaller than epsilon - var prevStep Decimal - maxIters := calcPrecision*2 + 10 - - for i := int32(0); i < maxIters; i++ { - // exp(a_n) - comp3, _ = comp1.ExpTaylor(calcPrecision) - // exp(a_n) - z - comp2 = comp3.Sub(z) - // 2 * (exp(a_n) - z) - comp2 = comp2.Add(comp2) - // exp(a_n) + z - comp4 = comp3.Add(z) - // 2 * (exp(a_n) - z) / (exp(a_n) + z) - comp3 = comp2.DivRound(comp4, calcPrecision) - // comp1 = a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z) - comp1 = comp1.Sub(comp3) - - if prevStep.Add(comp3).IsZero() { - // If iteration steps oscillate we should return early and prevent an infinity loop - // NOTE(mwoss): This should be quite a rare case, returning error is not necessary - break - } - - if comp3.Abs().Cmp(epsilon) <= 0 { - break - } - - prevStep = comp3 - } - } - - comp1 = comp1.Add(reduceAdjust) - - return comp1.Round(precision), nil -} - // NumDigits returns the number of digits of the decimal coefficient (d.Value) func (d Decimal) NumDigits() int { v := d.getValue() @@ -2251,158 +1313,6 @@ func (d Decimal) Truncate(precision int32) Decimal { return d } -// UnmarshalJSON implements the json.Unmarshaler interface. -func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { - if string(decimalBytes) == "null" { - return nil - } - - decimal, err := NewFromString(unquoteIfQuoted(string(decimalBytes))) - *d = decimal - if err != nil { - return fmt.Errorf("error decoding string '%s': %s", string(decimalBytes), err) - } - return nil -} - -// MarshalJSON implements the json.Marshaler interface. -func (d Decimal) MarshalJSON() ([]byte, error) { - str := d.String() - if MarshalJSONWithoutQuotes { - return []byte(str), nil - } - b := make([]byte, 0, len(str)+2) - b = append(b, '"') - b = append(b, str...) - return append(b, '"'), nil -} - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation -// is already used when encoding to text, this method stores that string as []byte -func (d *Decimal) UnmarshalBinary(data []byte) error { - // Verify we have at least 4 bytes for the exponent. The GOB encoded value - // may be empty. - if len(data) < 4 { - return fmt.Errorf("error decoding binary %v: expected at least 4 bytes, got %d", data, len(data)) - } - - // Extract the exponent - exp := int32(binary.BigEndian.Uint32(data[:4])) - if int64(exp) > int64(MaxDecodeExponent) || int64(exp) < -int64(MaxDecodeExponent) { - return fmt.Errorf("error decoding binary: exponent %d exceeds MaxDecodeExponent (%d)", exp, MaxDecodeExponent) - } - d.exp = exp - - // Extract the value - d.value = new(big.Int) - if err := d.value.GobDecode(data[4:]); err != nil { - return fmt.Errorf("error decoding binary %v: %s", data, err) - } - - return nil -} - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -func (d Decimal) MarshalBinary() (data []byte, err error) { - // exp is written first, but encode value first to know output size - var valueData []byte - if valueData, err = d.getValue().GobEncode(); err != nil { - return nil, err - } - - // Write the exponent in front, since it's a fixed size - expData := make([]byte, 4, len(valueData)+4) - binary.BigEndian.PutUint32(expData, uint32(d.exp)) - - // Return the byte array - return append(expData, valueData...), nil -} - -// Scan implements the sql.Scanner interface for database deserialization. -func (d *Decimal) Scan(value interface{}) error { - // first try to see if the data is stored in database as a Numeric datatype - switch v := value.(type) { - - case float32: - *d = NewFromFloat(float64(v)) - return nil - - case float64: - // numeric in sqlite3 sends us float64 - *d = NewFromFloat(v) - return nil - - case int64: - // at least in sqlite3 when the value is 0 in db, the data is sent - // to us as an int64 instead of a float64 ... - *d = New(v, 0) - return nil - - case uint64: - // while clickhouse may send 0 in db as uint64 - *d = NewFromUint64(v) - return nil - - case string: - var err error - *d, err = NewFromString(unquoteIfQuoted(v)) - return err - - case []byte: - var err error - *d, err = NewFromString(unquoteIfQuoted(string(v))) - return err - - default: - return fmt.Errorf("could not convert value '%+v' to any known type", value) - } -} - -// Value implements the driver.Valuer interface for database serialization. -func (d Decimal) Value() (driver.Value, error) { - return d.String(), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for XML -// deserialization. -func (d *Decimal) UnmarshalText(text []byte) error { - str := string(text) - - dec, err := NewFromString(str) - *d = dec - if err != nil { - return fmt.Errorf("error decoding string '%s': %s", str, err) - } - - return nil -} - -// MarshalText implements the encoding.TextMarshaler interface for XML -// serialization. -func (d Decimal) MarshalText() (text []byte, err error) { - return []byte(d.String()), nil -} - -// GobEncode implements the gob.GobEncoder interface for gob serialization. -func (d Decimal) GobEncode() ([]byte, error) { - return d.MarshalBinary() -} - -// GobDecode implements the gob.GobDecoder interface for gob serialization. -func (d *Decimal) GobDecode(data []byte) error { - return d.UnmarshalBinary(data) -} - -// DecodeSpanner decodes a Spanner value into a Decimal -func (d *Decimal) DecodeSpanner(val interface{}) error { - return d.Scan(val) -} - -// EncodeSpanner encodes a Decimal into a Spanner value -func (d Decimal) EncodeSpanner() (interface{}, error) { - return d.String(), nil -} - // StringScaled first scales the decimal then calls .String() on it. // // Deprecated: buggy and unintuitive. Use StringFixed instead. @@ -2467,8 +1377,6 @@ func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { return sign + intPart } -const zeros = "0000000000000000000000000000000000000000000000000000000000000000" - // ScientificNotationString serializes the decimal into standard scientific notation. // // The notation is normalized to have one non-zero digit followed by a decimal point and @@ -2574,343 +1482,3 @@ func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { return d1, d2 } - -func unquoteIfQuoted(value string) string { - // If the amount is quoted, strip the quotes - if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { - return value[1 : len(value)-1] - } - - return value -} - -// NullDecimal represents a nullable decimal with compatibility for -// scanning null values from the database. -type NullDecimal struct { - Decimal Decimal - Valid bool -} - -func NewNullDecimal(d Decimal) NullDecimal { - return NullDecimal{ - Decimal: d, - Valid: true, - } -} - -// Scan implements the sql.Scanner interface for database deserialization. -func (d *NullDecimal) Scan(value interface{}) error { - if value == nil { - d.Valid = false - return nil - } - err := d.Decimal.Scan(value) - d.Valid = err == nil - return err -} - -// Value implements the driver.Valuer interface for database serialization. -func (d NullDecimal) Value() (driver.Value, error) { - if !d.Valid { - return nil, nil - } - return d.Decimal.Value() -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { - if string(decimalBytes) == "null" { - d.Valid = false - return nil - } - err := d.Decimal.UnmarshalJSON(decimalBytes) - d.Valid = err == nil - return err -} - -// MarshalJSON implements the json.Marshaler interface. -func (d NullDecimal) MarshalJSON() ([]byte, error) { - if !d.Valid { - return []byte("null"), nil - } - return d.Decimal.MarshalJSON() -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for XML -// deserialization -func (d *NullDecimal) UnmarshalText(text []byte) error { - str := string(text) - - // check for empty XML or XML without body e.g., - if str == "" { - d.Valid = false - return nil - } - if err := d.Decimal.UnmarshalText(text); err != nil { - d.Valid = false - return err - } - d.Valid = true - return nil -} - -// MarshalText implements the encoding.TextMarshaler interface for XML -// serialization. -func (d NullDecimal) MarshalText() (text []byte, err error) { - if !d.Valid { - return []byte{}, nil - } - return d.Decimal.MarshalText() -} - -// DecodeSpanner decodes a Spanner value into a Decimal -func (d *NullDecimal) DecodeSpanner(value interface{}) error { - switch t := value.(type) { - case nil: - d.Valid = false - return nil - case *string: - if t == nil { - d.Valid = false - return nil - } - value = *t - } - - err := d.Decimal.Scan(value) - d.Valid = err == nil - return err -} - -// EncodeSpanner encodes a Decimal into a Spanner value -func (d NullDecimal) EncodeSpanner() (interface{}, error) { - if !d.Valid { - return nil, nil - } - return d.Decimal.String(), nil -} - -// Trig functions - -var ( - // Pi/4 split into three parts - pi4A = NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000 - pi4B = NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000 - pi4C = NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170 - m4PI = NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi - - atanP0 = NewFromFloat(-8.750608600031904122785e-01) - atanP1 = NewFromFloat(-1.615753718733365076637e+01) - atanP2 = NewFromFloat(-7.500855792314704667340e+01) - atanP3 = NewFromFloat(-1.228866684490136173410e+02) - atanP4 = NewFromFloat(-6.485021904942025371773e+01) - atanQ0 = NewFromFloat(2.485846490142306297962e+01) - atanQ1 = NewFromFloat(1.650270098316988542046e+02) - atanQ2 = NewFromFloat(4.328810604912902668951e+02) - atanQ3 = NewFromFloat(4.853903996359136964868e+02) - atanQ4 = NewFromFloat(1.945506571482613964425e+02) - - atanMorebits = NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits - tan3pio8 = NewFromFloat(2.41421356237309504880) // tan(3*pi/8) - piFloat = NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) -) - -// Atan returns the arctangent, in radians, of x. -func (d Decimal) Atan() Decimal { - if d.IsZero() { - return d - } - if d.IsPositive() { - return d.satan() - } - return d.Neg().satan().Neg() -} - -func (d Decimal) xatan() Decimal { - P0, P1, P2, P3, P4 := atanP0, atanP1, atanP2, atanP3, atanP4 - Q0, Q1, Q2, Q3, Q4 := atanQ0, atanQ1, atanQ2, atanQ3, atanQ4 - z := d.Mul(d) - b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z) - b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4) - z = b1.Div(b2) - z = d.Mul(z).Add(d) - return z -} - -// satan reduces its argument (known to be positive) -// to the range [0, 0.66] and calls xatan. -func (d Decimal) satan() Decimal { - Morebits, Tan3pio8, pi := atanMorebits, tan3pio8, piFloat - - if d.LessThanOrEqual(New(66, -2)) { - return d.xatan() - } - if d.GreaterThan(Tan3pio8) { - return pi.Div(New(2, 0)).Sub(New(1, 0).Div(d).xatan()).Add(Morebits) - } - return pi.Div(New(4, 0)).Add((d.Sub(New(1, 0)).Div(d.Add(New(1, 0)))).xatan()).Add(New(5, -1).Mul(Morebits)) -} - -// sin coefficients -var _sin = [...]Decimal{ - NewFromFloat(1.58962301576546568060e-10), // 0x3de5d8fd1fd19ccd - NewFromFloat(-2.50507477628578072866e-8), // 0xbe5ae5e5a9291f5d - NewFromFloat(2.75573136213857245213e-6), // 0x3ec71de3567d48a1 - NewFromFloat(-1.98412698295895385996e-4), // 0xbf2a01a019bfdf03 - NewFromFloat(8.33333333332211858878e-3), // 0x3f8111111110f7d0 - NewFromFloat(-1.66666666666666307295e-1), // 0xbfc5555555555548 -} - -// Sin returns the sine of the radian argument x. -func (d Decimal) Sin() Decimal { - PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI - - if d.IsZero() { - return d - } - // make argument positive but save the sign - sign := false - if d.IsNegative() { - d = d.Neg() - sign = true - } - - j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle - y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float - - // map zeros to origin - if j&1 == 1 { - j++ - y = y.Add(New(1, 0)) - } - j &= 7 // octant modulo 2Pi radians (360 degrees) - // reflect in x axis - if j > 3 { - sign = !sign - j -= 4 - } - z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic - zz := z.Mul(z) - - if j == 1 || j == 2 { - w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) - y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) - } else { - y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) - } - if sign { - y = y.Neg() - } - return y -} - -// cos coefficients -var _cos = [...]Decimal{ - NewFromFloat(-1.13585365213876817300e-11), // 0xbda8fa49a0861a9b - NewFromFloat(2.08757008419747316778e-9), // 0x3e21ee9d7b4e3f05 - NewFromFloat(-2.75573141792967388112e-7), // 0xbe927e4f7eac4bc6 - NewFromFloat(2.48015872888517045348e-5), // 0x3efa01a019c844f5 - NewFromFloat(-1.38888888888730564116e-3), // 0xbf56c16c16c14f91 - NewFromFloat(4.16666666666665929218e-2), // 0x3fa555555555554b -} - -// Cos returns the cosine of the radian argument x. -func (d Decimal) Cos() Decimal { - - PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI - - // make argument positive - sign := false - if d.IsNegative() { - d = d.Neg() - } - - j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle - y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float - - // map zeros to origin - if j&1 == 1 { - j++ - y = y.Add(New(1, 0)) - } - j &= 7 // octant modulo 2Pi radians (360 degrees) - // reflect in x axis - if j > 3 { - sign = !sign - j -= 4 - } - if j > 1 { - sign = !sign - } - - z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic - zz := z.Mul(z) - - if j == 1 || j == 2 { - y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) - } else { - w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) - y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) - } - if sign { - y = y.Neg() - } - return y -} - -var _tanP = [...]Decimal{ - NewFromFloat(-1.30936939181383777646e+4), // 0xc0c992d8d24f3f38 - NewFromFloat(1.15351664838587416140e+6), // 0x413199eca5fc9ddd - NewFromFloat(-1.79565251976484877988e+7), // 0xc1711fead3299176 -} -var _tanQ = [...]Decimal{ - NewFromFloat(1.00000000000000000000e+0), - NewFromFloat(1.36812963470692954678e+4), // 0x40cab8a5eeb36572 - NewFromFloat(-1.32089234440210967447e+6), // 0xc13427bc582abc96 - NewFromFloat(2.50083801823357915839e+7), // 0x4177d98fc2ead8ef - NewFromFloat(-5.38695755929454629881e+7), // 0xc189afe03cbe5a31 -} - -// Tan returns the tangent of the radian argument x. -func (d Decimal) Tan() Decimal { - - PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI - - if d.IsZero() { - return d - } - - // make argument positive but save the sign - sign := false - if d.IsNegative() { - d = d.Neg() - sign = true - } - - j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle - y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float - - // map zeros to origin - if j&1 == 1 { - j++ - y = y.Add(New(1, 0)) - } - - z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic - zz := z.Mul(z) - - if zz.GreaterThan(New(1, -14)) { - w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2])) - x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4]) - y = z.Add(z.Mul(w.Div(x))) - } else { - y = z - } - if j&2 == 2 { - y = New(-1, 0).Div(y) - } - if sign { - y = y.Neg() - } - return y -} diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..3239eac --- /dev/null +++ b/encoding.go @@ -0,0 +1,275 @@ +package decimal + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "math/big" +) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + return nil + } + + decimal, err := NewFromString(unquoteIfQuoted(string(decimalBytes))) + *d = decimal + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", string(decimalBytes), err) + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Decimal) MarshalJSON() ([]byte, error) { + str := d.String() + if MarshalJSONWithoutQuotes { + return []byte(str), nil + } + b := make([]byte, 0, len(str)+2) + b = append(b, '"') + b = append(b, str...) + return append(b, '"'), nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation +// is already used when encoding to text, this method stores that string as []byte +func (d *Decimal) UnmarshalBinary(data []byte) error { + // Verify we have at least 4 bytes for the exponent. The GOB encoded value + // may be empty. + if len(data) < 4 { + return fmt.Errorf("error decoding binary %v: expected at least 4 bytes, got %d", data, len(data)) + } + + // Extract the exponent + exp := int32(binary.BigEndian.Uint32(data[:4])) + if int64(exp) > int64(MaxDecodeExponent) || int64(exp) < -int64(MaxDecodeExponent) { + return fmt.Errorf("error decoding binary: exponent %d exceeds MaxDecodeExponent (%d)", exp, MaxDecodeExponent) + } + d.exp = exp + + // Extract the value + d.value = new(big.Int) + if err := d.value.GobDecode(data[4:]); err != nil { + return fmt.Errorf("error decoding binary %v: %s", data, err) + } + + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Decimal) MarshalBinary() (data []byte, err error) { + // exp is written first, but encode value first to know output size + var valueData []byte + if valueData, err = d.getValue().GobEncode(); err != nil { + return nil, err + } + + // Write the exponent in front, since it's a fixed size + expData := make([]byte, 4, len(valueData)+4) + binary.BigEndian.PutUint32(expData, uint32(d.exp)) + + // Return the byte array + return append(expData, valueData...), nil +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *Decimal) Scan(value interface{}) error { + // first try to see if the data is stored in database as a Numeric datatype + switch v := value.(type) { + + case float32: + *d = NewFromFloat(float64(v)) + return nil + + case float64: + // numeric in sqlite3 sends us float64 + *d = NewFromFloat(v) + return nil + + case int64: + // at least in sqlite3 when the value is 0 in db, the data is sent + // to us as an int64 instead of a float64 ... + *d = New(v, 0) + return nil + + case uint64: + // while clickhouse may send 0 in db as uint64 + *d = NewFromUint64(v) + return nil + + case string: + var err error + *d, err = NewFromString(unquoteIfQuoted(v)) + return err + + case []byte: + var err error + *d, err = NewFromString(unquoteIfQuoted(string(v))) + return err + + default: + return fmt.Errorf("could not convert value '%+v' to any known type", value) + } +} + +// Value implements the driver.Valuer interface for database serialization. +func (d Decimal) Value() (driver.Value, error) { + return d.String(), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization. +func (d *Decimal) UnmarshalText(text []byte) error { + str := string(text) + + dec, err := NewFromString(str) + *d = dec + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d Decimal) MarshalText() (text []byte, err error) { + return []byte(d.String()), nil +} + +// GobEncode implements the gob.GobEncoder interface for gob serialization. +func (d Decimal) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface for gob serialization. +func (d *Decimal) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +// DecodeSpanner decodes a Spanner value into a Decimal +func (d *Decimal) DecodeSpanner(val interface{}) error { + return d.Scan(val) +} + +// EncodeSpanner encodes a Decimal into a Spanner value +func (d Decimal) EncodeSpanner() (interface{}, error) { + return d.String(), nil +} + +func unquoteIfQuoted(value string) string { + // If the amount is quoted, strip the quotes + if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { + return value[1 : len(value)-1] + } + + return value +} + +// NullDecimal represents a nullable decimal with compatibility for +// scanning null values from the database. +type NullDecimal struct { + Decimal Decimal + Valid bool +} + +func NewNullDecimal(d Decimal) NullDecimal { + return NullDecimal{ + Decimal: d, + Valid: true, + } +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *NullDecimal) Scan(value interface{}) error { + if value == nil { + d.Valid = false + return nil + } + err := d.Decimal.Scan(value) + d.Valid = err == nil + return err +} + +// Value implements the driver.Valuer interface for database serialization. +func (d NullDecimal) Value() (driver.Value, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.Value() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + d.Valid = false + return nil + } + err := d.Decimal.UnmarshalJSON(decimalBytes) + d.Valid = err == nil + return err +} + +// MarshalJSON implements the json.Marshaler interface. +func (d NullDecimal) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte("null"), nil + } + return d.Decimal.MarshalJSON() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization +func (d *NullDecimal) UnmarshalText(text []byte) error { + str := string(text) + + // check for empty XML or XML without body e.g., + if str == "" { + d.Valid = false + return nil + } + if err := d.Decimal.UnmarshalText(text); err != nil { + d.Valid = false + return err + } + d.Valid = true + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d NullDecimal) MarshalText() (text []byte, err error) { + if !d.Valid { + return []byte{}, nil + } + return d.Decimal.MarshalText() +} + +// DecodeSpanner decodes a Spanner value into a Decimal +func (d *NullDecimal) DecodeSpanner(value interface{}) error { + switch t := value.(type) { + case nil: + d.Valid = false + return nil + case *string: + if t == nil { + d.Valid = false + return nil + } + value = *t + } + + err := d.Decimal.Scan(value) + d.Valid = err == nil + return err +} + +// EncodeSpanner encodes a Decimal into a Spanner value +func (d NullDecimal) EncodeSpanner() (interface{}, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.String(), nil +} diff --git a/math.go b/math.go new file mode 100644 index 0000000..6100afb --- /dev/null +++ b/math.go @@ -0,0 +1,1121 @@ +package decimal + +import ( + "fmt" + "math" + "math/big" + "math/bits" + "sync" +) + +var factorials = []Decimal{New(1, 0)} +var factorialsMutex sync.RWMutex + +// Pow returns d to the power of d2. +// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is +// rounded half away from zero to PowPrecisionNegativeExponent places after the decimal point. A result +// that would round to 0 keeps PowPrecisionNegativeExponent significant digits instead (at least one). +// +// Pow returns 0 (zero-value of Decimal) instead of error for power operation edge cases, to handle those edge cases use PowWithPrecision. +// Edge cases not handled by Pow: +// - 0 ** 0 => undefined value +// - 0 ** y, where y < 0 => infinity +// - x ** y, where x < 0 and y is non-integer decimal => imaginary value +// - x ** y, where the magnitude of the result or the precision is too large to represent +// +// Example: +// +// d1 := decimal.NewFromFloat(4.0) +// d2 := decimal.NewFromFloat(4.0) +// res1 := d1.Pow(d2) +// res1.String() // output: "256" +// +// d3 := decimal.NewFromFloat(5.0) +// d4 := decimal.NewFromFloat(5.73) +// res2 := d3.Pow(d4) +// res2.String() // output: "10118.0803715950193171" +// +// d5 := decimal.NewFromInt(10) +// d6 := decimal.NewFromInt(-18) +// res3 := d5.Pow(d6) +// res3.String() // output: "0.000000000000000001" +func (d Decimal) Pow(d2 Decimal) Decimal { + res, err := d.PowWithPrecision(d2, int32(PowPrecisionNegativeExponent)) + if err != nil { + return Decimal{} + } + return res +} + +// PowWithPrecision returns d to the power of d2. +// The result is exact for non-negative integer exponents. For negative or non-integer exponents it is +// rounded half away from zero to precision places after the decimal point. A result that would round +// to 0 keeps precision significant digits instead (at least one). +// +// PowWithPrecision returns error when: +// - 0 ** 0 => undefined value +// - 0 ** y, where y < 0 => infinity +// - x ** y, where x < 0 and y is non-integer decimal => imaginary value +// - x ** y, where the magnitude of the result or the precision is too large to represent +// +// Example: +// +// d1 := decimal.NewFromFloat(4.0) +// d2 := decimal.NewFromFloat(4.0) +// res1, err := d1.PowWithPrecision(d2, 2) +// res1.String() // output: "256" +// +// d3 := decimal.NewFromFloat(5.0) +// d4 := decimal.NewFromFloat(5.73) +// res2, err := d3.PowWithPrecision(d4, 5) +// res2.String() // output: "10118.08037" +// +// d5 := decimal.NewFromFloat(-3.0) +// d6 := decimal.NewFromFloat(-6.0) +// res3, err := d5.PowWithPrecision(d6, 10) +// res3.String() // output: "0.0013717421" +func (d Decimal) PowWithPrecision(d2 Decimal, precision int32) (Decimal, error) { + if d.IsZero() { + if d2.IsZero() { + return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") + } + if d2.Sign() < 0 { + return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") + } + return Decimal{zeroInt, 0}, nil + } + + if d2.IsInteger() { + return d.powBigIntWithPrecision(d2.BigInt(), precision) + } + + if d.Sign() < 0 { + return Decimal{}, fmt.Errorf("cannot represent imaginary value of x ** y, where x < 0 and y is non-integer decimal") + } + + return d.powFrac(d2, precision) +} + +// PowInt32 returns d to the power of exp, where exp is int32. +// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. +// +// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places +// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant +// digits instead (at least one). +// +// Example: +// +// d1, err := decimal.NewFromFloat(4.0).PowInt32(4) +// d1.String() // output: "256" +// +// d2, err := decimal.NewFromFloat(3.13).PowInt32(5) +// d2.String() // output: "300.4150512793" +func (d Decimal) PowInt32(exp int32) (Decimal, error) { + return d.PowBigInt(big.NewInt(int64(exp))) +} + +// PowBigInt returns d to the power of exp, where exp is big.Int. +// Returns error for 0 ** 0, 0 ** exp where exp < 0, and results whose magnitude is too large to represent. +// +// For negative exponents the result is rounded half away from zero to PowPrecisionNegativeExponent places +// after the decimal point. A result that would round to 0 keeps PowPrecisionNegativeExponent significant +// digits instead (at least one). +// +// Example: +// +// d1, err := decimal.NewFromFloat(3.0).PowBigInt(big.NewInt(3)) +// d1.String() // output: "27" +// +// d2, err := decimal.NewFromFloat(629.25).PowBigInt(big.NewInt(5)) +// d2.String() // output: "98654323103449.5673828125" +func (d Decimal) PowBigInt(exp *big.Int) (Decimal, error) { + return d.powBigIntWithPrecision(exp, int32(PowPrecisionNegativeExponent)) +} + +var errPowOutOfRange = fmt.Errorf("cannot represent result of power operation, its magnitude or precision is too large") + +// powExactBits is the largest estimated cost of computing d^n exactly for negative n. +// Bigger powers are approximated with truncated intermediates, which is cheaper. +const powExactBits = 3000 + +func (d Decimal) powBigIntWithPrecision(exp *big.Int, precision int32) (Decimal, error) { + switch { + case exp.Sign() > 0: + return d.powExact(exp) + case d.IsZero() && exp.Sign() == 0: + return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") + case exp.Sign() == 0: + return Decimal{oneInt, 0}, nil + case d.IsZero(): + return Decimal{}, fmt.Errorf("cannot represent infinity value of 0 ** y, where y < 0") + } + return d.powNegInt(new(big.Int).Neg(exp), precision) +} + +// powExact returns d^n for n > 0. +func (d Decimal) powExact(n *big.Int) (Decimal, error) { + exp := new(big.Int).Mul(n, big.NewInt(int64(d.exp))) + if !exp.IsInt64() || exp.Int64() > math.MaxInt32 || exp.Int64() < math.MinInt32 { + return Decimal{}, errPowOutOfRange + } + return Decimal{new(big.Int).Exp(d.getValue(), n, nil), int32(exp.Int64())}, nil +} + +// powNegInt returns d^-n for n > 0, rounded as described in PowWithPrecision. +func (d Decimal) powNegInt(n *big.Int, precision int32) (Decimal, error) { + if d.absIsOne() { + // only the parity of n matters + n = big.NewInt(2 - int64(n.Bit(0))) + } + + // small powers are cheap to compute exactly, and |log10(d^-n)| <= cost keeps them in range + if n.IsInt64() && n.Int64() <= powExactBits { + cost := n.Int64() * (int64(d.getValue().BitLen()) + abs64(int64(d.exp))) + if cost <= powExactBits && powInRange(float64(cost), precision) { + return d.powNegIntExact(n, precision) + } + } + + nf, _ := new(big.Float).SetInt(n).Float64() + lg := -nf * d.log10Abs() + if !powInRange(lg, precision) { + return Decimal{}, errPowOutOfRange + } + + // the digit shifts of the truncated c^n must fit into int64 + if nd := int64(d.NumDigits()); n.IsInt64() && n.Int64() <= math.MaxInt64/(4*nd) { + c := new(big.Int).Abs(d.getValue()) + w := powSigDigits(lg, precision) + int64(len(n.String())) + 6 + // each try adds 20 digits, failing 5 times means an exact tie or a value extremely close to one + for i := 0; i < 5; i, w = i+1, w+20 { + if res, ok := powNegIntApprox(c, int64(d.exp), n, w, precision); ok { + if d.Sign() < 0 && n.Bit(0) == 1 { + res.value.Neg(res.value) + } + return res, nil + } + } + } + + // same as for non-integer exponents, which handles any n and exact ties + res, err := d.Abs().powFrac(Decimal{value: new(big.Int).Neg(n)}, precision) + if err == nil && d.Sign() < 0 && n.Bit(0) == 1 { + res.value.Neg(res.value) + } + return res, err +} + +// powNegIntExact returns d^-n for n > 0, dividing by the exact d^n. +func (d Decimal) powNegIntExact(n *big.Int, precision int32) (Decimal, error) { + x, err := d.powExact(n) + if err != nil { + return Decimal{}, err + } + res := New(1, 0).DivRound(x, precision) + if res.IsZero() { + // first significant digit of 1/x is at place NumDigits+exp, one earlier when x is a power of ten + nd := int32(x.NumDigits()) + places := int32(powMinSig(precision)) - 1 + nd + x.exp + if x.getValue().CmpAbs(pow10(int64(nd)-1)) == 0 { + places-- + } + res = New(1, 0).DivRound(x, places) + } + return res, nil +} + +// powNegIntApprox rounds |1 / (c^n * 10^(e*n))| as described in PowWithPrecision. c^n is approximated +// from below by m*10^s, truncating m to w digits after every multiplication. Each truncation shrinks +// the result by a factor of at most 1-10^(1-w), and the errors add up to less than 5n*10^(1-w), +// lo below allows ten times that. +func powNegIntApprox(c *big.Int, e int64, n *big.Int, w int64, precision int32) (Decimal, bool) { + cw := new(big.Int).Set(c) + cs, exact := truncDigits(cw, w) + + m, s := big.NewInt(1), int64(0) + for i := n.BitLen() - 1; i >= 0; i-- { + m.Mul(m, m) + ds, ex := truncDigits(m, w) + s, exact = 2*s+ds, exact && ex + if n.Bit(i) == 1 { + m.Mul(m, cw) + ds, ex = truncDigits(m, w) + s, exact = s+cs+ds, exact && ex + } + } + + // 10^scale / (m * 10^(s+e*n)) = 10^z / m + z := int64(Decimal{value: m}.NumDigits()) + w + 2 + scale := z + s + e*n.Int64() + q, rem := new(big.Int).QuoRem(pow10(z), m, new(big.Int)) + lo, hi := q, q + if !exact || rem.Sign() != 0 { + hi = new(big.Int).Add(q, oneInt) + } + if !exact { + lo = new(big.Int).Mul(q, big.NewInt(5)) + lo.Mul(lo, n).Quo(lo, pow10(w-2)) + lo.Sub(q, lo).Sub(lo, oneInt) + } + return roundBounds(lo, hi, scale, precision) +} + +// truncDigits truncates m > 0 to at least w digits, returning the number of dropped digits +// and whether they were all zeros. +func truncDigits(m *big.Int, w int64) (int64, bool) { + // m >= 2^(BitLen-1), so m has at least floor((BitLen-1)*log10(2))+1 digits + drop := int64(float64(m.BitLen()-1)*math.Log10(2)-1e-9) + 1 - w + if drop <= 0 { + return 0, true + } + var r big.Int + m.QuoRem(m, pow10(drop), &r) + return drop, r.Sign() == 0 +} + +// powFrac returns d^y for d > 0 and non-integer (or negative integer) y, rounded as described in +// PowWithPrecision. It evaluates d^y = 10^q * e^s, where y*ln(d) = q*ln(10) + s and 0 <= s < ln(10), +// in binary fixed-point arithmetic, adding working digits until the error bounds decide the rounding. +func (d Decimal) powFrac(y Decimal, precision int32) (Decimal, error) { + if d.absIsOne() { + // 1^y = 1, also for y beyond float64 range + if !powInRange(0, precision) { + return Decimal{}, errPowOutOfRange + } + k := int64(abs(precision)) + 2 + res, _ := roundBounds(pow10(k), pow10(k), k, precision) + return res, nil + } + + yf := y.InexactFloat64() + lg := yf * d.log10Abs() + if !powInRange(lg, precision) { + return Decimal{}, errPowOutOfRange + } + + k := int64(d.NumDigits()) - 1 + int64(d.exp) + // covers the working error amplified by |y| and |k|, see powFracFixed + extra := int64(math.Log10(math.Abs(yf)*(math.Abs(float64(k))+64)+math.Abs(lg)+1)) + 4 + // 9 guard digits make a retry unlikely, each retry adds 20 more + f := powSigDigits(lg, precision) + 9 + exactTie := false + for i := 0; ; i, f = i+1, f+20 { + if res, ok := powFracFixed(d, y, k, f, extra, precision, exactTie); ok { + return res, nil + } + // only a terminating result can sit exactly on a rounding boundary, which no working + // precision resolves, then the upper bound rounds it away from zero + if i == 8 { + exactTie = powIsTerminating(d, y) + } + } +} + +// powIsTerminating reports whether d^y is a terminating decimal, for d > 0. +func powIsTerminating(d, y Decimal) bool { + p, q := powRatio(y) + num, den := powRatio(d) + if !q.IsInt64() { + return false + } + // d^(p/q) is rational only when num and den are perfect q-th powers + rn, ok := iroot(num, q.Int64()) + if !ok { + return false + } + rd, ok := iroot(den, q.Int64()) + if !ok { + return false + } + // (rn/rd)^p terminates when its denominator has no prime factors other than 2 and 5 + if p.Sign() < 0 { + rd = rn + } + rd = new(big.Int).Set(rd) + for _, f := range []*big.Int{twoInt, fiveInt} { + for r := new(big.Int); rd.Cmp(oneInt) > 0 && r.Rem(rd, f).Sign() == 0; { + rd.Quo(rd, f) + } + } + return rd.Cmp(oneInt) == 0 +} + +// powRatio returns d as a fraction p/q in lowest terms, q > 0. +func powRatio(d Decimal) (*big.Int, *big.Int) { + if d.exp >= 0 { + return new(big.Int).Mul(d.getValue(), pow10(int64(d.exp))), big.NewInt(1) + } + p, q := new(big.Int).Set(d.getValue()), new(big.Int).Set(pow10(-int64(d.exp))) + g := new(big.Int).GCD(nil, nil, new(big.Int).Abs(p), q) + return p.Quo(p, g), q.Quo(q, g) +} + +// iroot returns the integer k-th root of x >= 1 and whether it is exact. +func iroot(x *big.Int, k int64) (*big.Int, bool) { + if k == 1 || x.Cmp(oneInt) == 0 { + return x, true + } + if int64(x.BitLen()) <= k { + // 1 < x < 2^k has no integer k-th root + return nil, false + } + // Newton's method from above converges to the floor of the root + kb, km1 := big.NewInt(k), big.NewInt(k-1) + r := new(big.Int).Lsh(oneInt, uint((int64(x.BitLen())+k-1)/k)) + t, u := new(big.Int), new(big.Int) + for { + t.Quo(x, t.Exp(r, km1, nil)) + t.Add(t, u.Mul(r, km1)).Quo(t, kb) + if t.Cmp(r) >= 0 { + break + } + r.Set(t) + } + return r, t.Exp(r, kb, nil).Cmp(x) == 0 +} + +// powFracFixed computes d^y (see powFrac) as an interval of e^s * 10^f with f+extra working digits +// and rounds it. With force the upper bound is used as the exact value. +func powFracFixed(d, y Decimal, k, f, extra int64, precision int32, force bool) (Decimal, bool) { + b := uint(float64(f+extra)*math.Log2(10)) + 1 + + // d = m * 10^k, 1 <= m < 10 + m := new(big.Int).Lsh(d.getValue(), b) + m.Quo(m, pow10(int64(d.NumDigits())-1)) + t, lnErr := lnFixed(m, b) + ln10, ln10Err := ln10Fixed(b) + t.Add(t, new(big.Int).Mul(big.NewInt(k), ln10)) + tErr := big.NewInt(lnErr + 1 + abs64(k)*ln10Err) + + // t = y * ln(d) + t.Mul(t, y.getValue()) + if y.exp >= 0 { + t.Mul(t, pow10(int64(y.exp))) + } else { + t.Quo(t, pow10(-int64(y.exp))) + } + tErr.Mul(tErr, y.Abs().Ceil().getValue()).Add(tErr, oneInt) + + q, s := new(big.Int).DivMod(t, ln10, new(big.Int)) + sErr := new(big.Int).Abs(q) + sErr.Mul(sErr, big.NewInt(ln10Err)).Add(sErr, tErr) + + // e^s < 11, so an error in s grows at most 11 times + r, rErr := expFixed(s, b) + sErr.Mul(sErr, big.NewInt(11)).Add(sErr, big.NewInt(rErr+1)) + + // v * 10^(f-q) = e^s * 10^f + p := pow10(f) + hi := new(big.Int).Add(r, sErr) + hi.Mul(hi, p).Rsh(hi, b).Add(hi, oneInt) + lo := hi + if !force { + lo = new(big.Int).Sub(r, sErr) + lo.Mul(lo, p).Rsh(lo, b) + if lo.Sign() < 0 { + lo.SetInt64(0) + } + } + return roundBounds(lo, hi, f-q.Int64(), precision) +} + +// lnFixed returns ln(m / 2^b) * 2^b for m / 2^b in [1, 10], with its error bound in ulps. +func lnFixed(m *big.Int, b uint) (*big.Int, int64) { + // a = math.Log(m) is accurate to ~1e-15, so ln(m) = a + ln(1+z), where z = m*e^-a - 1 is tiny + // and the series ln(1+z) = z - z^2/2 + z^3/3 - ... gains ~50 bits per term. + top, sh := new(big.Int), m.BitLen()-60 + if sh > 0 { + top.Rsh(m, uint(sh)) + } else { + top.Lsh(m, uint(-sh)) + } + a := big.NewInt(int64(math.Ldexp(math.Log(math.Ldexp(float64(top.Uint64()), sh-int(b))), 52))) + if b >= 52 { + a.Lsh(a, b-52) + } else { + a.Rsh(a, 52-b) + } + + e, eErr := expFixed(new(big.Int).Neg(a), b) + z := e.Mul(e, m).Rsh(e, b) + z.Sub(z, new(big.Int).Lsh(oneInt, b)) + + sum, p, term, iv := new(big.Int).Set(z), new(big.Int).Set(z), new(big.Int), new(big.Int) + terms := int64(1) + for i := int64(2); ; i++ { + p.Mul(p, z).Rsh(p, b) + if p.Sign() == 0 { + break + } + term.Quo(p, iv.SetInt64(i)) + if i%2 == 0 { + sum.Sub(sum, term) + } else { + sum.Add(sum, term) + } + terms++ + } + // m <= 10 amplifies the error of e^-a at most 10 times + return sum.Add(sum, a), 10*eErr + 2*terms + 3 +} + +// expFixed returns e^(x / 2^b) * 2^b for |x / 2^b| <= 2.4, with its error bound in ulps. +func expFixed(x *big.Int, b uint) (*big.Int, int64) { + // e^x = (e^(x/2^j))^(2^j): Taylor series of the reduced argument, then j squarings. + // Guard bits cover the error of up to ~g Taylor terms, amplified up to 11*2^j times by squaring. + j := uint(math.Sqrt(float64(b))) + 1 + g := b + j + uint(bits.Len(33*(b+j+64)+363)) + 2 + + xr := new(big.Int).Lsh(x, g-b) + xr.Rsh(xr, j) + sum, term, iv := new(big.Int).Lsh(oneInt, g), new(big.Int).Lsh(oneInt, g), new(big.Int) + for i := int64(1); ; i++ { + term.Mul(term, xr).Rsh(term, g) + term.Quo(term, iv.SetInt64(i)) + if term.Sign() == 0 { + break + } + sum.Add(sum, term) + } + for ; j > 0; j-- { + sum.Mul(sum, sum).Rsh(sum, g) + } + return sum.Rsh(sum, g-b), 2 +} + +var ln10Cache struct { + once sync.Once + v *big.Int +} + +const ln10CacheBits = 2048 + +// ln10Fixed returns ln(10) * 2^b, with its error bound in ulps. +func ln10Fixed(b uint) (*big.Int, int64) { + if b <= ln10CacheBits { + ln10Cache.once.Do(func() { ln10Cache.v, _ = ln10FromDigits(ln10CacheBits) }) + return new(big.Int).Rsh(ln10Cache.v, ln10CacheBits-b), 3 + } + if v, ok := ln10FromDigits(b); ok { + return v, 2 + } + return lnFixed(new(big.Int).Lsh(big.NewInt(10), b), b) +} + +// ln10FromDigits returns ln(10) * 2^b computed from strLn10, if it has enough digits. +func ln10FromDigits(b uint) (*big.Int, bool) { + digits := int(float64(b)*math.Log10(2)) + 3 + if digits+2 > len(strLn10) { + return nil, false + } + v, _ := new(big.Int).SetString(strLn10[:1]+strLn10[2:digits+2], 10) + v.Lsh(v, b) + return v.Quo(v, pow10(int64(digits))), true +} + +// roundBounds rounds v, known only by lo <= v*10^scale <= hi (lo >= 0), half away from zero to +// precision places after the decimal point, or to max(precision, 1) significant digits when that +// would be 0. It reports false when lo and hi do not round to the same value. +func roundBounds(lo, hi *big.Int, scale int64, precision int32) (Decimal, bool) { + g := scale - int64(precision) + if g < 1 { + return Decimal{}, false + } + dhi := int64(Decimal{value: hi}.NumDigits()) + // with fewer than g digits hi rounds to 0, skip building 10^g for tiny results + if g <= dhi { + if q := roundHalfUp(hi, g); q.Sign() != 0 { + return Decimal{q, -precision}, q.Cmp(roundHalfUp(lo, g)) == 0 + } + } + + sig := powMinSig(precision) + g = dhi - sig + if g < 2 { + return Decimal{}, false + } + q := roundHalfUp(hi, g) + res := Decimal{q, int32(g - scale)} + if int64(Decimal{value: lo}.NumDigits()) == dhi { + return res, q.Cmp(roundHalfUp(lo, g)) == 0 + } + // lo < 10^(dhi-1) <= hi, decided only when both round to that power of ten + return res, q.Cmp(pow10(sig-1)) == 0 && roundHalfUp(lo, g-1).Cmp(pow10(sig)) == 0 +} + +// roundHalfUp returns x / 10^g rounded half away from zero, for x >= 0 and g >= 1. +func roundHalfUp(x *big.Int, g int64) *big.Int { + p := pow10(g) + q, r := new(big.Int).QuoRem(x, p, new(big.Int)) + if r.Lsh(r, 1).Cmp(p) >= 0 { + q.Add(q, oneInt) + } + return q +} + +// powSigDigits returns the number of significant digits of a result of magnitude 10^lg rounded to +// precision places, at least powMinSig(precision). +func powSigDigits(lg float64, precision int32) int64 { + if n := int64(math.Floor(lg)) + 1 + int64(precision); n > powMinSig(precision) { + return n + } + return powMinSig(precision) +} + +// powMinSig returns the significant digits kept by results that would round to zero. +func powMinSig(precision int32) int64 { + if precision < 1 { + return 1 + } + return int64(precision) +} + +// powInRange reports whether a result of magnitude 10^lg rounded to precision places has an exponent +// that fits into int32. +func powInRange(lg float64, precision int32) bool { + return math.Abs(lg)+math.Abs(float64(precision)) < math.MaxInt32/2 +} + +// absIsOne reports whether |d| = 1. +func (d Decimal) absIsOne() bool { + nd := int64(d.NumDigits()) + return nd-1+int64(d.exp) == 0 && d.getValue().CmpAbs(pow10(nd-1)) == 0 +} + +// log10Abs approximates log10(|d|) for d != 0. +func (d Decimal) log10Abs() float64 { + var m big.Float + e2 := new(big.Float).SetInt(d.getValue()).MantExp(&m) + mf, _ := m.Float64() + return math.Log10(math.Abs(mf)) + float64(e2)*math.Log10(2) + float64(d.exp) +} + +func abs64(n int64) int64 { + if n < 0 { + return -n + } + return n +} + +// ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. +// OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). +// +// ExpHullAbrham is faster than ExpTaylor for small precision values, but it is much slower for large precision values. +// +// Example: +// +// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000" +// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284" +func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { + // Algorithm based on Variable precision exponential function. + // ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham. + if d.IsZero() { + return Decimal{oneInt, 0}, nil + } + + currentPrecision := overallPrecision + + // Algorithm does not work if currentPrecision * 23 < |x|. + // Precision is automatically increased in such cases, so the value can be calculated precisely. + // If newly calculated precision is higher than ExpMaxIterations the currentPrecision will not be changed. + f := d.Abs().InexactFloat64() + if ncp := f / 23; ncp > float64(currentPrecision) && ncp < float64(ExpMaxIterations) { + currentPrecision = uint32(math.Ceil(ncp)) + } + + // fail if abs(d) beyond an over/underflow threshold + overflowThreshold := New(23*int64(currentPrecision), 0) + if d.Abs().Cmp(overflowThreshold) > 0 { + return Decimal{}, fmt.Errorf("over/underflow threshold, exp(x) cannot be calculated precisely") + } + + // Return 1 if abs(d) small enough; this also avoids later over/underflow + overflowThreshold2 := New(9, -int32(currentPrecision)-1) + if d.Abs().Cmp(overflowThreshold2) <= 0 { + return Decimal{oneInt, 0}, nil + } + + // t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1 + t := d.exp + int32(d.NumDigits()) // Add d.NumDigits because the paper assumes that d.value [0.1, 1) + + if t < 0 { + t = 0 + } + + k := New(1, t) // reduction factor + r := Decimal{new(big.Int).Set(d.getValue()), d.exp - t} // reduced argument + p := int32(currentPrecision) + t + 2 // precision for calculating the sum + + // Determine n, the number of therms for calculating sum + // use first Newton step (1.435p - 1.182) / log10(p/abs(r)) + // for solving appropriate equation, along with directed + // roundings and simple rational bound for log10(p/abs(r)) + rf := r.Abs().InexactFloat64() + pf := float64(p) + nf := math.Ceil((1.453*pf - 1.182) / math.Log10(pf/rf)) + if nf > float64(ExpMaxIterations) || math.IsNaN(nf) { + return Decimal{}, fmt.Errorf("exact value cannot be calculated in <=ExpMaxIterations iterations") + } + n := int64(nf) + + tmp := New(0, 0) + sum := New(1, 0) + one := New(1, 0) + for i := n - 1; i > 0; i-- { + tmp.value.SetInt64(i) + sum = sum.Mul(r.DivRound(tmp, p)) + sum = sum.Add(one) + } + + // res = sum^ki, the same value ki repeated multiplications would give + ki := k.IntPart() + expInt64 := int64(sum.exp) * ki + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) + } + res := Decimal{new(big.Int).Exp(sum.getValue(), big.NewInt(ki), nil), int32(expInt64)} + + resNumDigits := int32(res.NumDigits()) + + var roundDigits int32 + if resNumDigits > abs(res.exp) { + roundDigits = int32(currentPrecision) - resNumDigits - res.exp + } else { + roundDigits = int32(currentPrecision) + } + + res = res.Round(roundDigits) + + return res, nil +} + +// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// ExpTaylor is much faster for large precision values than ExpHullAbrham. +// +// Example: +// +// d, err := NewFromFloat(26.1).ExpTaylor(2).String() +// d.String() // output: "216314672147.06" +// +// NewFromFloat(26.1).ExpTaylor(20).String() +// d.String() // output: "216314672147.05767284062928674083" +// +// NewFromFloat(26.1).ExpTaylor(-10).String() +// d.String() // output: "220000000000" +func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { + // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only + if d.IsZero() { + return Decimal{oneInt, 0}.Round(precision), nil + } + + var epsilon Decimal + var divPrecision int32 + if precision < 0 { + epsilon = New(1, -1) + divPrecision = 8 + } else { + epsilon = New(1, -precision-1) + divPrecision = precision + 1 + } + + decAbs := d.Abs() + pow := d.Abs() + factorial := New(1, 0) + + result := New(1, 0) + + for i := int64(1); ; { + step := pow.DivRound(factorial, divPrecision) + result = result.Add(step) + + // Stop Taylor series when current step is smaller than epsilon + if step.Cmp(epsilon) < 0 { + break + } + + pow = pow.Mul(decAbs) + + i++ + + // Calculate next factorial number or retrieve cached value + factorialsMutex.RLock() + if len(factorials) >= int(i) && !factorials[i-1].IsZero() { + factorial = factorials[i-1] + factorialsMutex.RUnlock() + } else { + prevFactorial := factorials[i-2] + factorialsMutex.RUnlock() + factorial = prevFactorial.Mul(New(i, 0)) + factorialsMutex.Lock() + // Check again in case another goroutine already added it. + if len(factorials) < int(i) || factorials[i-1].IsZero() { + factorials = append(factorials, Zero) + factorials[i-1] = factorial + } + factorialsMutex.Unlock() + } + } + + if d.Sign() < 0 { + result = New(1, 0).DivRound(result, precision+1) + } + + result = result.Round(precision) + return result, nil +} + +// Ln calculates natural logarithm of d. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// Example: +// +// d1, err := NewFromFloat(13.3).Ln(2) +// d1.String() // output: "2.59" +// +// d2, err := NewFromFloat(579.161).Ln(10) +// d2.String() // output: "6.3615805046" +func (d Decimal) Ln(precision int32) (Decimal, error) { + // Algorithm based on The Use of Iteration Methods for Approximating the Natural Logarithm, + // James F. Epperson, The American Mathematical Monthly, Vol. 96, No. 9, November 1989, pp. 831-835. + if d.IsNegative() { + return Decimal{}, fmt.Errorf("cannot calculate natural logarithm for negative decimals") + } + + if d.IsZero() { + return Decimal{}, fmt.Errorf("cannot represent natural logarithm of 0, result: -infinity") + } + + calcPrecision := precision + 2 + z := d.Copy() + + var comp1, comp3, comp2, comp4, reduceAdjust Decimal + comp1 = z.Sub(Decimal{oneInt, 0}) + comp3 = Decimal{oneInt, -1} + + // for decimal in range [0.9, 1.1] where ln(d) is close to 0 + usePowerSeries := false + + if comp1.Abs().Cmp(comp3) <= 0 { + usePowerSeries = true + } else { + // reduce input decimal to range [0.1, 1) + expDelta := int32(z.NumDigits()) + z.exp + z.exp -= expDelta + + // Input decimal was reduced by factor of 10^expDelta, thus we will need to add + // ln(10^expDelta) = expDelta * ln(10) + // to the result to compensate that + ln10 := ln10.withPrecision(calcPrecision) + reduceAdjust = NewFromInt32(expDelta) + reduceAdjust = reduceAdjust.Mul(ln10) + + comp1 = z.Sub(Decimal{oneInt, 0}) + + if comp1.Abs().Cmp(comp3) <= 0 { + usePowerSeries = true + } else { + // initial estimate using floats + zFloat := z.InexactFloat64() + comp1 = NewFromFloat(math.Log(zFloat)) + } + } + + epsilon := Decimal{oneInt, -calcPrecision} + + if usePowerSeries { + // Power Series - https://en.wikipedia.org/wiki/Logarithm#Power_series + // Calculating n-th term of formula: ln(z+1) = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] + // until the difference between current and next term is smaller than epsilon. + // Coverage quite fast for decimals close to 1.0 + + // z + 2 + comp2 = comp1.Add(Decimal{twoInt, 0}) + // z / (z + 2) + comp3 = comp1.DivRound(comp2, calcPrecision) + // 2 * (z / (z + 2)) + comp1 = comp3.Add(comp3) + comp2 = comp1.Copy() + + for n := 1; ; n++ { + // 2 * (z / (z+2))^(2n+1) + comp2 = comp2.Mul(comp3).Mul(comp3) + + // 1 / (2n+1) * 2 * (z / (z+2))^(2n+1) + comp4 = NewFromInt(int64(2*n + 1)) + comp4 = comp2.DivRound(comp4, calcPrecision) + + // comp1 = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] + comp1 = comp1.Add(comp4) + + if comp4.Abs().Cmp(epsilon) <= 0 { + break + } + } + } else { + // Halley's Iteration. + // Calculating n-th term of formula: a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z), + // until the difference between current and next term is smaller than epsilon + var prevStep Decimal + maxIters := calcPrecision*2 + 10 + + for i := int32(0); i < maxIters; i++ { + // exp(a_n) + comp3, _ = comp1.ExpTaylor(calcPrecision) + // exp(a_n) - z + comp2 = comp3.Sub(z) + // 2 * (exp(a_n) - z) + comp2 = comp2.Add(comp2) + // exp(a_n) + z + comp4 = comp3.Add(z) + // 2 * (exp(a_n) - z) / (exp(a_n) + z) + comp3 = comp2.DivRound(comp4, calcPrecision) + // comp1 = a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z) + comp1 = comp1.Sub(comp3) + + if prevStep.Add(comp3).IsZero() { + // If iteration steps oscillate we should return early and prevent an infinity loop + // NOTE(mwoss): This should be quite a rare case, returning error is not necessary + break + } + + if comp3.Abs().Cmp(epsilon) <= 0 { + break + } + + prevStep = comp3 + } + } + + comp1 = comp1.Add(reduceAdjust) + + return comp1.Round(precision), nil +} + +// Trig functions + +var ( + // Pi/4 split into three parts + pi4A = NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000 + pi4B = NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000 + pi4C = NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170 + m4PI = NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + atanP0 = NewFromFloat(-8.750608600031904122785e-01) + atanP1 = NewFromFloat(-1.615753718733365076637e+01) + atanP2 = NewFromFloat(-7.500855792314704667340e+01) + atanP3 = NewFromFloat(-1.228866684490136173410e+02) + atanP4 = NewFromFloat(-6.485021904942025371773e+01) + atanQ0 = NewFromFloat(2.485846490142306297962e+01) + atanQ1 = NewFromFloat(1.650270098316988542046e+02) + atanQ2 = NewFromFloat(4.328810604912902668951e+02) + atanQ3 = NewFromFloat(4.853903996359136964868e+02) + atanQ4 = NewFromFloat(1.945506571482613964425e+02) + + atanMorebits = NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits + tan3pio8 = NewFromFloat(2.41421356237309504880) // tan(3*pi/8) + piFloat = NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) +) + +// Atan returns the arctangent, in radians, of x. +func (d Decimal) Atan() Decimal { + if d.IsZero() { + return d + } + if d.IsPositive() { + return d.satan() + } + return d.Neg().satan().Neg() +} + +func (d Decimal) xatan() Decimal { + P0, P1, P2, P3, P4 := atanP0, atanP1, atanP2, atanP3, atanP4 + Q0, Q1, Q2, Q3, Q4 := atanQ0, atanQ1, atanQ2, atanQ3, atanQ4 + z := d.Mul(d) + b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z) + b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4) + z = b1.Div(b2) + z = d.Mul(z).Add(d) + return z +} + +// satan reduces its argument (known to be positive) +// to the range [0, 0.66] and calls xatan. +func (d Decimal) satan() Decimal { + Morebits, Tan3pio8, pi := atanMorebits, tan3pio8, piFloat + + if d.LessThanOrEqual(New(66, -2)) { + return d.xatan() + } + if d.GreaterThan(Tan3pio8) { + return pi.Div(New(2, 0)).Sub(New(1, 0).Div(d).xatan()).Add(Morebits) + } + return pi.Div(New(4, 0)).Add((d.Sub(New(1, 0)).Div(d.Add(New(1, 0)))).xatan()).Add(New(5, -1).Mul(Morebits)) +} + +// sin coefficients +var _sin = [...]Decimal{ + NewFromFloat(1.58962301576546568060e-10), // 0x3de5d8fd1fd19ccd + NewFromFloat(-2.50507477628578072866e-8), // 0xbe5ae5e5a9291f5d + NewFromFloat(2.75573136213857245213e-6), // 0x3ec71de3567d48a1 + NewFromFloat(-1.98412698295895385996e-4), // 0xbf2a01a019bfdf03 + NewFromFloat(8.33333333332211858878e-3), // 0x3f8111111110f7d0 + NewFromFloat(-1.66666666666666307295e-1), // 0xbfc5555555555548 +} + +// Sin returns the sine of the radian argument x. +func (d Decimal) Sin() Decimal { + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI + + if d.IsZero() { + return d + } + // make argument positive but save the sign + sign := false + if d.IsNegative() { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(New(1, 0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) + } else { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } + if sign { + y = y.Neg() + } + return y +} + +// cos coefficients +var _cos = [...]Decimal{ + NewFromFloat(-1.13585365213876817300e-11), // 0xbda8fa49a0861a9b + NewFromFloat(2.08757008419747316778e-9), // 0x3e21ee9d7b4e3f05 + NewFromFloat(-2.75573141792967388112e-7), // 0xbe927e4f7eac4bc6 + NewFromFloat(2.48015872888517045348e-5), // 0x3efa01a019c844f5 + NewFromFloat(-1.38888888888730564116e-3), // 0xbf56c16c16c14f91 + NewFromFloat(4.16666666666665929218e-2), // 0x3fa555555555554b +} + +// Cos returns the cosine of the radian argument x. +func (d Decimal) Cos() Decimal { + + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI + + // make argument positive + sign := false + if d.IsNegative() { + d = d.Neg() + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(New(1, 0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + if j > 1 { + sign = !sign + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } else { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = New(1, 0).Sub(New(5, -1).Mul(zz)).Add(w) + } + if sign { + y = y.Neg() + } + return y +} + +var _tanP = [...]Decimal{ + NewFromFloat(-1.30936939181383777646e+4), // 0xc0c992d8d24f3f38 + NewFromFloat(1.15351664838587416140e+6), // 0x413199eca5fc9ddd + NewFromFloat(-1.79565251976484877988e+7), // 0xc1711fead3299176 +} +var _tanQ = [...]Decimal{ + NewFromFloat(1.00000000000000000000e+0), + NewFromFloat(1.36812963470692954678e+4), // 0x40cab8a5eeb36572 + NewFromFloat(-1.32089234440210967447e+6), // 0xc13427bc582abc96 + NewFromFloat(2.50083801823357915839e+7), // 0x4177d98fc2ead8ef + NewFromFloat(-5.38695755929454629881e+7), // 0xc189afe03cbe5a31 +} + +// Tan returns the tangent of the radian argument x. +func (d Decimal) Tan() Decimal { + + PI4A, PI4B, PI4C, M4PI := pi4A, pi4B, pi4C, m4PI + + if d.IsZero() { + return d + } + + // make argument positive but save the sign + sign := false + if d.IsNegative() { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(New(1, 0)) + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if zz.GreaterThan(New(1, -14)) { + w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2])) + x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4]) + y = z.Add(z.Mul(w.Div(x))) + } else { + y = z + } + if j&2 == 2 { + y = New(-1, 0).Div(y) + } + if sign { + y = y.Neg() + } + return y +} From d25658fa8de1d9ace7cc9f068f7469f9e44ec581 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 13:53:41 +0200 Subject: [PATCH 27/52] chore: lint with golangci-lint in CI instead of Go Report Card (#453) --- .github/workflows/ci.yml | 14 +++++++++++ .golangci.yml | 53 ++++++++++++++++++++++++++++++++++++++++ README.md | 1 - decimal.go | 5 ++-- decimal_test.go | 32 ++++++++++++------------ encoding.go | 1 + 6 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcce39b..8a02a04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: pull_request: branches: [ master ] types: [ opened, synchronize ] +permissions: + contents: read jobs: ci-job: runs-on: ubuntu-latest @@ -20,3 +22,15 @@ jobs: go-version: ${{ matrix.go }} - run: go build . - run: go test -v + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - uses: actions/checkout@v4 + - name: Use go + uses: actions/setup-go@v5 + with: + go-version: stable + - uses: golangci/golangci-lint-action@v9 + with: + version: v2.14.0 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..50e57bc --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,53 @@ +version: "2" + +# golangci-lint takes the Go version from go.mod (1.10), so linters that only suggest newer +# language features or APIs stay off: modernize, intrange, copyloopvar, and errorlint +# (errors.Is and %w need Go 1.13). + +linters: + default: standard # errcheck, govet, ineffassign, staticcheck, unused + enable: + - bidichk + - dupword + - gocritic + - makezero + - misspell + - nilerr + - nolintlint + - predeclared + - revive + - unconvert + - unparam + - wastedassign + # Off on purpose: + # - mnd: a math library is full of meaningful numeric constants. + # - gosec: its findings here are G115 integer conversions, which are deliberate and range checked. + # - funlen, gocyclo, gocognit, cyclop: numeric algorithms are long by nature. + settings: + govet: + enable-all: true + disable: + - fieldalignment # field order follows readability, not padding + - shadow # shadowing err and loop variables is common and harmless here + gocritic: + # Bug and performance checks only. The style checks are opinionated, and the experimental + # commentedOutCode check reads formula comments like "v * 10^(f-q) = e^s * 10^f" as code. + enabled-tags: + - diagnostic + - performance + disabled-tags: + - style + - experimental + - opinionated + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + +formatters: + enable: + - gofmt + - goimports + settings: + gofmt: + simplify: false # gofmt -s would rewrite many test tables that open pull requests also change diff --git a/README.md b/README.md index 318c9df..995e574 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![ci](https://github.com/shopspring/decimal/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/shopspring/decimal/actions/workflows/ci.yml) [![GoDoc](https://godoc.org/github.com/shopspring/decimal?status.svg)](https://godoc.org/github.com/shopspring/decimal) -[![Go Report Card](https://goreportcard.com/badge/github.com/shopspring/decimal)](https://goreportcard.com/report/github.com/shopspring/decimal) Arbitrary-precision fixed-point decimal numbers in go. diff --git a/decimal.go b/decimal.go index 4980cdd..b1aa884 100644 --- a/decimal.go +++ b/decimal.go @@ -865,6 +865,8 @@ func (d Decimal) Equal(d2 Decimal) bool { return d.Cmp(d2) == 0 } +// Equals returns whether the numbers represented by d and d2 are equal. +// // Deprecated: Equals is deprecated, please use Equal method instead. func (d Decimal) Equals(d2 Decimal) bool { return d.Equal(d2) @@ -1327,9 +1329,8 @@ func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { if d.exp >= 0 { if useScientificNotation { return d.ScientificNotationString() - } else { - return d.rescale(0).value.String() } + return d.rescale(0).value.String() } str := d.getValue().String() diff --git a/decimal_test.go b/decimal_test.go index bf82891..32368d4 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -263,11 +263,11 @@ func TestNewFromFormattedString(t *testing.T) { ReplRegex *regexp.Regexp }{ {"$10.99", "10.99", regexp.MustCompile("[$]")}, - {"$ 12.1", "12.1", regexp.MustCompile("[$\\s]")}, + {"$ 12.1", "12.1", regexp.MustCompile(`[$\s]`)}, {"$61,690.99", "61690.99", regexp.MustCompile("[$,]")}, {"1_000_000.00", "1000000.00", regexp.MustCompile("[_]")}, {"41,410.00", "41410.00", regexp.MustCompile("[,]")}, - {"5200 USD", "5200", regexp.MustCompile("[USD\\s]")}, + {"5200 USD", "5200", regexp.MustCompile(`[USD\s]`)}, } { dFormatted, err := NewFromFormattedString(testCase.Formatted, testCase.ReplRegex) if err != nil { @@ -517,16 +517,15 @@ func TestRequireFromString(t *testing.T) { func TestRequireFromStringErrs(t *testing.T) { s := "qwert" - var d Decimal var err interface{} - func(d Decimal) { + func() { defer func() { err = recover() }() RequireFromString(s) - }(d) + }() if err == nil { t.Errorf("panic expected when parsing %s", s) @@ -718,17 +717,18 @@ func TestNewFromBigRat(t *testing.T) { } tests := map[Inp]string{ - Inp{big.NewRat(0, 1), 16}: "0", - Inp{big.NewRat(4, 5), 16}: "0.8", - Inp{big.NewRat(10, 2), 16}: "5", - Inp{big.NewRat(1023427554493, 43432632), 16}: "23563.5628642767953828", // rounded - Inp{big.NewRat(1, 434324545566634), 16}: "0.0000000000000023", - Inp{big.NewRat(1, 3), 16}: "0.3333333333333333", - Inp{big.NewRat(2, 3), 2}: "0.67", // rounded - Inp{big.NewRat(2, 3), 16}: "0.6666666666666667", // rounded - Inp{big.NewRat(10000, 3), 16}: "3333.3333333333333333", - Inp{mustParseRat("30702832066636633479"), 16}: "30702832066636633479", - Inp{mustParseRat("487028320159896636679.1827512895753"), 16}: "487028320159896636679.1827512895753", + Inp{big.NewRat(0, 1), 16}: "0", + Inp{big.NewRat(4, 5), 16}: "0.8", + Inp{big.NewRat(10, 2), 16}: "5", + Inp{big.NewRat(1023427554493, 43432632), 16}: "23563.5628642767953828", // rounded + Inp{big.NewRat(1, 434324545566634), 16}: "0.0000000000000023", + Inp{big.NewRat(1, 3), 16}: "0.3333333333333333", + Inp{big.NewRat(2, 3), 2}: "0.67", // rounded + Inp{big.NewRat(2, 3), 16}: "0.6666666666666667", // rounded + Inp{big.NewRat(10000, 3), 16}: "3333.3333333333333333", + Inp{mustParseRat("30702832066636633479"), 16}: "30702832066636633479", + Inp{mustParseRat("487028320159896636679.1827512895753"), 16}: "487028320159896636679.1827512895753", + Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), -2}: "127028320612589896636633500", // rounded Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), 16}: "127028320612589896636633479.1735827512895753", // rounded Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), 32}: "127028320612589896636633479.173582751289575278357832", diff --git a/encoding.go b/encoding.go index 3239eac..792d9b1 100644 --- a/encoding.go +++ b/encoding.go @@ -175,6 +175,7 @@ type NullDecimal struct { Valid bool } +// NewNullDecimal returns a valid NullDecimal holding d. func NewNullDecimal(d Decimal) NullDecimal { return NullDecimal{ Decimal: d, From b2609858efe06dcfd25d4ecdec1a80531856d4d6 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 13:53:53 +0200 Subject: [PATCH 28/52] ci: test on Go 1.26 and 1.27 instead of 1.22 and 1.23 (#454) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a02a04..1d618bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.10.x', '1.22', '1.23', '1.24', '1.25', '1.x' ] + go: [ '1.10.x', '1.24', '1.25', '1.26', '1.27', '1.x' ] name: Go ${{ matrix.go }} steps: - uses: actions/checkout@v4 From f658fbf1eb94abc869558633a0b9b239300899f4 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Mon, 5 Oct 2026 14:11:08 +0200 Subject: [PATCH 29/52] docs: changelog for v1.5.0 (#455) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 432d0fd..0ba2325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +## Decimal v1.5.0 + +#### BEHAVIOR CHANGES +- `NewFromString` and `UnmarshalBinary`, and so JSON, text, SQL, gob and Spanner decoding, reject exponents larger than the new `MaxDecodeExponent` setting (default 10,000). Such input could hang the process or use gigabytes of memory. Raise `MaxDecodeExponent` if you need larger exponents [#440](https://github.com/shopspring/decimal/pull/440) +- `Pow`, `PowWithPrecision`, `PowInt32` and `PowBigInt` round correctly. Results for fractional exponents have `PowPrecisionNegativeExponent` (16) decimal places, and negative powers that used to return 0 keep 16 significant digits, so `10^-18` is `0.000000000000000001`. Results too large to represent return an error, or 0 from `Pow`, instead of hanging or panicking [#439](https://github.com/shopspring/decimal/pull/439) +- `RoundUp`, `RoundDown`, `RoundCeil` and `RoundFloor` always return exponent `-places`, like `Round`. Values don't change, but `Exponent()`, `Coefficient()` and `String()` with `TrimTrailingZeros = false` can [#426](https://github.com/shopspring/decimal/pull/426) [#441](https://github.com/shopspring/decimal/pull/441) +- `Truncate` supports negative precision: `5432` truncated to `-2` is `5400`. Before, it returned `5432` unchanged [#426](https://github.com/shopspring/decimal/pull/426) +- `Shift` panics when the exponent overflows an int32, instead of wrapping around [#437](https://github.com/shopspring/decimal/pull/437) +- `NewFromString` rejects a sign right after a leading decimal point, such as `".-5"` [#447](https://github.com/shopspring/decimal/pull/447) + +#### FEATURES +- Add `TrimTrailingZeros` and `UseScientificNotation` settings for string output [#389](https://github.com/shopspring/decimal/pull/389) [#416](https://github.com/shopspring/decimal/pull/416) +- Add Spanner support with `EncodeSpanner` and `DecodeSpanner` for `Decimal` and `NullDecimal` [#395](https://github.com/shopspring/decimal/pull/395) + +#### ENHANCEMENTS +- Most operations are faster and allocate less, about twice as fast on average across the benchmarks [#444](https://github.com/shopspring/decimal/pull/444) [#445](https://github.com/shopspring/decimal/pull/445) [#446](https://github.com/shopspring/decimal/pull/446) [#447](https://github.com/shopspring/decimal/pull/447) [#448](https://github.com/shopspring/decimal/pull/448) [#449](https://github.com/shopspring/decimal/pull/449) [#451](https://github.com/shopspring/decimal/pull/451) +- Faster `UnmarshalJSON`, `Scan` and `NewFromString` [#403](https://github.com/shopspring/decimal/pull/403) +- `ExpHullAbrham` raises its partial sum to a power by squaring. Large inputs that took minutes now take seconds [#450](https://github.com/shopspring/decimal/pull/450) +- Document that `NewFromFloat` returns the shortest decimal that converts back to the same float, and how to convert large whole numbers exactly [#442](https://github.com/shopspring/decimal/pull/442) +- Split `decimal.go` into `decimal.go`, `math.go` and `encoding.go` [#452](https://github.com/shopspring/decimal/pull/452) +- Lint with golangci-lint in CI, and test on Go 1.26 and 1.27 [#453](https://github.com/shopspring/decimal/pull/453) [#454](https://github.com/shopspring/decimal/pull/454) + +#### BUGFIXES +- Fix `Float64` hanging on decimals with huge exponents [#438](https://github.com/shopspring/decimal/pull/438) +- Fix `ExpHullAbrham` returning `10^exp` instead of 1 for tiny inputs [#433](https://github.com/shopspring/decimal/pull/433) +- Fix a data race in `ExpTaylor` [#414](https://github.com/shopspring/decimal/pull/414) +- Fix `NumDigits` for some exact powers of ten [#425](https://github.com/shopspring/decimal/pull/425) +- Fix the `omitzero` struct tag for `Decimal` fields in nested structs [#415](https://github.com/shopspring/decimal/pull/415) +- Fix `RoundCash` giving wrong results with a low `DivisionPrecision` [#443](https://github.com/shopspring/decimal/pull/443) +- `NullDecimal` is no longer `Valid` after a failed `Scan`, `UnmarshalJSON` or `DecodeSpanner` [#443](https://github.com/shopspring/decimal/pull/443) + ## Decimal v1.4.0 #### BREAKING - Drop support for Go version older than 1.10 [#361](https://github.com/shopspring/decimal/pull/361) From 970f0b7bae4017347f022cbd44f3031c7162a94b Mon Sep 17 00:00:00 2001 From: Archit Goyal Date: Sun, 11 Oct 2026 02:47:16 +0530 Subject: [PATCH 30/52] fix: accept Unicode minus in NewFromString (#428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some locales and copy-paste sources use U+2212 (−) instead of ASCII '-'. Normalize it before parsing so NewFromString accepts both. Closes #318 Co-authored-by: Archit Goyal Co-authored-by: mwoss --- decimal.go | 3 +++ decimal_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/decimal.go b/decimal.go index b1aa884..a0c0035 100644 --- a/decimal.go +++ b/decimal.go @@ -204,6 +204,9 @@ func NewFromString(value string) (Decimal, error) { originalInput := value var exp int64 + // Accept Unicode minus sign (U+2212) in addition to ASCII '-'. + value = strings.Replace(value, "\u2212", "-", -1) + // Check if number is using scientific notation and find dots eIndex := -1 pIndex := -1 diff --git a/decimal_test.go b/decimal_test.go index 32368d4..4836a7f 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -357,6 +357,26 @@ func TestFloat64OutOfRange(t *testing.T) { } } +func TestNewFromStringUnicodeMinus(t *testing.T) { + cases := []struct { + in, want string + }{ + {"\u2212123.45", "-123.45"}, + {"\u2212.5", "-0.5"}, + {"1e\u22122", "0.01"}, + } + for _, tc := range cases { + d, err := NewFromString(tc.in) + if err != nil { + t.Errorf("NewFromString(%q) unexpected error: %v", tc.in, err) + continue + } + if d.String() != tc.want { + t.Errorf("NewFromString(%q) = %q, want %q", tc.in, d.String(), tc.want) + } + } +} + func TestNewFromStringErrs(t *testing.T) { tests := []string{ "", From 511ed65a1218564ab137d858d9bc4e995322f206 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 10 Oct 2026 23:17:59 +0200 Subject: [PATCH 31/52] chore: turn off the strings.ReplaceAll lint check, it needs Go 1.12 (#458) --- .golangci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 50e57bc..794257a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,6 +29,17 @@ linters: disable: - fieldalignment # field order follows readability, not padding - shadow # shadowing err and loop variables is common and harmless here + staticcheck: + checks: + # golangci-lint's defaults + - all + - -ST1000 + - -ST1003 + - -ST1016 + - -ST1020 + - -ST1021 + - -ST1022 + - -QF1004 # suggests strings.ReplaceAll, which needs Go 1.12 gocritic: # Bug and performance checks only. The style checks are opinionated, and the experimental # commentedOutCode check reads formula comments like "v * 10^(f-q) = e^s * 10^f" as code. From e799c20820a0ac572bb26833847420fdeba007df Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Wed, 14 Oct 2026 19:27:42 +0200 Subject: [PATCH 32/52] feat: BSON Decimal128 support for the MongoDB Go driver v2 (#460) --- README.md | 1 + decimal.go | 3 +- decimal_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ encoding.go | 156 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 995e574..49621cf 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ _Note:_ Decimal library can "only" represent numbers with a maximum of 2^31 digi * Division with specified precision * Database/sql serialization/deserialization * JSON and XML serialization/deserialization + * MongoDB BSON serialization/deserialization as Decimal128, with the official Go driver v2 ## Install diff --git a/decimal.go b/decimal.go index a0c0035..cf2de6b 100644 --- a/decimal.go +++ b/decimal.go @@ -83,7 +83,8 @@ var UseScientificNotation = false var ExpMaxIterations = 1000 // MaxDecodeExponent is the largest exponent magnitude that NewFromString and UnmarshalBinary accept, -// and so UnmarshalJSON, UnmarshalText, Scan, GobDecode, DecodeSpanner and their NullDecimal variants too. +// and so UnmarshalJSON, UnmarshalText, Scan, GobDecode, DecodeSpanner, UnmarshalBSONValue and their +// NullDecimal variants too. // Operations like String, Add and Float64 take time and memory that grow with the exponent, so without // this limit a short input like "1e-2000000000" can stall the process or exhaust its memory. // The default covers the float64 and IEEE 754 decimal128 exponent ranges. diff --git a/decimal_test.go b/decimal_test.go index 4836a7f..6aae2ab 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2,6 +2,7 @@ package decimal import ( "database/sql/driver" + "encoding/hex" "encoding/json" "encoding/xml" "fmt" @@ -2741,6 +2742,142 @@ func TestDecimal_EncodeSpanner(t *testing.T) { } } +type bsonValueMarshaler interface { + MarshalBSONValue() (byte, []byte, error) +} + +type bsonValueUnmarshaler interface { + UnmarshalBSONValue(byte, []byte) error +} + +var ( + _ bsonValueMarshaler = Decimal{} + _ bsonValueMarshaler = NullDecimal{} + _ bsonValueUnmarshaler = (*Decimal)(nil) + _ bsonValueUnmarshaler = (*NullDecimal)(nil) +) + +// decimal128Tests are canonical values from the BSON spec's Decimal128 test corpus. +var decimal128Tests = []struct { + dec string + hex string // the 16 bytes as stored in BSON +}{ + {"0", "00000000000000000000000000004030"}, + {"1", "01000000000000000000000000004030"}, + {"-1", "010000000000000000000000000040b0"}, + {"0.1", "01000000000000000000000000003e30"}, + {"1E+3", "01000000000000000000000000004630"}, + {"1234567890123456789012345678901234", "f2af967ed05c82de3297ff6fde3c4030"}, + {"9.999999999999999999999999999999999E+6144", "ffffffff638e8d37c087adbe09edff5f"}, + {"1.000000000000000000000000000000000E+6144", "000000000a5bc138938d44c64d31fe5f"}, + {"1E-6176", "01000000000000000000000000000000"}, + {"-1E-6176", "01000000000000000000000000000080"}, + {"0E-6176", "00000000000000000000000000000000"}, + {"0E+6111", "0000000000000000000000000000fe5f"}, +} + +func mustDecodeHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil { + t.Fatal(err) + } + return b +} + +func TestDecimal_MarshalBSONValue(t *testing.T) { + type test struct { + name string + d Decimal + hex string + } + tests := []test{ + {"zero value", Decimal{}, "00000000000000000000000000004030"}, + {"41 digits drop trailing zeros", RequireFromString("1" + strings.Repeat("0", 40)), "000000000a5bc138938d44c64d314e30"}, + {"exponent above 6111 gains trailing zeros", RequireFromString("1E+6144"), "000000000a5bc138938d44c64d31fe5f"}, + {"exponent below -6176 drops trailing zeros", New(1000, -6179), "01000000000000000000000000000000"}, + {"zero with the largest exponent", New(0, math.MaxInt32), "0000000000000000000000000000fe5f"}, + {"zero with the smallest exponent", New(0, math.MinInt32), "00000000000000000000000000000000"}, + } + for _, c := range decimal128Tests { + tests = append(tests, test{c.dec, RequireFromString(c.dec), c.hex}) + } + for _, c := range tests { + typ, data, err := c.d.MarshalBSONValue() + if err != nil || typ != bsonTypeDecimal128 || hex.EncodeToString(data) != c.hex { + t.Errorf("MarshalBSONValue(%s): got %#x, %x, %v, want %#x, %s", c.name, typ, data, err, bsonTypeDecimal128, c.hex) + } + } + + for _, c := range []struct { + name string + d Decimal + }{ + {"35 significant digits", RequireFromString("1.2345678901234567890123456789012345")}, + {"below the smallest Decimal128", RequireFromString("1E-6177")}, + {"above the largest Decimal128", RequireFromString("1E+6145")}, + {"smallest int32 exponent", New(1, math.MinInt32)}, + } { + if _, _, err := c.d.MarshalBSONValue(); err == nil { + t.Errorf("MarshalBSONValue(%s): got no error", c.name) + } + } +} + +func TestDecimal_UnmarshalBSONValue(t *testing.T) { + type test struct { + name string + typ byte + hex string + want string + } + tests := []test{ + {"string", bsonTypeString, "04000000312e3500", "1.5"}, + {"double", bsonTypeDouble, "0000000000000440", "2.5"}, + {"int32", bsonTypeInt32, "f9ffffff", "-7"}, + {"int64", bsonTypeInt64, "0000000000010000", "1099511627776"}, + {"null", bsonTypeNull, "", "0"}, + {"undefined", bsonTypeUndefined, "", "0"}, + {"Decimal128 with an implied coefficient above 10^34 - 1", bsonTypeDecimal128, "ffffffffffffffffffffffffffff116c", "0E+3"}, + {"Decimal128 with the coefficient 10^34", bsonTypeDecimal128, "00000000648e8d37c087adbe09ed4130", "0"}, + } + for _, c := range decimal128Tests { + tests = append(tests, test{c.dec, bsonTypeDecimal128, c.hex, c.dec}) + } + for _, c := range tests { + want := RequireFromString(c.want) + got := New(5, 0) + err := got.UnmarshalBSONValue(c.typ, mustDecodeHex(t, c.hex)) + if err != nil || !got.Equal(want) || got.Exponent() != want.Exponent() { + t.Errorf("UnmarshalBSONValue(%s): got %se%d, %v, want %se%d", c.name, got.Coefficient(), got.Exponent(), err, want.Coefficient(), want.Exponent()) + } + } + + for _, c := range []struct { + name string + typ byte + hex string + }{ + {"Decimal128 NaN", bsonTypeDecimal128, "0000000000000000000000000000007c"}, + {"Decimal128 -Infinity", bsonTypeDecimal128, "000000000000000000000000000000f8"}, + {"Decimal128 of 15 bytes", bsonTypeDecimal128, "000000000000000000000000004030"}, + {"double NaN", bsonTypeDouble, "000000000000f87f"}, + {"double +Infinity", bsonTypeDouble, "000000000000f07f"}, + {"string abc", bsonTypeString, "0400000061626300"}, + {"boolean", 0x08, "01"}, + } { + if err := new(Decimal).UnmarshalBSONValue(c.typ, mustDecodeHex(t, c.hex)); err == nil { + t.Errorf("UnmarshalBSONValue(%s): got no error", c.name) + } + } + + defer func(limit int) { MaxDecodeExponent = limit }(MaxDecodeExponent) + MaxDecodeExponent = 100 + if err := new(Decimal).UnmarshalBSONValue(bsonTypeDecimal128, mustDecodeHex(t, "01000000000000000000000000000000")); err == nil { + t.Error("UnmarshalBSONValue(1E-6176): got no error with MaxDecodeExponent 100") + } +} + // old tests after this line func TestDecimal_Scale(t *testing.T) { @@ -3910,6 +4047,43 @@ func TestNullDecimal_EncodeSpanner(t *testing.T) { } } +func TestNullDecimal_MarshalBSONValue(t *testing.T) { + for _, c := range []struct { + d NullDecimal + typ byte + hex string + }{ + {NullDecimal{}, bsonTypeNull, ""}, + {NewNullDecimal(New(1, 3)), bsonTypeDecimal128, "01000000000000000000000000004630"}, + } { + typ, data, err := c.d.MarshalBSONValue() + if err != nil || typ != c.typ || hex.EncodeToString(data) != c.hex { + t.Errorf("MarshalBSONValue(%v): got %#x, %x, %v, want %#x, %s", c.d, typ, data, err, c.typ, c.hex) + } + } +} + +func TestNullDecimal_UnmarshalBSONValue(t *testing.T) { + for _, c := range []struct { + name string + typ byte + hex string + want string // "" if the result is invalid + wantErr bool + }{ + {"Decimal128", bsonTypeDecimal128, "01000000000000000000000000004630", "1000", false}, + {"null", bsonTypeNull, "", "", false}, + {"undefined", bsonTypeUndefined, "", "", false}, + {"boolean", 0x08, "01", "", true}, + } { + got := NewNullDecimal(New(5, 0)) + err := got.UnmarshalBSONValue(c.typ, mustDecodeHex(t, c.hex)) + if (err != nil) != c.wantErr || got.Valid != (c.want != "") || got.Valid && got.Decimal.String() != c.want { + t.Errorf("UnmarshalBSONValue(%s): got %v, %v, want %q and error=%v", c.name, got, err, c.want, c.wantErr) + } + } +} + func TestBinary(t *testing.T) { for _, y := range testTable { x := y.float diff --git a/encoding.go b/encoding.go index 792d9b1..b386c40 100644 --- a/encoding.go +++ b/encoding.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/binary" "fmt" + "math" "math/big" ) @@ -159,6 +160,144 @@ func (d Decimal) EncodeSpanner() (interface{}, error) { return d.String(), nil } +// BSON type numbers, see https://bsonspec.org/spec.html +const ( + bsonTypeDouble byte = 0x01 + bsonTypeString byte = 0x02 + bsonTypeUndefined byte = 0x06 + bsonTypeNull byte = 0x0A + bsonTypeInt32 byte = 0x10 + bsonTypeInt64 byte = 0x12 + bsonTypeDecimal128 byte = 0x13 +) + +// MarshalBSONValue implements the bson.ValueMarshaler interface of the MongoDB Go driver v2. It stores +// the decimal as a BSON Decimal128, MongoDB's decimal type, which holds 34 significant digits and +// exponents from -6176 to 6111. Trailing zeros move between the coefficient and the exponent to fit, so +// Exponent() can change in a round trip but the value can't. Decimals that still don't fit return an +// error instead of being rounded. +// +// The v1 driver declares this interface with its own bsontype.Type, so it doesn't call this method. +func (d Decimal) MarshalBSONValue() (byte, []byte, error) { + coef := new(big.Int).Abs(d.getValue()) + exp := d.exp + + // Zero fits at any exponent, and the loops below could take 2^31 steps to get there. + if coef.Sign() == 0 { + if exp < decimal128MinExp { + exp = decimal128MinExp + } else if exp > decimal128MaxExp { + exp = decimal128MaxExp + } + } + + quo, rem := new(big.Int), new(big.Int) + for (coef.Cmp(decimal128MaxCoef) > 0 || exp < decimal128MinExp) && exp < decimal128MaxExp { + if quo.QuoRem(coef, tenInt, rem); rem.Sign() != 0 { + break + } + coef, quo = quo, coef + exp++ + } + for exp > decimal128MaxExp && coef.Cmp(decimal128MaxCoef) <= 0 { + coef.Mul(coef, tenInt) + exp-- + } + if coef.Cmp(decimal128MaxCoef) > 0 || exp < decimal128MinExp { + return 0, nil, fmt.Errorf("can't encode %se%d as BSON Decimal128 without rounding: it holds 34 significant digits and exponents from %d to %d", + d.getValue(), d.exp, decimal128MinExp, decimal128MaxExp) + } + return bsonTypeDecimal128, encodeDecimal128(d.Sign() < 0, coef, exp), nil +} + +// UnmarshalBSONValue implements the bson.ValueUnmarshaler interface of the MongoDB Go driver v2. +// It decodes BSON Decimal128, string, double, int32 and int64 values. BSON null and undefined decode +// to zero, as they do for the driver's own number types. +func (d *Decimal) UnmarshalBSONValue(typ byte, data []byte) error { + var err error + switch { + case typ == bsonTypeNull || typ == bsonTypeUndefined: + *d = Decimal{} + case typ == bsonTypeDecimal128 && len(data) == 16: + *d, err = decodeDecimal128(data) + case typ == bsonTypeString && len(data) >= 5: + // int32 length, the string, then a NUL byte + *d, err = NewFromString(string(data[4 : len(data)-1])) + case typ == bsonTypeDouble && len(data) == 8: + f := math.Float64frombits(binary.LittleEndian.Uint64(data)) + if math.IsNaN(f) || math.IsInf(f, 0) { + return fmt.Errorf("can't convert BSON double %v to decimal", f) + } + *d = NewFromFloat(f) + case typ == bsonTypeInt32 && len(data) == 4: + *d = New(int64(int32(binary.LittleEndian.Uint32(data))), 0) + case typ == bsonTypeInt64 && len(data) == 8: + *d = New(int64(binary.LittleEndian.Uint64(data)), 0) + default: + return fmt.Errorf("can't convert BSON value of type 0x%02x and length %d to decimal", typ, len(data)) + } + return err +} + +// BSON stores a Decimal128 as a little-endian IEEE 754-2008 decimal128 in the binary integer decimal +// encoding: from the top bit down, a sign bit, a 14-bit exponent biased by 6176 and a 113-bit coefficient. +const ( + decimal128MinExp = -6176 + decimal128MaxExp = 6111 +) + +// decimal128MaxCoef is 10^34 - 1, the largest Decimal128 coefficient. +var decimal128MaxCoef = new(big.Int).Sub(new(big.Int).Exp(tenInt, big.NewInt(34), nil), oneInt) + +// encodeDecimal128 expects a coefficient and exponent that are already in range. +func encodeDecimal128(neg bool, coef *big.Int, exp int32) []byte { + var buf [16]byte + b := coef.Bytes() + copy(buf[16-len(b):], b) + hi := binary.BigEndian.Uint64(buf[:8]) | uint64(exp-decimal128MinExp)<<49 + if neg { + hi |= 1 << 63 + } + data := make([]byte, 16) + binary.LittleEndian.PutUint64(data, binary.BigEndian.Uint64(buf[8:])) + binary.LittleEndian.PutUint64(data[8:], hi) + return data +} + +func decodeDecimal128(data []byte) (Decimal, error) { + lo := binary.LittleEndian.Uint64(data) + hi := binary.LittleEndian.Uint64(data[8:]) + neg := hi>>63 == 1 + + var exp int32 + switch { + case hi>>59&0xF == 0xF: + return Decimal{}, fmt.Errorf("can't convert BSON Decimal128 NaN or Infinity to decimal") + case hi>>61&3 == 3: + // The exponent starts 2 bits lower, and the coefficient is always above 10^34 - 1. + exp, hi, lo = int32(hi>>47&0x3FFF), 0, 0 + default: + exp, hi = int32(hi>>49&0x3FFF), hi&(1<<49-1) + } + exp += decimal128MinExp + if int64(exp) > int64(MaxDecodeExponent) || int64(exp) < -int64(MaxDecodeExponent) { + return Decimal{}, fmt.Errorf("can't convert BSON Decimal128 to decimal: exponent %d exceeds MaxDecodeExponent (%d)", exp, MaxDecodeExponent) + } + + var buf [16]byte + binary.BigEndian.PutUint64(buf[:8], hi) + binary.BigEndian.PutUint64(buf[8:], lo) + value := new(big.Int).SetBytes(buf[:]) + // IEEE 754 reads coefficients above 10^34 - 1 as zero. + if value.Cmp(decimal128MaxCoef) > 0 { + value.SetInt64(0) + } + if neg { + value.Neg(value) + } + return Decimal{value: value, exp: exp}, nil +} + func unquoteIfQuoted(value string) string { // If the amount is quoted, strip the quotes if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { @@ -274,3 +413,20 @@ func (d NullDecimal) EncodeSpanner() (interface{}, error) { } return d.Decimal.String(), nil } + +// MarshalBSONValue implements the bson.ValueMarshaler interface of the MongoDB Go driver v2. +// An invalid NullDecimal is stored as BSON null, see Decimal.MarshalBSONValue for the rest. +func (d NullDecimal) MarshalBSONValue() (byte, []byte, error) { + if !d.Valid { + return bsonTypeNull, nil, nil + } + return d.Decimal.MarshalBSONValue() +} + +// UnmarshalBSONValue implements the bson.ValueUnmarshaler interface of the MongoDB Go driver v2. +// BSON null and undefined make the NullDecimal invalid, see Decimal.UnmarshalBSONValue for the rest. +func (d *NullDecimal) UnmarshalBSONValue(typ byte, data []byte) error { + err := d.Decimal.UnmarshalBSONValue(typ, data) + d.Valid = err == nil && typ != bsonTypeNull && typ != bsonTypeUndefined + return err +} From ec4943b1a3d82a9007325f3fe5d2c23f05bf85df Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Thu, 15 Oct 2026 20:05:04 +0200 Subject: [PATCH 33/52] feat: implement fmt.Formatter (#463) --- decimal.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ decimal_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/decimal.go b/decimal.go index cf2de6b..dea157a 100644 --- a/decimal.go +++ b/decimal.go @@ -18,6 +18,7 @@ package decimal import ( "fmt" + "io" "math" "math/big" "math/bits" @@ -1085,6 +1086,106 @@ func (d Decimal) StringFixedCash(interval uint8) string { return rounded.string(false, false) } +// Format implements fmt.Formatter, so decimals work with the fmt verbs: +// +// - %v and %s print String(), and %q, %x and %X quote it or print it in hex, as for any fmt.Stringer. +// - %f and %F print fixed-point notation. With a precision, as in %.2f, the decimal is rounded like +// StringFixed. Without one, it's printed exactly, like String() but never in scientific notation. +// Width and the '-', '+', ' ' and '0' flags work as they do for floats. +// - %#v prints Go syntax that recreates the decimal exactly, such as decimal.New(314, -2). +// +// Other verbs print fmt's usual error, such as %!d(decimal.Decimal=3.14). +func (d Decimal) Format(f fmt.State, verb rune) { + switch verb { + case 'v', 's', 'q', 'x', 'X': + if verb == 'v' && f.Flag('#') { + formatString(f, 's', d.goSyntax()) + } else { + formatString(f, verb, d.String()) + } + case 'f', 'F': + d.formatFixed(f) + default: + _, _ = fmt.Fprintf(f, "%%!%c(decimal.Decimal=%s)", verb, d.String()) + } +} + +// formatString formats s as fmt would for the directive that called Format, with verb in place of its own. +func formatString(f fmt.State, verb rune, s string) { + // Println and plain %v are the common case, and rebuilding the directive costs 3 allocations. + _, hasWidth := f.Width() + _, hasPrec := f.Precision() + if (verb == 'v' || verb == 's') && !hasWidth && !hasPrec { + _, _ = io.WriteString(f, s) + return + } + _, _ = fmt.Fprintf(f, fmtDirective(f, verb), s) +} + +// fmtDirective rebuilds the directive that called Format, as fmt.FormatString does from Go 1.20. +func fmtDirective(f fmt.State, verb rune) string { + b := []byte{'%'} + for _, flag := range "+-# 0" { + if f.Flag(int(flag)) { + b = append(b, byte(flag)) + } + } + if width, ok := f.Width(); ok { + b = strconv.AppendInt(b, int64(width), 10) + } + if prec, ok := f.Precision(); ok { + b = strconv.AppendInt(append(b, '.'), int64(prec), 10) + } + return string(append(b, string(verb)...)) +} + +func (d Decimal) formatFixed(f fmt.State) { + var s string + if prec, ok := f.Precision(); ok { + s = d.StringFixed(int32(prec)) + } else { + s = d.string(TrimTrailingZeros, false) + } + + sign := "" + switch { + case s[0] == '-': + sign, s = "-", s[1:] + case f.Flag('+'): + sign = "+" + case f.Flag(' '): + sign = " " + } + // Before Go 1.23, fmt keeps the width of an earlier directive when ok is false. + pad := 0 + if width, ok := f.Width(); ok { + pad = width - len(sign) - len(s) + } + switch { + case pad <= 0: + s = sign + s + case f.Flag('-'): + s = sign + s + strings.Repeat(" ", pad) + case f.Flag('0'): + s = sign + strings.Repeat("0", pad) + s + default: + s = strings.Repeat(" ", pad) + sign + s + } + _, _ = io.WriteString(f, s) +} + +func (d Decimal) goSyntax() string { + switch { + case d.value == nil: + return "decimal.Decimal{}" + case d.value.IsInt64(): + return fmt.Sprintf("decimal.New(%d, %d)", d.value.Int64(), d.exp) + default: + // Shift instead of an exponent in the string, which RequireFromString caps at MaxDecodeExponent. + return fmt.Sprintf("decimal.RequireFromString(%q).Shift(%d)", d.value.String(), d.exp) + } +} + // Round rounds the decimal to places decimal places. // If places < 0, it will round the integer part to the nearest 10^(-places). // diff --git a/decimal_test.go b/decimal_test.go index 6aae2ab..7b8ef4b 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4523,6 +4523,99 @@ func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) { } } +func TestDecimal_Format(t *testing.T) { + pi := RequireFromString("3.14159") + for i, c := range []struct { + format string + v interface{} + want string + }{ + {"%v", pi, "3.14159"}, + {"%s", pi, "3.14159"}, + {"%v", Decimal{}, "0"}, + {"%10v", pi, " 3.14159"}, + {"%-10s|", pi, "3.14159 |"}, + {"%.3s", pi, "3.1"}, + {"%q", pi, `"3.14159"`}, + {"%x", pi, "332e3134313539"}, + {"%v", &pi, "3.14159"}, + {"%v", (*Decimal)(nil), ""}, + {"%v", []Decimal{pi, pi.Neg()}, "[3.14159 -3.14159]"}, + {"%+v", NewNullDecimal(pi), "{Decimal:3.14159 Valid:true}"}, + + {"%f", RequireFromString("1.23456789"), "1.23456789"}, + {"%F", pi, "3.14159"}, + {"%f", New(150, -2), "1.5"}, + {"%f", New(12, 3), "12000"}, + {"%.2f", pi, "3.14"}, + {"%.0f", pi, "3"}, + {"%.7f", pi, "3.1415900"}, + {"%.1f", RequireFromString("2.25"), "2.3"}, + {"%.1f", RequireFromString("-2.25"), "-2.3"}, + {"%.2f", RequireFromString("-0.001"), "0.00"}, + {"%2.2f", pi, "3.14"}, + {"%8.2f", pi, " 3.14"}, + {"%-8.2f|", pi, "3.14 |"}, + {"%08.2f", pi, "00003.14"}, + {"%08.2f", pi.Neg(), "-0003.14"}, + {"%-08.2f|", pi, "3.14 |"}, + {"%+.2f", pi, "+3.14"}, + {"%+.2f", pi.Neg(), "-3.14"}, + {"%+08.2f", pi, "+0003.14"}, + {"% .2f", pi, " 3.14"}, + + {"%#v", New(314, -2), "decimal.New(314, -2)"}, + {"%#v", New(-1500, -3), "decimal.New(-1500, -3)"}, + {"%#v", Decimal{}, "decimal.Decimal{}"}, + {"%#v", New(1, math.MaxInt32), "decimal.New(1, 2147483647)"}, + {"%#v", RequireFromString("-12345678901234567890123456789012").Shift(-2), `decimal.RequireFromString("-12345678901234567890123456789012").Shift(-2)`}, + {"%#v", RequireFromString("12345678901234567890").Shift(20000), `decimal.RequireFromString("12345678901234567890").Shift(20000)`}, + {"%#v", NewNullDecimal(New(314, -2)), "decimal.NullDecimal{Decimal:decimal.New(314, -2), Valid:true}"}, + + {"%d", pi, "%!d(decimal.Decimal=3.14159)"}, + {"%e", pi, "%!e(decimal.Decimal=3.14159)"}, + } { + if got := fmt.Sprintf(c.format, c.v); got != c.want { + t.Errorf("Sprintf(%q) of case %d: got %q, want %q", c.format, i, got, c.want) + } + } + + for _, c := range []struct { + got, want string + }{ + {fmt.Sprintln(pi, pi.Neg()), "3.14159 -3.14159\n"}, + {fmt.Sprintf("%8d|%f|%.2f|", 1, pi, pi), " 1|3.14159|3.14|"}, + } { + if c.got != c.want { + t.Errorf("got %q, want %q", c.got, c.want) + } + } +} + +func TestDecimal_FormatWithStringSettings(t *testing.T) { + defer func() { + TrimTrailingZeros = true + UseScientificNotation = false + }() + TrimTrailingZeros = false + UseScientificNotation = true + + for _, c := range []struct { + format string + d Decimal + want string + }{ + {"%v", New(150, -2), "1.50"}, + {"%f", New(150, -2), "1.50"}, + {"%v", New(1, 3), "1E3"}, + {"%f", New(1, 3), "1000"}, + } { + if got := fmt.Sprintf(c.format, c.d); got != c.want { + t.Errorf("Sprintf(%q, %se%d): got %q, want %q", c.format, c.d.Coefficient(), c.d.Exponent(), got, c.want) + } + } +} + func TestDecimal_ScientificNotation(t *testing.T) { type testData struct { input string From c5d65cf1e967f711c0f870ecc00524ad70db2fe1 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Fri, 16 Oct 2026 14:43:08 +0200 Subject: [PATCH 34/52] feat: add Normalize method (#464) --- decimal.go | 42 ++++++++++++++++++++++++++++++++++++++++++ decimal_bench_test.go | 20 ++++++++++++++++++++ decimal_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/decimal.go b/decimal.go index dea157a..eaefdce 100644 --- a/decimal.go +++ b/decimal.go @@ -678,6 +678,48 @@ func (d Decimal) Shift(shift int32) Decimal { } } +// Normalize returns d with the trailing zeros removed from its coefficient, so equal decimals get the +// same coefficient and exponent: 1.50, 1.5 and New(15, -1) all become New(15, -1), and every zero +// becomes Decimal{}, the same as Zero. It's like Java's BigDecimal.stripTrailingZeros. +// +// reflect.DeepEqual, and test helpers built on it such as testify's assert.Equal and gomock.Eq, compare +// the coefficient and exponent rather than the value, so normalize decimals before comparing them that +// way. Equal and Cmp compare values and don't need it. +// +// The value doesn't change, but its string form can: with TrimTrailingZeros = false, 1.50 prints as +// 1.5, and with UseScientificNotation = true, 1500 prints as 1.5E3. +func (d Decimal) Normalize() Decimal { + v := d.getValue() + if v.Sign() == 0 { + return Decimal{} + } + + // Count the trailing zeros with integer math: in v if it fits an int64, else in v mod 10^19, + // and in all the digits of v only when those 19 are all zeros. + var zeros int64 + if v.IsInt64() { + for x := v.Int64(); x%10 == 0; x /= 10 { + zeros++ + } + } else { + r := new(big.Int).Rem(v, pow10(19)) + for n := r.Abs(r).Uint64(); n%10 == 0 && zeros < 19; n /= 10 { + zeros++ + } + if zeros == 19 { + s := v.String() + zeros = int64(len(s) - len(strings.TrimRight(s, "0"))) + } + } + if room := math.MaxInt32 - int64(d.exp); zeros > room { + zeros = room + } + if zeros == 0 { + return d + } + return Decimal{value: new(big.Int).Quo(v, pow10(zeros)), exp: d.exp + int32(zeros)} +} + // Div returns d / d2. If it doesn't divide exactly, the result will have // DivisionPrecision digits after the decimal point. func (d Decimal) Div(d2 Decimal) Decimal { diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 834040e..6e3fdcc 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -322,6 +322,26 @@ func BenchmarkDecimal_ExpTaylor(b *testing.B) { } } +func BenchmarkDecimal_Normalize(b *testing.B) { + for _, c := range []struct { + name string + d Decimal + }{ + {"NoZeros", RequireFromString("123.456")}, + {"OneZero", RequireFromString("12.50")}, + {"Div", NewFromInt(5).Div(NewFromInt(2))}, + {"Big", RequireFromString("123456789012345678901234567890.5000")}, + {"ManyZeros", RequireFromString("1.5").Round(10000)}, + } { + b.Run(c.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + c.d.Normalize() + } + }) + } +} + func BenchmarkDecimal_UnmarshalJSON(b *testing.B) { b.ResetTimer() diff --git a/decimal_test.go b/decimal_test.go index 7b8ef4b..ae6e925 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2005,6 +2005,49 @@ func TestDecimal_ShiftExponentOverflow(t *testing.T) { } } +func TestDecimal_Normalize(t *testing.T) { + for _, c := range []struct { + d Decimal + coef string + exp int32 + }{ + {New(123, -2), "123", -2}, + {New(100, 0), "1", 2}, + {New(100, -3), "1", -1}, + {New(-1500, -3), "-15", -1}, + {RequireFromString("2.5000000000000000"), "25", -1}, + {RequireFromString("-123456789012345678901234567891000"), "-123456789012345678901234567891", 3}, + {RequireFromString("12.500000000000000000"), "125", -1}, + {RequireFromString("1" + strings.Repeat("0", 40)), "1", 40}, + {New(100, math.MaxInt32-1), "10", math.MaxInt32}, + {New(10, math.MaxInt32), "10", math.MaxInt32}, + {NewFromBigInt(pow10(40), math.MaxInt32-30), "10000000000", math.MaxInt32}, + } { + coef, exp := c.d.Coefficient().String(), c.d.Exponent() + got := c.d.Normalize() + if got.Coefficient().String() != c.coef || got.Exponent() != c.exp { + t.Errorf("Normalize(%se%d): got %se%d, want %se%d", coef, exp, got.Coefficient(), got.Exponent(), c.coef, c.exp) + } + if c.d.Coefficient().String() != coef || c.d.Exponent() != exp { + t.Errorf("Normalize(%se%d) changed its receiver to %se%d", coef, exp, c.d.Coefficient(), c.d.Exponent()) + } + } + + one := NewFromInt(1) + for _, equal := range [][]Decimal{ + {Decimal{}, Zero, New(0, 0), NewFromFloat(0), RequireFromString("0.00"), one.Sub(one), RequireFromString("0.4").Round(0), RequireFromString("0.5").RoundDown(0)}, + {New(1, 1), RequireFromString("10"), NewFromFloat(10), RequireFromString("10.00"), NewFromInt(20).Div(NewFromInt(2)), RequireFromString("2.5").Mul(NewFromInt(4)), New(10000000000, -9)}, + {New(-15, -1), RequireFromString("-1.500"), NewFromFloat(-1.5), NewFromInt(-3).Div(NewFromInt(2))}, + } { + want := equal[0] + for _, d := range equal[1:] { + if got := d.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("Normalize(%se%d): got %#v, want %#v", d.Coefficient(), d.Exponent(), got, want) + } + } + } +} + func TestDecimal_Div(t *testing.T) { type Inp struct { a string From 2d97fa9ba3edac546d70fb5005b439bffdd1a32f Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Fri, 16 Oct 2026 18:21:15 +0200 Subject: [PATCH 35/52] feat: add Sqrt and SqrtRound (#465) --- decimal_bench_test.go | 20 +++++++++++++ decimal_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++ math.go | 39 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/decimal_bench_test.go b/decimal_bench_test.go index 6e3fdcc..a40dc51 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -342,6 +342,26 @@ func BenchmarkDecimal_Normalize(b *testing.B) { } } +func BenchmarkDecimal_Sqrt(b *testing.B) { + for _, c := range []struct { + name string + d Decimal + places int32 + }{ + {"Small", RequireFromString("0.002342"), 16}, + {"Int", NewFromInt(2), 16}, + {"Big", RequireFromString("123456789012345678901234567890.123"), 16}, + {"Places1000", NewFromInt(2), 1000}, + } { + b.Run(c.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = c.d.SqrtRound(c.places) + } + }) + } +} + func BenchmarkDecimal_UnmarshalJSON(b *testing.B) { b.ResetTimer() diff --git a/decimal_test.go b/decimal_test.go index ae6e925..dc90fd7 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3406,6 +3406,72 @@ func TestDecimal_PowBigInt_UndefinedResult(t *testing.T) { } } +func TestDecimal_SqrtRound(t *testing.T) { + for _, c := range []struct { + in string + places int32 + want string + }{ + {"0", 16, "0"}, + {"1", 16, "1"}, + {"4", 16, "2"}, + {"1.44", 16, "1.2"}, + {"3", 16, "1.7320508075688773"}, + {"4.5", 16, "2.1213203435596426"}, + {"0.002342", 16, "0.0483942145302514"}, + {"0.002342", 4, "0.0484"}, + {"0.002342", 32, "0.04839421453025144409653399922499"}, + {"3289854", 16, "1813.7954680724064484"}, + {"0.0025", 1, "0.1"}, + {"12345", -1, "110"}, + {"1e40", 0, "1e20"}, + {"1e-40", 16, "0"}, + {"1e-9000", 16, "0"}, + } { + got, err := RequireFromString(c.in).SqrtRound(c.places) + if want := RequireFromString(c.want); err != nil || !got.Equal(want) || got.Exponent() != -c.places { + t.Errorf("SqrtRound(%s, %d): got %s (exponent %d), %v, want %s", c.in, c.places, got, got.Exponent(), err, want) + } + } + + if got, err := New(1, math.MinInt32).SqrtRound(16); err != nil || !got.IsZero() { + t.Errorf("SqrtRound(1e%d, 16): got %s, %v, want 0", math.MinInt32, got, err) + } + for _, d := range []Decimal{NewFromInt(-1), RequireFromString("-0.0001")} { + if _, err := d.SqrtRound(16); err == nil { + t.Errorf("SqrtRound(%s, 16): got no error", d) + } + } + + // r is the correctly rounded root of d exactly when (r - u/2)^2 <= d < (r + u/2)^2, with u = 10^-places + r := rand.New(rand.NewSource(1)) + for i := 0; i < 2000; i++ { + d := NewFromBigInt(new(big.Int).Rand(r, pow10(int64(1+r.Intn(40)))), int32(r.Intn(80)-50)) + places := int32(r.Intn(46) - 5) + got, err := d.SqrtRound(places) + half := New(5, -places-1) + lo, hi := got.Sub(half), got.Add(half) + if err != nil || got.Sign() > 0 && lo.Mul(lo).GreaterThan(d) || hi.Mul(hi).LessThanOrEqual(d) { + t.Fatalf("SqrtRound(%s, %d): got %s, %v, which isn't the correctly rounded root", d, places, got, err) + } + } +} + +func TestDecimal_Sqrt(t *testing.T) { + if got, err := NewFromInt(2).Sqrt(); err != nil || got.String() != "1.414213562373095" || got.Exponent() != -16 { + t.Errorf("Sqrt(2): got %s (exponent %d), %v", got, got.Exponent(), err) + } + + defer func(prec int) { DivisionPrecision = prec }(DivisionPrecision) + DivisionPrecision = 4 + if got, err := NewFromInt(2).Sqrt(); err != nil || got.String() != "1.4142" { + t.Errorf("Sqrt(2) with DivisionPrecision 4: got %s, %v", got, err) + } + if _, err := NewFromInt(-4).Sqrt(); err == nil { + t.Error("Sqrt(-4): got no error") + } +} + func TestDecimal_IsInteger(t *testing.T) { for _, testCase := range []struct { Dec string diff --git a/math.go b/math.go index 6100afb..9317abe 100644 --- a/math.go +++ b/math.go @@ -597,6 +597,45 @@ func abs64(n int64) int64 { return n } +// Sqrt returns the square root of d with DivisionPrecision digits after the decimal point, like Div. +// It returns an error for negative d. +// +// Example: +// +// d, err := NewFromInt(2).Sqrt() +// d.String() // output: "1.414213562373095" +func (d Decimal) Sqrt() (Decimal, error) { + return d.SqrtRound(int32(DivisionPrecision)) +} + +// SqrtRound returns the square root of d rounded half away from zero to places digits after the +// decimal point, like DivRound. The result is exact up to that rounding. It returns an error for +// negative d. +// +// Example: +// +// d, err := NewFromFloat(0.002342).SqrtRound(4) +// d.String() // output: "0.0484" +func (d Decimal) SqrtRound(places int32) (Decimal, error) { + if d.Sign() < 0 { + return Decimal{}, fmt.Errorf("cannot represent imaginary value of sqrt(x), where x < 0") + } + + // floor(sqrt(d) * 10^(places+1)) is the integer square root of d * 10^(2*places+2), and its + // last digit decides the rounding. Below 1 that product truncates to an integer first, which + // doesn't change the floor, and when it's below 1 the result is 0. + k := int64(d.exp) + 2*int64(places) + 2 + n := new(big.Int) + if k >= 0 { + n.Mul(d.getValue(), pow10(k)) + } else if -k <= int64(d.NumDigits()) { + n.Quo(d.getValue(), pow10(-k)) + } + n.Sqrt(n) + n.Add(n, fiveInt).Quo(n, tenInt) + return Decimal{value: n, exp: -places}, nil +} + // ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. // OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). // From 0146aff76d1a3e1a06866658022434b060cc2269 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Fri, 16 Oct 2026 22:52:22 +0200 Subject: [PATCH 36/52] docs: warn that global settings are not goroutine-safe, list them in the README (#466) --- README.md | 18 ++++++++++++++++++ decimal.go | 24 +++++++++++++++++++++--- math.go | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49621cf..2a81974 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Arbitrary-precision fixed-point decimal numbers in go. _Note:_ Decimal library can "only" represent numbers with a maximum of 2^31 digits after the decimal point. +Results that aren't exact, like `Div` and `Sqrt`, are rounded to 16 decimal places by default, and decoded input is +limited to exponents up to 10,000. See [Global settings](#global-settings). ## Features @@ -64,6 +66,22 @@ func main() { } ``` +## Global settings + +These package-level variables change how some functions behave: + +| Variable | Default | What it changes | Per-call alternative | +|---|---|---|---| +| `DivisionPrecision` | `16` | Decimal places in `Div` and `Sqrt` results, and in `Avg`, `Atan` and `Tan` | `DivRound`, `SqrtRound` | +| `PowPrecisionNegativeExponent` | `16` | Decimal places in `Pow`, `PowInt32` and `PowBigInt` results for negative or fractional exponents | `PowWithPrecision` | +| `TrimTrailingZeros` | `true` | Whether `String`, and so JSON, text and SQL output, drops trailing zeros: `2.50` prints as `2.5` | `StringFixed` | +| `UseScientificNotation` | `false` | Whether `String` prints numbers stored with a positive exponent in scientific notation, like `1.2E3` | `StringFixed` | +| `MarshalJSONWithoutQuotes` | `false` | Whether JSON output is a number instead of a string | | +| `ExpMaxIterations` | `1000` | Iteration limit of `ExpHullAbrham` | | +| `MaxDecodeExponent` | `10000` | Largest exponent size that `NewFromString` and the decoders accept | | + +Set them once at program start. Changing one while other goroutines use the library is a data race. + ## Alternative libraries When working with decimal numbers, you might face problems this library is not perfectly suited for. diff --git a/decimal.go b/decimal.go index eaefdce..f8cb0da 100644 --- a/decimal.go +++ b/decimal.go @@ -28,7 +28,10 @@ import ( ) // DivisionPrecision is the number of decimal places in the result when it -// doesn't divide exactly. +// doesn't divide exactly. Div and Sqrt read it, and so do Avg, Atan and Tan through Div. +// +// Set it once at program start. Changing it while other goroutines use the package is a data race. +// To choose the precision per call, use DivRound instead of Div and SqrtRound instead of Sqrt. // // Example: // @@ -48,6 +51,9 @@ var DivisionPrecision = 16 // PowPrecisionNegativeExponent significant digits instead (at least one). // PowWithPrecision is not affected, it takes the precision as an argument. // +// Set it once at program start. Changing it while other goroutines use the package is a data race. +// To choose the precision per call, use PowWithPrecision. +// // Example: // // d1, err := decimal.NewFromFloat(15.2).PowInt32(-2) @@ -64,12 +70,18 @@ var PowPrecisionNegativeExponent = 16 // unmarshallers (ex: Javascript's) will unmarshal JSON numbers to IEEE 754 // double-precision floating point numbers, which means you can potentially // silently lose precision. +// +// Set it once at program start. Changing it while other goroutines use the package is a data race. var MarshalJSONWithoutQuotes = false // TrimTrailingZeros specifies whether trailing zeroes should be trimmed from a string representation of decimal. // If set to true, trailing zeroes will be truncated (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13), // otherwise trailing zeroes will be preserved (2.00 -> 2.00, 3.11 -> 3.11, 13.000 -> 13.000). // Setting this value to false can be useful for APIs where exact decimal string representation matters. +// It affects String and everything built on it, such as MarshalJSON, MarshalText and Value. +// +// Set it once at program start. Changing it while other goroutines use the package is a data race. +// For a fixed number of decimal places per call, use StringFixed. var TrimTrailingZeros = true // UseScientificNotation specifies whether scientific notation should be used when a decimal is turned @@ -77,10 +89,15 @@ var TrimTrailingZeros = true // // For example, 1200 rounded to the nearest 100 cannot accurately be shown as "1200" because the last two // digits are unknown. With this set to true, that number would be expressed as "1.2E3" instead. +// +// Set it once at program start. Changing it while other goroutines use the package is a data race. +// StringFixed never uses scientific notation. var UseScientificNotation = false // ExpMaxIterations specifies the maximum number of iterations needed to calculate // precise natural exponent value using ExpHullAbrham method. +// +// Set it once at program start. Changing it while other goroutines use the package is a data race. var ExpMaxIterations = 1000 // MaxDecodeExponent is the largest exponent magnitude that NewFromString and UnmarshalBinary accept, @@ -94,7 +111,8 @@ var ExpMaxIterations = 1000 // their own String or MarshalBinary output. The limit does not bound the cost of Pow or ExpTaylor // arguments or of very long inputs; validate those separately. // -// Set it once at program start, before any decoding. Raise it only for trusted input. +// Set it once at program start, before any decoding. Changing it while other goroutines decode is a +// data race. Raise it only for trusted input. var MaxDecodeExponent = 10000 // Zero constant, to make computations faster. @@ -721,7 +739,7 @@ func (d Decimal) Normalize() Decimal { } // Div returns d / d2. If it doesn't divide exactly, the result will have -// DivisionPrecision digits after the decimal point. +// DivisionPrecision digits after the decimal point. Use DivRound to choose the precision per call. func (d Decimal) Div(d2 Decimal) Decimal { return d.DivRound(d2, int32(DivisionPrecision)) } diff --git a/math.go b/math.go index 9317abe..9867577 100644 --- a/math.go +++ b/math.go @@ -598,7 +598,7 @@ func abs64(n int64) int64 { } // Sqrt returns the square root of d with DivisionPrecision digits after the decimal point, like Div. -// It returns an error for negative d. +// Use SqrtRound to choose the precision per call. It returns an error for negative d. // // Example: // From 1863390911dd3e2ac7575662be6c920cae12a085 Mon Sep 17 00:00:00 2001 From: cdpzyafk <55048345+cdpzyafk@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:15:40 +0800 Subject: [PATCH 37/52] perf: use Sign() instead of Cmp(zeroInt) in IsInteger and Ceil Co-authored-by: terry Co-authored-by: mwoss --- decimal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decimal.go b/decimal.go index f8cb0da..e98d7bb 100644 --- a/decimal.go +++ b/decimal.go @@ -876,7 +876,7 @@ func (d Decimal) IsInteger() bool { q := new(big.Int).Set(v) for z := abs(d.exp); z > 0; z-- { q.QuoRem(q, tenInt, &r) - if r.Cmp(zeroInt) != 0 { + if r.Sign() != 0 { return false } } @@ -1456,7 +1456,7 @@ func (d Decimal) Ceil() Decimal { // NOTE(vadim): must negate after casting to prevent int32 overflow z, m := new(big.Int).DivMod(d.getValue(), pow10(-int64(d.exp)), new(big.Int)) - if m.Cmp(zeroInt) != 0 { + if m.Sign() != 0 { z.Add(z, oneInt) } return Decimal{value: z, exp: 0} From 175974ac6bc103360c3ecb890f50d4c397d7c4fe Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 17 Oct 2026 20:36:28 +0200 Subject: [PATCH 38/52] test: fix benchmarks that measured setup or the wrong function (#467) Co-authored-by: Igor Mikushkin --- decimal_bench_test.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/decimal_bench_test.go b/decimal_bench_test.go index a40dc51..06f0bb9 100644 --- a/decimal_bench_test.go +++ b/decimal_bench_test.go @@ -23,10 +23,9 @@ func BenchmarkNewFromFloatWithExponent(b *testing.B) { in[i] = rng.NormFloat64() * 10e20 } b.ReportAllocs() - b.StartTimer() + b.ResetTimer() for i := 0; i < b.N; i++ { - in := rng.NormFloat64() * 10e20 - _ = NewFromFloatWithExponent(in, math.MinInt32) + _ = NewFromFloatWithExponent(in[i], math.MinInt32) } } @@ -37,7 +36,7 @@ func BenchmarkNewFromFloat(b *testing.B) { in[i] = rng.NormFloat64() * 10e20 } b.ReportAllocs() - b.StartTimer() + b.ResetTimer() for i := 0; i < b.N; i++ { _ = NewFromFloat(in[i]) } @@ -45,15 +44,14 @@ func BenchmarkNewFromFloat(b *testing.B) { func BenchmarkNewFromStringFloat(b *testing.B) { rng := rand.New(rand.NewSource(0xdead1337)) - in := make([]float64, b.N) + in := make([]string, b.N) for i := range in { - in[i] = rng.NormFloat64() * 10e20 + in[i] = strconv.FormatFloat(rng.NormFloat64()*10e20, 'f', -1, 64) } b.ReportAllocs() - b.StartTimer() + b.ResetTimer() for i := 0; i < b.N; i++ { - in := strconv.FormatFloat(in[i], 'f', -1, 64) - _, _ = NewFromString(in) + _, _ = NewFromString(in[i]) } } @@ -200,7 +198,7 @@ func BenchmarkDecimal_Sub_same_precision(b *testing.B) { b.ReportAllocs() b.StartTimer() for i := 0; i < b.N; i++ { - d1.Add(d2) + d1.Sub(d2) } } From 9e64f2e1881b162a3e955804a445d4ff1bb153b2 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 17 Oct 2026 20:47:30 +0200 Subject: [PATCH 39/52] docs: fix doc examples that were wrong or didn't compile (#468) Co-authored-by: Takamura --- decimal.go | 20 ++++++++++---------- math.go | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/decimal.go b/decimal.go index e98d7bb..8da3031 100644 --- a/decimal.go +++ b/decimal.go @@ -558,18 +558,18 @@ func (d Decimal) Copy() Decimal { // // Example: // -// d := New(12345, -4) -// d2 := d.rescale(-1) +// d1 := New(12345, -4) +// d2 := d1.rescale(-1) // d3 := d2.rescale(-4) -// println(d1) -// println(d2) -// println(d3) +// fmt.Println(d1) +// fmt.Println(d2) +// fmt.Println(d3) // // Output: // // 1.2345 // 1.2 -// 1.2000 +// 1.2 func (d Decimal) rescale(exp int32) Decimal { if d.exp == exp { return Decimal{ @@ -1110,7 +1110,7 @@ func (d Decimal) String() string { // NewFromFloat(5.45).StringFixed(1) // output: "5.5" // NewFromFloat(5.45).StringFixed(2) // output: "5.45" // NewFromFloat(5.45).StringFixed(3) // output: "5.450" -// NewFromFloat(545).StringFixed(-1) // output: "540" +// NewFromFloat(545).StringFixed(-1) // output: "550" // // Regardless of the UseScientificNotation option, the returned string will never be in scientific notation. func (d Decimal) StringFixed(places int32) string { @@ -1470,9 +1470,9 @@ func (d Decimal) Ceil() Decimal { // // Example: // -// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" -// decimal.NewFromString("5432").Truncate(-2).String() // "5400" -// decimal.NewFromString("-5432").Truncate(-2).String() // "-5400" +// decimal.RequireFromString("123.456").Truncate(2).String() // output: "123.45" +// decimal.RequireFromString("5432").Truncate(-2).String() // output: "5400" +// decimal.RequireFromString("-5432").Truncate(-2).String() // output: "-5400" func (d Decimal) Truncate(precision int32) Decimal { if -precision > d.exp { return d.rescale(-precision) diff --git a/math.go b/math.go index 9867577..f56be71 100644 --- a/math.go +++ b/math.go @@ -643,8 +643,11 @@ func (d Decimal) SqrtRound(places int32) (Decimal, error) { // // Example: // -// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000" -// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284" +// d1, err := NewFromFloat(26.1).ExpHullAbrham(2) +// d1.String() // output: "220000000000" +// +// d2, err := NewFromFloat(26.1).ExpHullAbrham(20) +// d2.String() // output: "216314672147.05767284" func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { // Algorithm based on Variable precision exponential function. // ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham. @@ -736,14 +739,14 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { // // Example: // -// d, err := NewFromFloat(26.1).ExpTaylor(2).String() -// d.String() // output: "216314672147.06" +// d1, err := NewFromFloat(26.1).ExpTaylor(2) +// d1.String() // output: "216314672147.06" // -// NewFromFloat(26.1).ExpTaylor(20).String() -// d.String() // output: "216314672147.05767284062928674083" +// d2, err := NewFromFloat(26.1).ExpTaylor(20) +// d2.String() // output: "216314672147.05767284062928674083" // -// NewFromFloat(26.1).ExpTaylor(-10).String() -// d.String() // output: "220000000000" +// d3, err := NewFromFloat(26.1).ExpTaylor(-10) +// d3.String() // output: "220000000000" func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only if d.IsZero() { From 5bc569143a2eee93c9c7c04e7e1b9e9ad88a624b Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 17 Oct 2026 21:00:54 +0200 Subject: [PATCH 40/52] feat: Scan accepts Decimal, big.Int and all integer sizes (#469) Co-authored-by: Pavel Skorikov <5002158+it66ru@users.noreply.github.com> Co-authored-by: Lucca Dukic <109136188+LuccaBitfly@users.noreply.github.com> Co-authored-by: Anucha Petchagun Co-authored-by: agammann <161159040+agammann@users.noreply.github.com> --- decimal_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- encoding.go | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/decimal_test.go b/decimal_test.go index dc90fd7..337cb9c 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2663,7 +2663,7 @@ func TestDecimal_Scan(t *testing.T) { // apparently MySQL 5.7.16 and returns these as float32 so we need // to handle these as well dbvalueFloat32 := float32(54.33) - expected = NewFromFloat(float64(dbvalueFloat32)) + expected = RequireFromString("54.33") scanHelper(t, dbvalueFloat32, expected) // at least SQLite returns an int64 when 0 is stored in the db @@ -2696,6 +2696,50 @@ func TestDecimal_Scan(t *testing.T) { } } +func TestDecimal_ScanTypes(t *testing.T) { + int128, _ := new(big.Int).SetString("170141183460469231731687303715884105727", 10) + for _, c := range []struct { + value interface{} + want string + }{ + {int(-3264), "-3264"}, + {int8(-7), "-7"}, + {int16(300), "300"}, + {int32(-70000), "-70000"}, + {int64(math.MinInt64), "-9223372036854775808"}, + {uint(3264), "3264"}, + {uint8(200), "200"}, + {uint16(65535), "65535"}, + {uint32(4000000000), "4000000000"}, + {uint64(math.MaxUint64), "18446744073709551615"}, + {float32(-0.1), "-0.1"}, + {float32(math.MaxFloat32), strconv.FormatFloat(math.MaxFloat32, 'f', -1, 32)}, + {float32(math.SmallestNonzeroFloat32), strconv.FormatFloat(math.SmallestNonzeroFloat32, 'f', -1, 32)}, + {*int128, "170141183460469231731687303715884105727"}, + {int128, "170141183460469231731687303715884105727"}, + {New(2, 3), "2000"}, + } { + var d Decimal + if err := d.Scan(c.value); err != nil || d.String() != c.want { + t.Errorf("Scan(%T %v): got %s, %v, want %s", c.value, c.value, d, err, c.want) + } + var n NullDecimal + if err := n.Scan(c.value); err != nil || !n.Valid || n.Decimal.String() != c.want { + t.Errorf("NullDecimal.Scan(%T %v): got %v, %v, want %s", c.value, c.value, n, err, c.want) + } + } + if int128.String() != "170141183460469231731687303715884105727" { + t.Errorf("Scan changed the scanned *big.Int to %s", int128) + } + + for _, v := range []interface{}{(*big.Int)(nil), math.NaN(), math.Inf(1), float32(math.Inf(-1))} { + var n NullDecimal + if err := n.Scan(v); err == nil || n.Valid { + t.Errorf("NullDecimal.Scan(%T %v): got %v, %v, want an error", v, v, n, err) + } + } +} + func TestDecimal_Value(t *testing.T) { // Make sure this does implement the database/sql's driver.Valuer interface var d Decimal diff --git a/encoding.go b/encoding.go index b386c40..c333af9 100644 --- a/encoding.go +++ b/encoding.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "math/big" + "reflect" ) // UnmarshalJSON implements the json.Unmarshaler interface. @@ -81,11 +82,17 @@ func (d *Decimal) Scan(value interface{}) error { switch v := value.(type) { case float32: - *d = NewFromFloat(float64(v)) + if math.IsNaN(float64(v)) || math.IsInf(float64(v), 0) { + return fmt.Errorf("could not convert value '%v' to decimal", v) + } + *d = NewFromFloat32(v) return nil case float64: // numeric in sqlite3 sends us float64 + if math.IsNaN(v) || math.IsInf(v, 0) { + return fmt.Errorf("could not convert value '%v' to decimal", v) + } *d = NewFromFloat(v) return nil @@ -100,6 +107,31 @@ func (d *Decimal) Scan(value interface{}) error { *d = NewFromUint64(v) return nil + case int, int8, int16, int32: + // clickhouse and test mocks send the other integer sizes + *d = New(reflect.ValueOf(v).Int(), 0) + return nil + + case uint, uint8, uint16, uint32: + *d = NewFromUint64(reflect.ValueOf(v).Uint()) + return nil + + case big.Int: + // clickhouse sends Int128, UInt128 and Int256 through database/sql as big.Int + *d = NewFromBigInt(&v, 0) + return nil + + case *big.Int: + if v != nil { + *d = NewFromBigInt(v, 0) + return nil + } + + case Decimal: + // clickhouse sends Decimal columns through its native API as Decimal + *d = v + return nil + case string: var err error *d, err = NewFromString(unquoteIfQuoted(v)) @@ -109,10 +141,8 @@ func (d *Decimal) Scan(value interface{}) error { var err error *d, err = NewFromString(unquoteIfQuoted(string(v))) return err - - default: - return fmt.Errorf("could not convert value '%+v' to any known type", value) } + return fmt.Errorf("could not convert value '%+v' to any known type", value) } // Value implements the driver.Valuer interface for database serialization. From 77f8f075c2c97e98bf64eab709792dff5e9e9487 Mon Sep 17 00:00:00 2001 From: Bing Gao Date: Sat, 17 Oct 2026 21:30:33 +0200 Subject: [PATCH 41/52] fix: ExpHullAbrham drops significant figures for results below 1 (#429) ExpHullAbrham is documented to return overallPrecision significant figures, but its final rounding used a separate branch when the result was < 1 and rounded to overallPrecision decimal places instead. That discarded the leading-zero positions, so results below 0.1 lost significant figures and collapsed to 0 once the leading-zero count reached overallPrecision -- e.g. exp(-50).ExpHullAbrham(10) returned 0 (true value 1.93e-22). The >=1 branch formula currentPrecision - resNumDigits - res.exp is the significant-figure-correct rounding position in both cases, so drop the branch. Positive arguments and results in [0.1,1) are unchanged. Correct the negative rows of the ExpHullAbrham table (they encoded the truncated values) and add a significant-figure sweep over the negative half-line, checked against exp(x) to 40+ digits. Co-authored-by: mwoss --- decimal_test.go | 57 ++++++++++++++++++++++++++++++++++++++++--------- math.go | 11 ++++------ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/decimal_test.go b/decimal_test.go index 337cb9c..9a5e99a 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3587,17 +3587,17 @@ func TestDecimal_ExpHullAbrham(t *testing.T) { {"-0.569297", 20, "0.56592314285957604443"}, {"-1.0", 1, "0.4"}, {"-1.0", 5, "0.36788"}, - {"-3.0", 1, "0"}, + {"-3.0", 1, "0.05"}, {"-3.0", 2, "0.05"}, - {"-3.0", 10, "0.0497870684"}, - {"-5.26", 2, "0.01"}, - {"-5.26", 10, "0.0051953047"}, - {"-5.2663117716", 2, "0.01"}, - {"-5.2663117716", 10, "0.0051626164"}, - {"-26.1", 2, "0"}, - {"-26.1", 15, "0.000000000004623"}, - {"-50.1591", 10, "0"}, - {"-50.1591", 30, "0.000000000000000000000164505208"}, + {"-3.0", 10, "0.04978706837"}, + {"-5.26", 2, "0.0052"}, + {"-5.26", 10, "0.005195304719"}, + {"-5.2663117716", 2, "0.0052"}, + {"-5.2663117716", 10, "0.005162616411"}, + {"-26.1", 2, "0.0000000000046"}, + {"-26.1", 15, "0.00000000000462289492466867"}, + {"-50.1591", 10, "0.0000000000000000000001645052084"}, + {"-50.1591", 30, "0.000000000000000000000164505208424152908216198963722"}, } { d, _ := NewFromString(testCase.Dec) expected, _ := NewFromString(testCase.ExpectedDec) @@ -3614,6 +3614,43 @@ func TestDecimal_ExpHullAbrham(t *testing.T) { } } +// ExpHullAbrham is documented to return overallPrecision significant figures. +// It used to round the result to overallPrecision decimal places whenever the +// result was < 1, discarding the leading-zero positions, so every sufficiently +// negative argument lost significant figures (exp(-50).ExpHullAbrham(10) +// returned 0). Check the delivered precision against exp(x) computed to 40 +// digits with mpmath, across the negative half-line. +func TestDecimal_ExpHullAbrham_significantFigures(t *testing.T) { + trueExp := map[string]string{ + "-2.5": "8.20849986238987951695286744671598078378041e-2", + "-5": "6.73794699908546709663604842314842424884959e-3", + "-10": "4.53999297624848515355915155605506102379181e-5", + "-20": "2.06115362243855782796594038015582097637581e-9", + "-30": "9.35762296884017460491583222337870674495832e-14", + "-50": "1.92874984796391778301734281652701257475283e-22", + } + for _, x := range []string{"-2.5", "-5", "-10", "-20", "-30", "-50"} { + d := RequireFromString(x) + want := RequireFromString(trueExp[x]) + for _, prec := range []uint32{10, 20, 30} { + got, err := d.ExpHullAbrham(prec) + if err != nil { + t.Fatalf("exp(%s).ExpHullAbrham(%d): %v", x, prec, err) + } + if got.IsZero() { + t.Errorf("exp(%s).ExpHullAbrham(%d) = 0, want ~%s", x, prec, want) + continue + } + relErr := got.Sub(want).Abs().DivRound(want, 45) + tol := New(1, -int32(prec)+1) // one unit in the prec-th significant figure + if relErr.Cmp(tol) > 0 { + t.Errorf("exp(%s).ExpHullAbrham(%d) = %s: relative error %s > %s, fewer than %d significant figures", + x, prec, got, relErr, tol, prec) + } + } + } +} + func TestDecimal_ExpTaylor(t *testing.T) { for _, testCase := range []struct { Dec string diff --git a/math.go b/math.go index f56be71..11f07da 100644 --- a/math.go +++ b/math.go @@ -717,14 +717,11 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { } res := Decimal{new(big.Int).Exp(sum.getValue(), big.NewInt(ki), nil), int32(expInt64)} + // Round to overallPrecision significant figures. This holds whether res is + // >=1 or <1; the old <1 branch rounded to a fixed number of decimal places, + // dropping leading zeros (so e.g. exp(-50).ExpHullAbrham(10) returned 0). resNumDigits := int32(res.NumDigits()) - - var roundDigits int32 - if resNumDigits > abs(res.exp) { - roundDigits = int32(currentPrecision) - resNumDigits - res.exp - } else { - roundDigits = int32(currentPrecision) - } + roundDigits := int32(currentPrecision) - resNumDigits - res.exp res = res.Round(roundDigits) From 4fef1ec699ebf19e65c305d8dc5ea8ef92e8a55f Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 17 Oct 2026 21:32:15 +0200 Subject: [PATCH 42/52] fix: ExpHullAbrham returns the requested number of significant figures for large inputs (#470) --- decimal_test.go | 3 +++ math.go | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/decimal_test.go b/decimal_test.go index 9a5e99a..6f9c5ed 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3598,6 +3598,9 @@ func TestDecimal_ExpHullAbrham(t *testing.T) { {"-26.1", 15, "0.00000000000462289492466867"}, {"-50.1591", 10, "0.0000000000000000000001645052084"}, {"-50.1591", 30, "0.000000000000000000000164505208424152908216198963722"}, + {"23.31", 1, "10000000000"}, + {"50", 2, "5200000000000000000000"}, + {"-50", 2, "0.00000000000000000000019"}, } { d, _ := NewFromString(testCase.Dec) expected, _ := NewFromString(testCase.ExpectedDec) diff --git a/math.go b/math.go index 11f07da..b8ee379 100644 --- a/math.go +++ b/math.go @@ -717,11 +717,13 @@ func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { } res := Decimal{new(big.Int).Exp(sum.getValue(), big.NewInt(ki), nil), int32(expInt64)} - // Round to overallPrecision significant figures. This holds whether res is - // >=1 or <1; the old <1 branch rounded to a fixed number of decimal places, - // dropping leading zeros (so e.g. exp(-50).ExpHullAbrham(10) returned 0). - resNumDigits := int32(res.NumDigits()) - roundDigits := int32(currentPrecision) - resNumDigits - res.exp + // Round to overallPrecision significant figures, at least 1. A raised currentPrecision only keeps + // the calculation precise, it doesn't add digits to the result. + sig := int32(overallPrecision) + if sig == 0 { + sig = 1 + } + roundDigits := sig - int32(res.NumDigits()) - res.exp res = res.Round(roundDigits) From 78a38f8f6fa2593f452dec95596e3aa2067b6ba3 Mon Sep 17 00:00:00 2001 From: Raiven Kao Date: Sun, 18 Oct 2026 05:21:43 +0800 Subject: [PATCH 43/52] perf: NewFromFloat uses strconv, drop the vendored strconv copy (#431) * perf: delegate NewFromFloat shortest-decimal conversion to strconv NewFromFloat and NewFromFloat32 need the shortest decimal digit string that round-trips back to the input float. strconv already computes exactly that for FormatFloat with precision -1, but the library computes it itself through decimal-go.go and rounding.go, a copy of the 2009 strconv/decimal.go: assign the mantissa into an 800-byte digit buffer, shift it one binary bit at a time, then walk the digits against the two neighbouring floats to find where they diverge. Ask strconv for the value in 'e' format instead and read the digits back out. The 'e' layout is [-]d[.ddd]e+dd, at most 24 bytes and trivial to scan, unlike 'f' which can run past 750 bytes at the exponent extremes. The result is identical, so no caller-visible behaviour changes. Benchmarks (Ryzen 9 5900X, -benchtime 2s -count 5, median): go1.22 go1.27 Float 366 -> 168 ns 332 -> 94 ns Float32 271 -> 130 ns 241 -> 92 ns Allocations are unchanged at 2 / 40 B, both from the big.NewInt that Decimal.value requires. The gap widens on go1.27 because that release replaced strconv's shortest-float conversion with the uscale algorithm. Before go1.13, strconv.FormatFloat could return a shortest form that was not the nearest one (golang/go#29491, the bug behind the two test table entries added for it), so the old path is kept behind a !go1.13 build tag. Nothing changes for go1.10 through go1.12; once the module's minimum Go version passes 1.13, decimal-go.go, rounding.go and newfromfloat_legacy.go can be deleted outright, removing 575 lines. Verified equivalent against the previous implementation on go1.13 and go1.27 over all 4,278,190,078 non-zero finite float32 bit patterns and 578,922,574 float64 values (random bit patterns, every power of two and its neighbours, and a dense subnormal sweep), with no mismatch. Tested on go1.10.8, go1.11.13, go1.12.17, go1.13.15, go1.22.12 and go1.27.0. * refactor: use strconv on every Go version, drop the vendored strconv copy Keeping the old implementation behind build tags for Go 1.10-1.12 meant maintaining two copies of NewFromFloat. Those versions now go through strconv too, which there can pick a different, still round-tripping shortest form in rare cases (golang/go#29491), so the two test cases for that only run on Go 1.13 and later. --------- Co-authored-by: mwoss --- decimal-go.go | 415 ------------------------------------------ decimal.go | 83 +++++---- decimal_go113_test.go | 122 +++++++++++++ decimal_test.go | 3 - rounding.go | 160 ---------------- 5 files changed, 169 insertions(+), 614 deletions(-) delete mode 100644 decimal-go.go create mode 100644 decimal_go113_test.go delete mode 100644 rounding.go diff --git a/decimal-go.go b/decimal-go.go deleted file mode 100644 index 9958d69..0000000 --- a/decimal-go.go +++ /dev/null @@ -1,415 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Multiprecision decimal numbers. -// For floating-point formatting only; not general purpose. -// Only operations are assign and (binary) left/right shift. -// Can do binary floating point in multiprecision decimal precisely -// because 2 divides 10; cannot do decimal floating point -// in multiprecision binary precisely. - -package decimal - -type decimal struct { - d [800]byte // digits, big-endian representation - nd int // number of digits used - dp int // decimal point - neg bool // negative flag - trunc bool // discarded nonzero digits beyond d[:nd] -} - -func (a *decimal) String() string { - n := 10 + a.nd - if a.dp > 0 { - n += a.dp - } - if a.dp < 0 { - n += -a.dp - } - - buf := make([]byte, n) - w := 0 - switch { - case a.nd == 0: - return "0" - - case a.dp <= 0: - // zeros fill space between decimal point and digits - buf[w] = '0' - w++ - buf[w] = '.' - w++ - w += digitZero(buf[w : w+-a.dp]) - w += copy(buf[w:], a.d[0:a.nd]) - - case a.dp < a.nd: - // decimal point in middle of digits - w += copy(buf[w:], a.d[0:a.dp]) - buf[w] = '.' - w++ - w += copy(buf[w:], a.d[a.dp:a.nd]) - - default: - // zeros fill space between digits and decimal point - w += copy(buf[w:], a.d[0:a.nd]) - w += digitZero(buf[w : w+a.dp-a.nd]) - } - return string(buf[0:w]) -} - -func digitZero(dst []byte) int { - for i := range dst { - dst[i] = '0' - } - return len(dst) -} - -// trim trailing zeros from number. -// (They are meaningless; the decimal point is tracked -// independent of the number of digits.) -func trim(a *decimal) { - for a.nd > 0 && a.d[a.nd-1] == '0' { - a.nd-- - } - if a.nd == 0 { - a.dp = 0 - } -} - -// Assign v to a. -func (a *decimal) Assign(v uint64) { - var buf [24]byte - - // Write reversed decimal in buf. - n := 0 - for v > 0 { - v1 := v / 10 - v -= 10 * v1 - buf[n] = byte(v + '0') - n++ - v = v1 - } - - // Reverse again to produce forward decimal in a.d. - a.nd = 0 - for n--; n >= 0; n-- { - a.d[a.nd] = buf[n] - a.nd++ - } - a.dp = a.nd - trim(a) -} - -// Maximum shift that we can do in one pass without overflow. -// A uint has 32 or 64 bits, and we have to be able to accommodate 9<> 63) -const maxShift = uintSize - 4 - -// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow. -func rightShift(a *decimal, k uint) { - r := 0 // read pointer - w := 0 // write pointer - - // Pick up enough leading digits to cover first shift. - var n uint - for ; n>>k == 0; r++ { - if r >= a.nd { - if n == 0 { - // a == 0; shouldn't get here, but handle anyway. - a.nd = 0 - return - } - for n>>k == 0 { - n = n * 10 - r++ - } - break - } - c := uint(a.d[r]) - n = n*10 + c - '0' - } - a.dp -= r - 1 - - var mask uint = (1 << k) - 1 - - // Pick up a digit, put down a digit. - for ; r < a.nd; r++ { - c := uint(a.d[r]) - dig := n >> k - n &= mask - a.d[w] = byte(dig + '0') - w++ - n = n*10 + c - '0' - } - - // Put down extra digits. - for n > 0 { - dig := n >> k - n &= mask - if w < len(a.d) { - a.d[w] = byte(dig + '0') - w++ - } else if dig > 0 { - a.trunc = true - } - n = n * 10 - } - - a.nd = w - trim(a) -} - -// Cheat sheet for left shift: table indexed by shift count giving -// number of new digits that will be introduced by that shift. -// -// For example, leftcheats[4] = {2, "625"}. That means that -// if we are shifting by 4 (multiplying by 16), it will add 2 digits -// when the string prefix is "625" through "999", and one fewer digit -// if the string prefix is "000" through "624". -// -// Credit for this trick goes to Ken. - -type leftCheat struct { - delta int // number of new digits - cutoff string // minus one digit if original < a. -} - -var leftcheats = []leftCheat{ - // Leading digits of 1/2^i = 5^i. - // 5^23 is not an exact 64-bit floating point number, - // so have to use bc for the math. - // Go up to 60 to be large enough for 32bit and 64bit platforms. - /* - seq 60 | sed 's/^/5^/' | bc | - awk 'BEGIN{ print "\t{ 0, \"\" }," } - { - log2 = log(2)/log(10) - printf("\t{ %d, \"%s\" },\t// * %d\n", - int(log2*NR+1), $0, 2**NR) - }' - */ - {0, ""}, - {1, "5"}, // * 2 - {1, "25"}, // * 4 - {1, "125"}, // * 8 - {2, "625"}, // * 16 - {2, "3125"}, // * 32 - {2, "15625"}, // * 64 - {3, "78125"}, // * 128 - {3, "390625"}, // * 256 - {3, "1953125"}, // * 512 - {4, "9765625"}, // * 1024 - {4, "48828125"}, // * 2048 - {4, "244140625"}, // * 4096 - {4, "1220703125"}, // * 8192 - {5, "6103515625"}, // * 16384 - {5, "30517578125"}, // * 32768 - {5, "152587890625"}, // * 65536 - {6, "762939453125"}, // * 131072 - {6, "3814697265625"}, // * 262144 - {6, "19073486328125"}, // * 524288 - {7, "95367431640625"}, // * 1048576 - {7, "476837158203125"}, // * 2097152 - {7, "2384185791015625"}, // * 4194304 - {7, "11920928955078125"}, // * 8388608 - {8, "59604644775390625"}, // * 16777216 - {8, "298023223876953125"}, // * 33554432 - {8, "1490116119384765625"}, // * 67108864 - {9, "7450580596923828125"}, // * 134217728 - {9, "37252902984619140625"}, // * 268435456 - {9, "186264514923095703125"}, // * 536870912 - {10, "931322574615478515625"}, // * 1073741824 - {10, "4656612873077392578125"}, // * 2147483648 - {10, "23283064365386962890625"}, // * 4294967296 - {10, "116415321826934814453125"}, // * 8589934592 - {11, "582076609134674072265625"}, // * 17179869184 - {11, "2910383045673370361328125"}, // * 34359738368 - {11, "14551915228366851806640625"}, // * 68719476736 - {12, "72759576141834259033203125"}, // * 137438953472 - {12, "363797880709171295166015625"}, // * 274877906944 - {12, "1818989403545856475830078125"}, // * 549755813888 - {13, "9094947017729282379150390625"}, // * 1099511627776 - {13, "45474735088646411895751953125"}, // * 2199023255552 - {13, "227373675443232059478759765625"}, // * 4398046511104 - {13, "1136868377216160297393798828125"}, // * 8796093022208 - {14, "5684341886080801486968994140625"}, // * 17592186044416 - {14, "28421709430404007434844970703125"}, // * 35184372088832 - {14, "142108547152020037174224853515625"}, // * 70368744177664 - {15, "710542735760100185871124267578125"}, // * 140737488355328 - {15, "3552713678800500929355621337890625"}, // * 281474976710656 - {15, "17763568394002504646778106689453125"}, // * 562949953421312 - {16, "88817841970012523233890533447265625"}, // * 1125899906842624 - {16, "444089209850062616169452667236328125"}, // * 2251799813685248 - {16, "2220446049250313080847263336181640625"}, // * 4503599627370496 - {16, "11102230246251565404236316680908203125"}, // * 9007199254740992 - {17, "55511151231257827021181583404541015625"}, // * 18014398509481984 - {17, "277555756156289135105907917022705078125"}, // * 36028797018963968 - {17, "1387778780781445675529539585113525390625"}, // * 72057594037927936 - {18, "6938893903907228377647697925567626953125"}, // * 144115188075855872 - {18, "34694469519536141888238489627838134765625"}, // * 288230376151711744 - {18, "173472347597680709441192448139190673828125"}, // * 576460752303423488 - {19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976 -} - -// Is the leading prefix of b lexicographically less than s? -func prefixIsLessThan(b []byte, s string) bool { - for i := 0; i < len(s); i++ { - if i >= len(b) { - return true - } - if b[i] != s[i] { - return b[i] < s[i] - } - } - return false -} - -// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow. -func leftShift(a *decimal, k uint) { - delta := leftcheats[k].delta - if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) { - delta-- - } - - r := a.nd // read index - w := a.nd + delta // write index - - // Pick up a digit, put down a digit. - var n uint - for r--; r >= 0; r-- { - n += (uint(a.d[r]) - '0') << k - quo := n / 10 - rem := n - 10*quo - w-- - if w < len(a.d) { - a.d[w] = byte(rem + '0') - } else if rem != 0 { - a.trunc = true - } - n = quo - } - - // Put down extra digits. - for n > 0 { - quo := n / 10 - rem := n - 10*quo - w-- - if w < len(a.d) { - a.d[w] = byte(rem + '0') - } else if rem != 0 { - a.trunc = true - } - n = quo - } - - a.nd += delta - if a.nd >= len(a.d) { - a.nd = len(a.d) - } - a.dp += delta - trim(a) -} - -// Binary shift left (k > 0) or right (k < 0). -func (a *decimal) Shift(k int) { - switch { - case a.nd == 0: - // nothing to do: a == 0 - case k > 0: - for k > maxShift { - leftShift(a, maxShift) - k -= maxShift - } - leftShift(a, uint(k)) - case k < 0: - for k < -maxShift { - rightShift(a, maxShift) - k += maxShift - } - rightShift(a, uint(-k)) - } -} - -// If we chop a at nd digits, should we round up? -func shouldRoundUp(a *decimal, nd int) bool { - if nd < 0 || nd >= a.nd { - return false - } - if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even - // if we truncated, a little higher than what's recorded - always round up - if a.trunc { - return true - } - return nd > 0 && (a.d[nd-1]-'0')%2 != 0 - } - // not halfway - digit tells all - return a.d[nd] >= '5' -} - -// Round a to nd digits (or fewer). -// If nd is zero, it means we're rounding -// just to the left of the digits, as in -// 0.09 -> 0.1. -func (a *decimal) Round(nd int) { - if nd < 0 || nd >= a.nd { - return - } - if shouldRoundUp(a, nd) { - a.RoundUp(nd) - } else { - a.RoundDown(nd) - } -} - -// Round a down to nd digits (or fewer). -func (a *decimal) RoundDown(nd int) { - if nd < 0 || nd >= a.nd { - return - } - a.nd = nd - trim(a) -} - -// Round a up to nd digits (or fewer). -func (a *decimal) RoundUp(nd int) { - if nd < 0 || nd >= a.nd { - return - } - - // round up - for i := nd - 1; i >= 0; i-- { - c := a.d[i] - if c < '9' { // can stop after this digit - a.d[i]++ - a.nd = i + 1 - return - } - } - - // Number is all 9s. - // Change to single 1 with adjusted decimal point. - a.d[0] = '1' - a.nd = 1 - a.dp++ -} - -// Extract integer part, rounded appropriately. -// No guarantees about overflow. -func (a *decimal) RoundedInteger() uint64 { - if a.dp > 20 { - return 0xFFFFFFFFFFFFFFFF - } - var i int - n := uint64(0) - for i = 0; i < a.dp && i < a.nd; i++ { - n = n*10 + uint64(a.d[i]-'0') - } - for ; i < a.dp; i++ { - n *= 10 - } - if shouldRoundUp(a, a.dp) { - n++ - } - return n -} diff --git a/decimal.go b/decimal.go index 8da3031..63bd7db 100644 --- a/decimal.go +++ b/decimal.go @@ -387,7 +387,7 @@ func NewFromFloat(value float64) Decimal { if value == 0 { return New(0, 0) } - return newFromFloat(value, math.Float64bits(value), &float64info) + return newFromFloat(value, 64) } // NewFromFloat32 converts a float32 to Decimal. @@ -406,55 +406,66 @@ func NewFromFloat32(value float32) Decimal { if value == 0 { return New(0, 0) } - // XOR is workaround for https://github.com/golang/go/issues/26285 - a := math.Float32bits(value) ^ 0x80808080 - return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info) + return newFromFloat(float64(value), 32) } -func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal { +// newFromFloat converts val to a Decimal holding the shortest decimal digit +// string that round-trips back to val when parsed at the given bit size. +// +// strconv already computes exactly that string, so we ask it for the value in +// 'e' format with precision -1 and read the digits back out. The 'e' layout is +// [-]d[.ddd]e±dd, which is bounded (at most 24 bytes) and trivial to scan, +// unlike 'f' which can run to over 750 bytes for extreme exponents. +func newFromFloat(val float64, bitSize int) Decimal { if math.IsNaN(val) || math.IsInf(val, 0) { panic(fmt.Sprintf("Cannot create a Decimal from %v", val)) } - exp := int(bits>>flt.mantbits) & (1<>(flt.expbits+flt.mantbits) != 0 + // Split the significand from the exponent. b always contains an 'e'. + var i int + for b[i] != 'e' { + i++ + } + digits, expDigits := b[:i], b[i+1:] - roundShortest(&d, mant, exp, flt) - // If less than 19 digits, we can do calculation in an int64. - if d.nd < 19 { - tmp := int64(0) - m := int64(1) - for i := d.nd - 1; i >= 0; i-- { - tmp += m * int64(d.d[i]-'0') - m *= 10 + // The shortest round-trip form has at most 17 significant digits for a + // float64 and 9 for a float32, so the significand always fits in an int64. + var mant int64 + nd := 0 + for _, c := range digits { + if c == '.' { + continue } - if d.neg { - tmp *= -1 - } - return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)} + mant = mant*10 + int64(c-'0') + nd++ } - dValue := new(big.Int) - dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10) - if ok { - return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)} + if neg { + mant = -mant } - return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd)) + esign := 1 + switch expDigits[0] { + case '-': + esign = -1 + expDigits = expDigits[1:] + case '+': + expDigits = expDigits[1:] + } + e := 0 + for _, c := range expDigits { + e = e*10 + int(c-'0') + } + + // digits is d.ddd, i.e. the significand scaled by 10^(nd-1). + return Decimal{value: big.NewInt(mant), exp: int32(esign*e - nd + 1)} } // NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary diff --git a/decimal_go113_test.go b/decimal_go113_test.go new file mode 100644 index 0000000..b6132be --- /dev/null +++ b/decimal_go113_test.go @@ -0,0 +1,122 @@ +//go:build go1.13 +// +build go1.13 + +package decimal + +import ( + "math" + "math/rand" + "strconv" + "testing" +) + +// Before Go 1.13, strconv could return a shortest form that wasn't the nearest one for these (golang/go#29491). +func init() { + testTable = append(testTable, + &testEnt{498484681984085570, "498484681984085570", "", ""}, + &testEnt{5.8339553793802237e+23, "583395537938022370000000", "", ""}, + ) +} + +// TestNewFromFloatShortestRoundTrip pins the contract of NewFromFloat and +// NewFromFloat32: the result is the shortest decimal that parses back to the +// original float. It covers the exponent and significand shapes a +// shortest-representation conversion has to get right, namely subnormals, +// three-digit exponents, single-digit significands and the range extremes. +// +// It is gated on Go 1.13 because before that strconv itself could return a +// shortest form that was not the nearest one (golang/go#29491), which would make +// the reference value below wrong rather than the conversion under test. +func TestNewFromFloatShortestRoundTrip(t *testing.T) { + f64 := []float64{ + 1, -1, 0.1, -0.1, 0.5, 2.5, 100, 123.456, + 1e15, 1e16, 1e17, 1e21, 1e22, 1e23, 9007199254740992, + 1e-100, 1e100, 1e-300, 1e300, + math.MaxFloat64, -math.MaxFloat64, + math.SmallestNonzeroFloat64, -math.SmallestNonzeroFloat64, + math.Pi, math.E, + } + // Every power of two, which sweeps the whole exponent range including the + // subnormals, plus both neighbours of every value collected so far. + for e := -1074; e <= 1023; e++ { + f64 = append(f64, math.Ldexp(1, e)) + } + for _, f := range append([]float64{}, f64...) { + f64 = append(f64, math.Nextafter(f, math.Inf(1)), math.Nextafter(f, math.Inf(-1))) + } + + for _, f := range f64 { + if f == 0 || math.IsInf(f, 0) { + continue + } + want, err := NewFromString(strconv.FormatFloat(f, 'f', -1, 64)) + if err != nil { + t.Fatalf("NewFromString(%v): %v", f, err) + } + got := NewFromFloat(f) + if !got.Equal(want) { + t.Errorf("NewFromFloat(%v) = %s (%s, %d), want %s (%s, %d)", + f, got, got.value, got.exp, want, want.value, want.exp) + } + if back, _ := got.Float64(); back != f { + t.Errorf("NewFromFloat(%v).Float64() = %v, does not round-trip", f, back) + } + } + + f32 := []float32{ + 1, -1, 0.1, -0.1, 100, 123.456, + math.MaxFloat32, -math.MaxFloat32, + math.SmallestNonzeroFloat32, -math.SmallestNonzeroFloat32, + } + for e := -149; e <= 127; e++ { + f32 = append(f32, float32(math.Ldexp(1, e))) + } + for _, f := range f32 { + if f == 0 { + continue + } + want, err := NewFromString(strconv.FormatFloat(float64(f), 'f', -1, 32)) + if err != nil { + t.Fatalf("NewFromString(%v): %v", f, err) + } + got := NewFromFloat32(f) + if !got.Equal(want) { + t.Errorf("NewFromFloat32(%v) = %s (%s, %d), want %s (%s, %d)", + f, got, got.value, got.exp, want, want.value, want.exp) + } + } +} + +// TestNewFromFloatRandomRoundTrip sweeps random bit patterns, which puts a +// meaningful share of subnormals and extreme exponents through the conversion. +func TestNewFromFloatRandomRoundTrip(t *testing.T) { + rng := rand.New(rand.NewSource(0xdead1337)) + for i := 0; i < 200000; i++ { + f := math.Float64frombits(rng.Uint64()) + if f == 0 || math.IsNaN(f) || math.IsInf(f, 0) { + continue + } + want, err := NewFromString(strconv.FormatFloat(f, 'f', -1, 64)) + if err != nil { + t.Fatalf("NewFromString(%v): %v", f, err) + } + if got := NewFromFloat(f); !got.Equal(want) { + t.Fatalf("NewFromFloat(%v) = %s (%s, %d), want %s (%s, %d)", + f, got, got.value, got.exp, want, want.value, want.exp) + } + } + for i := 0; i < 200000; i++ { + f := math.Float32frombits(rng.Uint32()) + if f == 0 || math.IsNaN(float64(f)) || math.IsInf(float64(f), 0) { + continue + } + want, err := NewFromString(strconv.FormatFloat(float64(f), 'f', -1, 32)) + if err != nil { + t.Fatalf("NewFromString(%v): %v", f, err) + } + if got := NewFromFloat32(f); !got.Equal(want) { + t.Fatalf("NewFromFloat32(%v) = %s (%s, %d), want %s (%s, %d)", + f, got, got.value, got.exp, want, want.value, want.exp) + } + } +} diff --git a/decimal_test.go b/decimal_test.go index 6f9c5ed..5c6fcca 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -60,9 +60,6 @@ var testTable = []*testEnt{ {2e250, "20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "", ""}, {math.MaxInt64, strconv.FormatFloat(float64(math.MaxInt64), 'f', -1, 64), "", strconv.FormatInt(math.MaxInt64, 10)}, {1.29067116156722e-309, "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000129067116156722", "", "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001290671161567218558822290567835270536800098852722416870074139002112543896676308448335063375297788379444685193974290737962187240854947838776604607190387984577130572928111657710645015086812756013489109884753559084166516937690932698276436869274093950997935137476803610007959500457935217950764794724766740819156974617155861568214427828145972181876775307023388139991104942469299524961281641158436752347582767153796914843896176260096039358494077706152272661453132497761307744086665088096215425146090058519888494342944692629602847826300550628670375451325582843627504604013541465361435761965354140678551369499812124085312128659002910905639984075064968459581691226705666561364681985266583563078466180095375402399087817404368974165082030458595596655868575908243656158447265625000000000000000000000000000000000000004440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, - // go Issue 29491. - {498484681984085570, "498484681984085570", "", ""}, - {5.8339553793802237e+23, "583395537938022370000000", "", ""}, } var testTableScientificNotation = map[string]string{ diff --git a/rounding.go b/rounding.go deleted file mode 100644 index d4b0cd0..0000000 --- a/rounding.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Multiprecision decimal numbers. -// For floating-point formatting only; not general purpose. -// Only operations are assign and (binary) left/right shift. -// Can do binary floating point in multiprecision decimal precisely -// because 2 divides 10; cannot do decimal floating point -// in multiprecision binary precisely. - -package decimal - -type floatInfo struct { - mantbits uint - expbits uint - bias int -} - -var float32info = floatInfo{23, 8, -127} -var float64info = floatInfo{52, 11, -1023} - -// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits -// that will let the original floating point value be precisely reconstructed. -func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) { - // If mantissa is zero, the number is zero; stop now. - if mant == 0 { - d.nd = 0 - return - } - - // Compute upper and lower such that any decimal number - // between upper and lower (possibly inclusive) - // will round to the original floating point number. - - // We may see at once that the number is already shortest. - // - // Suppose d is not denormal, so that 2^exp <= d < 10^dp. - // The closest shorter number is at least 10^(dp-nd) away. - // The lower/upper bounds computed below are at distance - // at most 2^(exp-mantbits). - // - // So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits), - // or equivalently log2(10)*(dp-nd) > exp-mantbits. - // It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32). - minexp := flt.bias + 1 // minimum possible exponent - if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) { - // The number is already shortest. - return - } - - // d = mant << (exp - mantbits) - // Next highest floating point number is mant+1 << exp-mantbits. - // Our upper bound is halfway between, mant*2+1 << exp-mantbits-1. - upper := new(decimal) - upper.Assign(mant*2 + 1) - upper.Shift(exp - int(flt.mantbits) - 1) - - // d = mant << (exp - mantbits) - // Next lowest floating point number is mant-1 << exp-mantbits, - // unless mant-1 drops the significant bit and exp is not the minimum exp, - // in which case the next lowest is mant*2-1 << exp-mantbits-1. - // Either way, call it mantlo << explo-mantbits. - // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1. - var mantlo uint64 - var explo int - if mant > 1<= d.nd { - break - } - li := ui - upper.dp + lower.dp - l := byte('0') // lower digit - if li >= 0 && li < lower.nd { - l = lower.d[li] - } - m := byte('0') // middle digit - if mi >= 0 { - m = d.d[mi] - } - u := byte('0') // upper digit - if ui < upper.nd { - u = upper.d[ui] - } - - // Okay to round down (truncate) if lower has a different digit - // or if lower is inclusive and is exactly the result of rounding - // down (i.e., and we have reached the final digit of lower). - okdown := l != m || inclusive && li+1 == lower.nd - - switch { - case upperdelta == 0 && m+1 < u: - // Example: - // m = 12345xxx - // u = 12347xxx - upperdelta = 2 - case upperdelta == 0 && m != u: - // Example: - // m = 12345xxx - // u = 12346xxx - upperdelta = 1 - case upperdelta == 1 && (m != '9' || u != '0'): - // Example: - // m = 1234598x - // u = 1234600x - upperdelta = 2 - } - // Okay to round up if upper has a different digit and either upper - // is inclusive or upper is bigger than the result of rounding up. - okup := upperdelta > 0 && (inclusive || upperdelta > 1 || ui+1 < upper.nd) - - // If it's okay to do either, then round to the nearest one. - // If it's okay to do only one, do it. - switch { - case okdown && okup: - d.Round(mi + 1) - return - case okdown: - d.RoundDown(mi + 1) - return - case okup: - d.RoundUp(mi + 1) - return - } - } -} From 37aad64c5a01f3b0fee4c203ca9868cae7f7d0aa Mon Sep 17 00:00:00 2001 From: adam magued Date: Sun, 18 Oct 2026 00:29:19 +0300 Subject: [PATCH 44/52] feat: add NewFromBigFloat constructor (#457) Co-authored-by: mwoss --- decimal.go | 22 ++++++++++ decimal_test.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/decimal.go b/decimal.go index 63bd7db..4bcb759 100644 --- a/decimal.go +++ b/decimal.go @@ -212,6 +212,28 @@ func NewFromBigRat(value *big.Rat, precision int32) Decimal { }, precision) } +// NewFromBigFloat returns a new Decimal from a big.Float. The result is the shortest decimal +// that rounds back to value at value's precision, as NewFromFloat does for a float64. +// It returns an error for nil and infinite values, and for exponents beyond MaxDecodeExponent. +// +// Example: +// +// f, _ := new(big.Float).SetString("-123.45") +// d, err := NewFromBigFloat(f) +// d.String() // output: "-123.45" +func NewFromBigFloat(value *big.Float) (Decimal, error) { + if value == nil { + return Decimal{}, fmt.Errorf("can't convert nil *big.Float to decimal") + } + if value.IsInf() { + return Decimal{}, fmt.Errorf("can't convert infinite *big.Float to decimal") + } + if value.Sign() == 0 { + return New(0, 0), nil + } + return NewFromString(value.Text('g', -1)) +} + // NewFromString returns a new Decimal from a string representation. // Trailing zeroes are not trimmed. // diff --git a/decimal_test.go b/decimal_test.go index 5c6fcca..6b5120d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -769,6 +769,88 @@ func TestNewFromBigRat(t *testing.T) { } } +func TestNewFromBigFloat(t *testing.T) { + mustParseFloat := func(s string, prec uint) *big.Float { + f, _, err := big.ParseFloat(s, 10, prec, big.ToNearestEven) + if err != nil { + t.Fatalf("failed to parse float %s: %v", s, err) + } + return f + } + + testCases := []struct { + name string + input *big.Float + expected string + }{ + {"zero", new(big.Float).SetInt64(0), "0"}, + {"uninitialized", new(big.Float), "0"}, + {"negative zero", new(big.Float).Neg(new(big.Float)), "0"}, + {"integer positive", new(big.Float).SetInt64(12345), "12345"}, + {"integer negative", new(big.Float).SetInt64(-12345), "-12345"}, + {"simple float positive", mustParseFloat("12.1", 64), "12.1"}, + {"simple float negative", mustParseFloat("-12.1", 64), "-12.1"}, + {"fractional positive", mustParseFloat("0.01", 64), "0.01"}, + {"fractional negative", mustParseFloat("-0.01", 64), "-0.01"}, + {"decimal 123.456", mustParseFloat("123.456", 64), "123.456"}, + {"decimal -123.456", mustParseFloat("-123.456", 64), "-123.456"}, + {"from float64 exact", new(big.Float).SetFloat64(1.25), "1.25"}, + {"from float64 0.5", new(big.Float).SetFloat64(0.5), "0.5"}, + {"from rat half", new(big.Float).SetRat(big.NewRat(1, 2)), "0.5"}, + {"high precision pi", mustParseFloat("3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679", 512), "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"}, + {"high precision large", mustParseFloat("1234567890123456789012345678901234567890.1234567890123456789", 512), "1234567890123456789012345678901234567890.1234567890123456789"}, + {"large exponent", mustParseFloat("1e25", 256), "10000000000000000000000000"}, + {"small exponent", mustParseFloat("1e-25", 256), "0.0000000000000000000000001"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d, err := NewFromBigFloat(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if d.String() != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, d.String()) + } + }) + } +} + +func TestNewFromBigFloat_Error(t *testing.T) { + t.Run("nil", func(t *testing.T) { + _, err := NewFromBigFloat(nil) + if err == nil { + t.Fatal("expected error for nil, got nil") + } + }) + + t.Run("positive inf", func(t *testing.T) { + f := new(big.Float).SetInf(false) + _, err := NewFromBigFloat(f) + if err == nil { + t.Fatal("expected error for +Inf, got nil") + } + }) + + t.Run("negative inf", func(t *testing.T) { + f := new(big.Float).SetInf(true) + _, err := NewFromBigFloat(f) + if err == nil { + t.Fatal("expected error for -Inf, got nil") + } + }) + + t.Run("exponent overflow", func(t *testing.T) { + f, _, err := big.ParseFloat("1e15000", 10, 64, big.ToNearestEven) + if err == nil { + _, err = NewFromBigFloat(f) + if err == nil { + t.Fatal("expected error for exponent exceeding MaxDecodeExponent, got nil") + } + } + }) +} + func TestCopy(t *testing.T) { origin := New(1, 0) cpy := origin.Copy() @@ -2581,6 +2663,36 @@ func TestBigFloat(t *testing.T) { } } +func TestBigFloatRoundTrip(t *testing.T) { + testCases := []string{ + "0", + "1", + "-1", + "0.5", + "-0.5", + "0.25", + "12.1", + "-12.1", + "123.456", + "-123.456", + } + + for _, s := range testCases { + d, err := NewFromString(s) + if err != nil { + t.Fatal(err) + } + bf := d.BigFloat() + roundTripD, err := NewFromBigFloat(bf) + if err != nil { + t.Fatalf("NewFromBigFloat failed for %s: %v", s, err) + } + if !d.Equal(roundTripD) { + t.Errorf("round trip failed: expected %s, got %s", d, roundTripD) + } + } +} + func TestDecimal_Min(t *testing.T) { // the first element in the array is the expected answer, rest are inputs testCases := [][]float64{ From bed77f5cb683c3752148c852ed9d97cfb058aec4 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sat, 17 Oct 2026 23:45:12 +0200 Subject: [PATCH 45/52] fix: Ln rounds correctly, computed with error bounds like Pow (#471) Co-authored-by: vera-rubin <322284620+vera-rubin@users.noreply.github.com> --- const.go | 55 ------------------- const_test.go | 34 ------------ decimal_test.go | 18 +++++++ math.go | 137 ++++++++++++------------------------------------ 4 files changed, 51 insertions(+), 193 deletions(-) delete mode 100644 const_test.go diff --git a/const.go b/const.go index 4538cab..f2f8647 100644 --- a/const.go +++ b/const.go @@ -2,67 +2,12 @@ package decimal import ( "math/big" - "strings" ) const ( strLn10 = "2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286248633409525465082806756666287369098781689482907208325554680843799894826233198528393505308965377732628846163366222287698219886746543667474404243274365155048934314939391479619404400222105101714174800368808401264708068556774321622835522011480466371565912137345074785694768346361679210180644507064800027750268491674655058685693567342067058113642922455440575892572420824131469568901675894025677631135691929203337658714166023010570308963457207544037084746994016826928280848118428931484852494864487192780967627127577539702766860595249671667418348570442250719796500471495105049221477656763693866297697952211071826454973477266242570942932258279850258550978526538320760672631716430950599508780752371033310119785754733154142180842754386359177811705430982748238504564801909561029929182431823752535770975053956518769751037497088869218020518933950723853920514463419726528728696511086257149219884997874887377134568620916705849807828059751193854445009978131146915934666241071846692310107598438319191292230792503747298650929009880391941702654416816335727555703151596113564846546190897042819763365836983716328982174407366009162177850541779276367731145041782137660111010731042397832521894898817597921798666394319523936855916447118246753245630912528778330963604262982153040874560927760726641354787576616262926568298704957954913954918049209069438580790032763017941503117866862092408537949861264933479354871737451675809537088281067452440105892444976479686075120275724181874989395971643105518848195288330746699317814634930000321200327765654130472621883970596794457943468343218395304414844803701305753674262153675579814770458031413637793236291560128185336498466942261465206459942072917119370602444929358037007718981097362533224548366988505528285966192805098447175198503666680874970496982273220244823343097169111136813588418696549323714996941979687803008850408979618598756579894836445212043698216415292987811742973332588607915912510967187510929248475023930572665446276200923068791518135803477701295593646298412366497023355174586195564772461857717369368404676577047874319780573853271810933883496338813069945569399346101090745616033312247949360455361849123333063704751724871276379140924398331810164737823379692265637682071706935846394531616949411701841938119405416449466111274712819705817783293841742231409930022911502362192186723337268385688273533371925103412930705632544426611429765388301822384091026198582888433587455960453004548370789052578473166283701953392231047527564998119228742789713715713228319641003422124210082180679525276689858180956119208391760721080919923461516952599099473782780648128058792731993893453415320185969711021407542282796298237068941764740642225757212455392526179373652434440560595336591539160312524480149313234572453879524389036839236450507881731359711238145323701508413491122324390927681724749607955799151363982881058285740538000653371655553014196332241918087621018204919492651483892" ) -var ( - ln10 = newConstApproximation(strLn10) -) - -type constApproximation struct { - exact Decimal - approximations []Decimal -} - -func newConstApproximation(value string) constApproximation { - parts := strings.Split(value, ".") - coeff, fractional := parts[0], parts[1] - - coeffLen := len(coeff) - maxPrecision := len(fractional) - - var approximations []Decimal - for p := 1; p < maxPrecision; p *= 2 { - r := RequireFromString(value[:coeffLen+p]) - approximations = append(approximations, r) - } - - return constApproximation{ - RequireFromString(value), - approximations, - } -} - -// Returns the smallest approximation available that's at least as precise -// as the passed precision (places after decimal point), i.e. Floor[ log2(precision) ] + 1 -func (c constApproximation) withPrecision(precision int32) Decimal { - i := 0 - - if precision >= 1 { - i++ - } - - for precision >= 16 { - precision /= 16 - i += 4 - } - - for precision >= 2 { - precision /= 2 - i++ - } - - if i >= len(c.approximations) { - return c.exact - } - - return c.approximations[i] -} - var zeroInt = big.NewInt(0) var oneInt = big.NewInt(1) var twoInt = big.NewInt(2) diff --git a/const_test.go b/const_test.go deleted file mode 100644 index 67a27a7..0000000 --- a/const_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package decimal - -import "testing" - -func TestConstApproximation(t *testing.T) { - for _, testCase := range []struct { - Const string - Precision int32 - ExpectedApproximation string - }{ - {"2.3025850929940456840179914546", 0, "2"}, - {"2.3025850929940456840179914546", 1, "2.3"}, - {"2.3025850929940456840179914546", 3, "2.302"}, - {"2.3025850929940456840179914546", 5, "2.302585"}, - {"2.3025850929940456840179914546", 10, "2.302585092994045"}, - {"2.3025850929940456840179914546", 100, "2.3025850929940456840179914546"}, - {"2.3025850929940456840179914546", -1, "2"}, - {"2.3025850929940456840179914546", -5, "2"}, - {"3.14159265359", 0, "3"}, - {"3.14159265359", 1, "3.1"}, - {"3.14159265359", 2, "3.141"}, - {"3.14159265359", 4, "3.1415926"}, - {"3.14159265359", 13, "3.14159265359"}, - } { - ca := newConstApproximation(testCase.Const) - expected, _ := NewFromString(testCase.ExpectedApproximation) - - approximation := ca.withPrecision(testCase.Precision) - - if approximation.Cmp(expected) != 0 { - t.Errorf("expected approximation %s, got %s - for const with %s precision %d", testCase.ExpectedApproximation, approximation.String(), testCase.Const, testCase.Precision) - } - } -} diff --git a/decimal_test.go b/decimal_test.go index 6b5120d..b608560 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3935,6 +3935,11 @@ func TestDecimal_Ln(t *testing.T) { {"3.13", 100, "1.1410330045520618486427823998884819383708962910797239760817078430268177216960996098918971117211892839"}, {"5.71", 25, "1.7422190236679188486939833"}, {"5.7185108151957193571930205", 50, "1.74370842450178929149992165925283704012576949094645"}, + {"10", 5, "2.30259"}, + // e^2.5 truncated, ln is 2.5 - 5e-45, so the first working precisions can't decide the rounding + {"12.18249396070347343807017595116796618318276779", 0, "2"}, + {"1e-28", 16, "-64.4723826038332792"}, + {"1e-34", 13, "-78.2878931617976"}, {"839101.0351", 25, "13.6400864014410013994397240"}, {"839101.0351094726488848490572028502", 50, "13.64008640145229044389152437468283605382056561604272"}, {"5023583755703750094849.03519358513093500275017501750602739169823", 25, "49.9684305274348922267409953"}, @@ -3955,6 +3960,19 @@ func TestDecimal_Ln(t *testing.T) { } } +// ln(10^k) is k*ln(10), and strLn10 has far more digits than any rounding below needs. +func TestDecimal_LnPowersOfTen(t *testing.T) { + ln10 := RequireFromString(strLn10) + for k := int32(-40); k <= 40; k++ { + for p := int32(0); p <= 30; p++ { + want := ln10.Mul(New(int64(k), 0)).Round(p) + if got, err := New(1, k).Ln(p); err != nil || !got.Equal(want) { + t.Errorf("Ln(1e%d, %d): got %s, %v, want %s", k, p, got, err, want) + } + } + } +} + func TestDecimal_LnZero(t *testing.T) { d := New(0, 0) diff --git a/math.go b/math.go index b8ee379..f444119 100644 --- a/math.go +++ b/math.go @@ -810,7 +810,7 @@ func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { // Ln calculates natural logarithm of d. // Precision argument specifies how precise the result must be (number of digits after decimal point). -// Negative precision is allowed. +// Negative precision is allowed. The result is correctly rounded half away from zero. // // Example: // @@ -820,120 +820,49 @@ func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { // d2, err := NewFromFloat(579.161).Ln(10) // d2.String() // output: "6.3615805046" func (d Decimal) Ln(precision int32) (Decimal, error) { - // Algorithm based on The Use of Iteration Methods for Approximating the Natural Logarithm, - // James F. Epperson, The American Mathematical Monthly, Vol. 96, No. 9, November 1989, pp. 831-835. if d.IsNegative() { return Decimal{}, fmt.Errorf("cannot calculate natural logarithm for negative decimals") } - if d.IsZero() { return Decimal{}, fmt.Errorf("cannot represent natural logarithm of 0, result: -infinity") } - calcPrecision := precision + 2 - z := d.Copy() + // d = m * 10^k with 1 <= m < 10, so ln(d) = ln(m) + k*ln(10). Both are computed with error bounds, + // and the working precision grows until the whole interval rounds to the same value. That ends, + // because ln(d) is irrational for every d other than 1. + k := int64(d.NumDigits()) - 1 + int64(d.exp) + f := int64(precision) + 10 + if f < 10 { + f = 10 + } + for ; ; f += 20 { + b := uint(float64(f)*math.Log2(10)) + uint(bits.Len64(uint64(abs64(k)))) + 8 + m := new(big.Int).Lsh(d.getValue(), b) + m.Quo(m, pow10(int64(d.NumDigits())-1)) + t, lnErr := lnFixed(m, b) + ln10, ln10Err := ln10Fixed(b) + t.Add(t, new(big.Int).Mul(big.NewInt(k), ln10)) + tErr := big.NewInt(lnErr + 1 + abs64(k)*ln10Err) - var comp1, comp3, comp2, comp4, reduceAdjust Decimal - comp1 = z.Sub(Decimal{oneInt, 0}) - comp3 = Decimal{oneInt, -1} + neg := t.Sign() < 0 + t.Abs(t) + p := pow10(f) + hi := new(big.Int).Add(t, tErr) + hi.Mul(hi, p).Rsh(hi, b).Add(hi, oneInt) + lo := t.Sub(t, tErr) + if lo.Sign() < 0 { + lo.SetInt64(0) + } + lo.Mul(lo, p).Rsh(lo, b) - // for decimal in range [0.9, 1.1] where ln(d) is close to 0 - usePowerSeries := false - - if comp1.Abs().Cmp(comp3) <= 0 { - usePowerSeries = true - } else { - // reduce input decimal to range [0.1, 1) - expDelta := int32(z.NumDigits()) + z.exp - z.exp -= expDelta - - // Input decimal was reduced by factor of 10^expDelta, thus we will need to add - // ln(10^expDelta) = expDelta * ln(10) - // to the result to compensate that - ln10 := ln10.withPrecision(calcPrecision) - reduceAdjust = NewFromInt32(expDelta) - reduceAdjust = reduceAdjust.Mul(ln10) - - comp1 = z.Sub(Decimal{oneInt, 0}) - - if comp1.Abs().Cmp(comp3) <= 0 { - usePowerSeries = true - } else { - // initial estimate using floats - zFloat := z.InexactFloat64() - comp1 = NewFromFloat(math.Log(zFloat)) + g := f - int64(precision) + if q := roundHalfUp(hi, g); q.Cmp(roundHalfUp(lo, g)) == 0 { + if neg { + q.Neg(q) + } + return Decimal{value: q, exp: -precision}, nil } } - - epsilon := Decimal{oneInt, -calcPrecision} - - if usePowerSeries { - // Power Series - https://en.wikipedia.org/wiki/Logarithm#Power_series - // Calculating n-th term of formula: ln(z+1) = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] - // until the difference between current and next term is smaller than epsilon. - // Coverage quite fast for decimals close to 1.0 - - // z + 2 - comp2 = comp1.Add(Decimal{twoInt, 0}) - // z / (z + 2) - comp3 = comp1.DivRound(comp2, calcPrecision) - // 2 * (z / (z + 2)) - comp1 = comp3.Add(comp3) - comp2 = comp1.Copy() - - for n := 1; ; n++ { - // 2 * (z / (z+2))^(2n+1) - comp2 = comp2.Mul(comp3).Mul(comp3) - - // 1 / (2n+1) * 2 * (z / (z+2))^(2n+1) - comp4 = NewFromInt(int64(2*n + 1)) - comp4 = comp2.DivRound(comp4, calcPrecision) - - // comp1 = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] - comp1 = comp1.Add(comp4) - - if comp4.Abs().Cmp(epsilon) <= 0 { - break - } - } - } else { - // Halley's Iteration. - // Calculating n-th term of formula: a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z), - // until the difference between current and next term is smaller than epsilon - var prevStep Decimal - maxIters := calcPrecision*2 + 10 - - for i := int32(0); i < maxIters; i++ { - // exp(a_n) - comp3, _ = comp1.ExpTaylor(calcPrecision) - // exp(a_n) - z - comp2 = comp3.Sub(z) - // 2 * (exp(a_n) - z) - comp2 = comp2.Add(comp2) - // exp(a_n) + z - comp4 = comp3.Add(z) - // 2 * (exp(a_n) - z) / (exp(a_n) + z) - comp3 = comp2.DivRound(comp4, calcPrecision) - // comp1 = a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z) - comp1 = comp1.Sub(comp3) - - if prevStep.Add(comp3).IsZero() { - // If iteration steps oscillate we should return early and prevent an infinity loop - // NOTE(mwoss): This should be quite a rare case, returning error is not necessary - break - } - - if comp3.Abs().Cmp(epsilon) <= 0 { - break - } - - prevStep = comp3 - } - } - - comp1 = comp1.Add(reduceAdjust) - - return comp1.Round(precision), nil } // Trig functions From 100aebf14acc86ffcc47293430ea904d74309685 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 18 Oct 2026 16:44:29 +0200 Subject: [PATCH 46/52] feat: add Root and RootRound (#473) --- decimal_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ math.go | 58 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/decimal_test.go b/decimal_test.go index b608560..76418f7 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3625,6 +3625,74 @@ func TestDecimal_Sqrt(t *testing.T) { } } +func TestDecimal_RootRound(t *testing.T) { + for _, c := range []struct { + in string + n int64 + places int32 + want string + }{ + {"27", 3, 16, "3"}, + {"2", 3, 16, "1.2599210498948732"}, + {"-8", 3, 16, "-2"}, + {"-2", 3, 5, "-1.25992"}, + {"16", 4, 16, "2"}, + {"32", 5, 0, "2"}, + {"0", 5, 16, "0"}, + {"1e-30", 3, 16, "0.0000000001"}, + {"0.001", 3, 2, "0.1"}, + {"0.125", 3, 0, "1"}, + {"-0.125", 3, 0, "-1"}, + {"1000000", 3, -1, "100"}, + {"2", 1, 3, "2"}, + {"5e-150", 10, 14, "0"}, + } { + got, err := RequireFromString(c.in).RootRound(c.n, c.places) + if want := RequireFromString(c.want); err != nil || !got.Equal(want) || got.Exponent() != -c.places { + t.Errorf("RootRound(%s, %d, %d): got %s (exponent %d), %v, want %s", c.in, c.n, c.places, got, got.Exponent(), err, want) + } + } + + for _, c := range []struct { + d Decimal + n int64 + }{{NewFromInt(2), 0}, {NewFromInt(2), -3}, {NewFromInt(-4), 2}, {NewFromInt(-16), 4}} { + if _, err := c.d.RootRound(c.n, 16); err == nil { + t.Errorf("RootRound(%s, %d, 16): got no error", c.d, c.n) + } + } + + // r is the correctly rounded root of d exactly when (r - u/2)^n <= d < (r + u/2)^n, with u = 10^-places + r := rand.New(rand.NewSource(1)) + for i := 0; i < 2000; i++ { + d := NewFromBigInt(new(big.Int).Rand(r, pow10(int64(1+r.Intn(30)))), int32(r.Intn(60)-40)) + n := int64(1 + r.Intn(7)) + places := int32(r.Intn(33) - 2) + got, err := d.RootRound(n, places) + half := New(5, -places-1) + lo, _ := got.Sub(half).PowInt32(int32(n)) + hi, _ := got.Add(half).PowInt32(int32(n)) + if err != nil || got.Sign() > 0 && lo.GreaterThan(d) || hi.LessThanOrEqual(d) { + t.Fatalf("RootRound(%s, %d, %d): got %s, %v, which isn't the correctly rounded root", d, n, places, got, err) + } + if sqrt, _ := d.SqrtRound(places); n == 2 && !got.Equal(sqrt) { + t.Fatalf("RootRound(%s, 2, %d) = %s, SqrtRound gives %s", d, places, got, sqrt) + } + } +} + +func TestDecimal_Root(t *testing.T) { + if got, err := NewFromInt(2).Root(3); err != nil || got.String() != "1.2599210498948732" || got.Exponent() != -16 { + t.Errorf("Root(2, 3): got %s (exponent %d), %v", got, got.Exponent(), err) + } + + defer func(prec int) { DivisionPrecision = prec }(DivisionPrecision) + DivisionPrecision = 4 + if got, err := NewFromInt(2).Root(3); err != nil || got.String() != "1.2599" { + t.Errorf("Root(2, 3) with DivisionPrecision 4: got %s, %v", got, err) + } +} + func TestDecimal_IsInteger(t *testing.T) { for _, testCase := range []struct { Dec string diff --git a/math.go b/math.go index f444119..e5c7d5c 100644 --- a/math.go +++ b/math.go @@ -350,14 +350,14 @@ func powRatio(d Decimal) (*big.Int, *big.Int) { return p.Quo(p, g), q.Quo(q, g) } -// iroot returns the integer k-th root of x >= 1 and whether it is exact. +// iroot returns the integer k-th root of x >= 1, rounded down, and whether it is exact. func iroot(x *big.Int, k int64) (*big.Int, bool) { if k == 1 || x.Cmp(oneInt) == 0 { return x, true } if int64(x.BitLen()) <= k { - // 1 < x < 2^k has no integer k-th root - return nil, false + // 1 < x < 2^k + return big.NewInt(1), false } // Newton's method from above converges to the floor of the root kb, km1 := big.NewInt(k), big.NewInt(k-1) @@ -636,6 +636,58 @@ func (d Decimal) SqrtRound(places int32) (Decimal, error) { return Decimal{value: n, exp: -places}, nil } +// Root returns the n-th root of d with DivisionPrecision digits after the decimal point, like Div. +// Use RootRound to choose the precision per call. Odd roots of negative numbers are negative. +// It returns an error when n < 1, and for negative d when n is even. +// +// Example: +// +// d, err := NewFromInt(2).Root(3) +// d.String() // output: "1.2599210498948732" +func (d Decimal) Root(n int64) (Decimal, error) { + return d.RootRound(n, int32(DivisionPrecision)) +} + +// RootRound returns the n-th root of d rounded half away from zero to places digits after the +// decimal point, like DivRound. The result is exact up to that rounding. Odd roots of negative +// numbers are negative. It returns an error when n < 1, and for negative d when n is even. +// The work grows with n times places, so very large roots such as the millionth are slow. +// +// Example: +// +// d, err := NewFromFloat(-0.125).RootRound(3, 2) +// d.String() // output: "-0.5" +func (d Decimal) RootRound(n int64, places int32) (Decimal, error) { + if n < 1 { + return Decimal{}, fmt.Errorf("cannot calculate the %d-th root, n must be at least 1", n) + } + if d.Sign() < 0 && n%2 == 0 { + return Decimal{}, fmt.Errorf("cannot represent imaginary value of an even root of x, where x < 0") + } + + // floor(root(|d|) * 10^(places+1)) is the integer n-th root of |d| * 10^(n*(places+1)), and its + // last digit decides the rounding, as in SqrtRound. + k := int64(d.exp) + n*(int64(places)+1) + x := new(big.Int).Abs(d.getValue()) + if k >= 0 { + x.Mul(x, pow10(k)) + } else if -k <= int64(d.NumDigits()) { + x.Quo(x, pow10(-k)) + } else { + x.SetInt64(0) + } + r := new(big.Int) + if x.Sign() > 0 { + root, _ := iroot(x, n) + r.Set(root) + } + r.Add(r, fiveInt).Quo(r, tenInt) + if d.Sign() < 0 { + r.Neg(r) + } + return Decimal{value: r, exp: -places}, nil +} + // ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. // OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). // From 59f775d55cfbc2fad65d4938421d4736eea9b834 Mon Sep 17 00:00:00 2001 From: Ryoya Sekino Date: Sun, 18 Oct 2026 23:47:19 +0900 Subject: [PATCH 47/52] README: Fix URLs for the official Go pages (#264) Co-authored-by: mwoss --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2a81974..62a8806 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # decimal [![ci](https://github.com/shopspring/decimal/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/shopspring/decimal/actions/workflows/ci.yml) -[![GoDoc](https://godoc.org/github.com/shopspring/decimal?status.svg)](https://godoc.org/github.com/shopspring/decimal) +[![GoDoc](https://pkg.go.dev/badge/github.com/shopspring/decimal)](https://pkg.go.dev/github.com/shopspring/decimal) Arbitrary-precision fixed-point decimal numbers in go. @@ -28,7 +28,7 @@ Decimal library requires Go version `>=1.10` ## Documentation -http://godoc.org/github.com/shopspring/decimal +https://pkg.go.dev/github.com/shopspring/decimal ## Usage @@ -100,7 +100,7 @@ Explore other alternatives to find the one that best fits your needs :) Because float64 (or any binary floating point type, actually) can't represent numbers such as `0.1` exactly. -Consider this code: http://play.golang.org/p/TQBd4yJe6B You might expect that +Consider this code: https://go.dev/play/p/TQBd4yJe6B You might expect that it prints out `10`, but it actually prints `9.999999999999831`. Over time, these small errors can really add up! @@ -115,7 +115,7 @@ out, the string output has to stop somewhere (let's say it stops at 3 decimal digits, for simplicity), so you'll get 0.333, 0.333, and 0.333. But where did the other 0.001 go? -Here's the above example as code: http://play.golang.org/p/lCZZs0w9KE +Here's the above example as code: https://go.dev/play/p/lCZZs0w9KE With Decimal, the strings being printed out represent the number exactly. So, if you have `x = y = 1/3` (with precision 3), they will actually be equal to @@ -140,7 +140,7 @@ modifies `a` and sets `z` as an alias for `a`, which they might not expect. It also modifies any other aliases to `a`. Here's an example of the subtle bugs you can introduce with big.Int's API: -https://play.golang.org/p/x2R_78pa8r +https://go.dev/play/p/x2R_78pa8r In contrast, it's difficult to make such mistakes with decimal. Decimals behave like other go numbers types: even though `a = b` will not deep copy From ddb96d1ae7c49c6301f1657ae8bff92298f328f5 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 18 Oct 2026 16:56:12 +0200 Subject: [PATCH 48/52] fix: clearer error when scanning NULL into a Decimal (#474) --- decimal_test.go | 4 ++++ encoding.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/decimal_test.go b/decimal_test.go index 76418f7..13221b3 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2803,6 +2803,10 @@ func TestDecimal_Scan(t *testing.T) { if err == nil { t.Errorf("a.Scan(Foo{}) should have thrown an error but did not") } + + if err := a.Scan(nil); err == nil || !strings.Contains(err.Error(), "NullDecimal") { + t.Errorf("a.Scan(nil): got %v, want an error that points to NullDecimal", err) + } } func TestDecimal_ScanTypes(t *testing.T) { diff --git a/encoding.go b/encoding.go index c333af9..6801f99 100644 --- a/encoding.go +++ b/encoding.go @@ -81,6 +81,9 @@ func (d *Decimal) Scan(value interface{}) error { // first try to see if the data is stored in database as a Numeric datatype switch v := value.(type) { + case nil: + return fmt.Errorf("can't scan NULL into a Decimal, use NullDecimal or *Decimal") + case float32: if math.IsNaN(float64(v)) || math.IsInf(float64(v), 0) { return fmt.Errorf("could not convert value '%v' to decimal", v) From 54748831b6d0eec5b618088811ba50dd3a24703d Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 18 Oct 2026 17:16:13 +0200 Subject: [PATCH 49/52] docs: show how to control the JSON format of one field (#475) --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 62a8806..e6a8e30 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,22 @@ this causes extra allocations, so Decimal is less performant. My assumption is that if you're using Decimals, you probably care more about correctness than performance. +#### How do I control the JSON format of one field? + +Wrap the field in a small type with its own `MarshalJSON`. For example, to always write money with 2 decimal places: + +```go +type Money struct{ decimal.Decimal } + +func (m Money) MarshalJSON() ([]byte, error) { + return []byte(`"` + m.StringFixed(2) + `"`), nil +} +``` + +A `Money` field marshals as `"12.50"`, other decimals keep their usual format, and unmarshaling still works through +the embedded `Decimal`. To write one field as a JSON number instead of a string, return `[]byte(m.String())`. +This is safer than the global settings, which change every decimal in the program. + ## License The MIT License (MIT) From 540b9153592dd656f22f0135f8b8b479d21fab72 Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 18 Oct 2026 17:19:27 +0200 Subject: [PATCH 50/52] docs: add udecimal to the alternative libraries (#476) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e6a8e30..4b782f5 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Explore other alternatives to find the one that best fits your needs :) * [cockroachdb/apd](https://github.com/cockroachdb/apd) - arbitrary precision, mutable and rich API similar to `big.Int`, more performant than this library * [alpacahq/alpacadecimal](https://github.com/alpacahq/alpacadecimal) - high performance, low precision (12 digits), fully compatible API with this library * [govalues/decimal](https://github.com/govalues/decimal) - high performance, zero-allocation, low precision (19 digits) +* [quagmt/udecimal](https://github.com/quagmt/udecimal) - high performance, mostly zero-allocation, up to 19 digits after the decimal point * [greatcloak/decimal](https://github.com/greatcloak/decimal) - fork focusing on billing and e-commerce web application related use cases, includes out-of-the-box BSON marshaling support ## FAQ From 4ce352fe1580995cb2664a16f78600b661453ffb Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:29:12 +0300 Subject: [PATCH 51/52] fix: reject overflowing division precision (#472) Signed-off-by: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Co-authored-by: mwoss --- decimal.go | 2 +- decimal_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index 4bcb759..90a17e3 100644 --- a/decimal.go +++ b/decimal.go @@ -796,7 +796,7 @@ func (d Decimal) quoRem(d2 Decimal, precision int32, bb *big.Int) (Decimal, Deci } scale := -precision e := int64(d.exp) - int64(d2.exp) - int64(scale) - if e > math.MaxInt32 || e < math.MinInt32 { + if precision == math.MinInt32 || e > math.MaxInt32 || e < math.MinInt32 { panic("overflow in decimal QuoRem") } if bb == nil { diff --git a/decimal_test.go b/decimal_test.go index 13221b3..73d43c8 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2542,6 +2542,16 @@ func TestDecimal_Overflow(t *testing.T) { if !didPanic(func() { New(1, math.MaxInt32).Mul(New(1, math.MaxInt32)) }) { t.Fatalf("should have gotten an overflow panic") } + // -math.MinInt32 wraps back to itself as an int32. + if !didPanic(func() { New(1, math.MinInt32).QuoRem(New(1, 0), math.MinInt32) }) { + t.Fatalf("should have gotten an overflow panic") + } + if !didPanic(func() { New(1, math.MinInt32).DivRound(New(1, 0), math.MinInt32) }) { + t.Fatalf("should have gotten an overflow panic") + } + if q, _ := New(1, math.MaxInt32).QuoRem(New(1, 0), math.MinInt32+1); !q.Equal(New(1, math.MaxInt32)) { + t.Fatalf("precision math.MinInt32+1 should still work, got %s", q) + } } func TestDecimal_ExtremeValues(t *testing.T) { From 447b833d1eea2d7720d37ae95fcf090d01fa866b Mon Sep 17 00:00:00 2001 From: Mateusz Wos Date: Sun, 18 Oct 2026 17:33:05 +0200 Subject: [PATCH 52/52] docs: changelog for v1.6.0 (#477) --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba2325..2d3bad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## Decimal v1.6.0 + +#### BEHAVIOR CHANGES +- `Decimal` implements `fmt.Formatter`. `%#v` prints Go code that recreates the value, like `decimal.New(314, -2)`, instead of the struct fields, and verbs that don't apply, like `%d`, print `%!d(decimal.Decimal=3.14)`. `%v`, `%s`, `%q`, `%x` and `fmt.Println` print the same as before [#463](https://github.com/shopspring/decimal/pull/463) +- `Ln` rounds correctly, so some results change in the last digit: `Ln` of 10 to 5 places is `2.30259`, not `2.30258` [#471](https://github.com/shopspring/decimal/pull/471) +- `ExpHullAbrham` returns `overallPrecision` significant figures for every input. Negative inputs used to lose digits or return 0, like `-50` at precision 10, and large inputs returned extra digits [#429](https://github.com/shopspring/decimal/pull/429) [#470](https://github.com/shopspring/decimal/pull/470) +- `Scan` converts `float32` with `NewFromFloat32`, so `54.33` stays `54.33` instead of becoming `54.33000183105469`. NaN and infinite floats return an error instead of panicking [#469](https://github.com/shopspring/decimal/pull/469) +- On Go 1.10 to 1.12, `NewFromFloat` can pick a different shortest form in rare cases ([golang/go#29491](https://github.com/golang/go/issues/29491)). It still converts back to the same float [#431](https://github.com/shopspring/decimal/pull/431) + +#### FEATURES +- Add BSON Decimal128 support for `Decimal` and `NullDecimal` with the official MongoDB Go driver v2, without a new dependency [#460](https://github.com/shopspring/decimal/pull/460) +- Add `%f` formatting with precision, width and flags, like `%.2f` and `%08.2f` [#463](https://github.com/shopspring/decimal/pull/463) +- Add `Normalize`, which strips trailing zeros so equal decimals also match with `reflect.DeepEqual`, testify and gomock [#464](https://github.com/shopspring/decimal/pull/464) +- Add `Sqrt` and `SqrtRound` [#465](https://github.com/shopspring/decimal/pull/465) +- Add `Root` and `RootRound` for n-th roots [#473](https://github.com/shopspring/decimal/pull/473) +- Add `NewFromBigFloat` constructor [#457](https://github.com/shopspring/decimal/pull/457) +- `Scan` accepts `Decimal`, `big.Int`, `*big.Int` and all integer sizes, as passed by ClickHouse and other drivers [#469](https://github.com/shopspring/decimal/pull/469) + +#### ENHANCEMENTS +- `NewFromFloat` and `NewFromFloat32` are 3 to 6 times faster. They use `strconv` instead of a vendored copy of it from 2009, which is removed [#431](https://github.com/shopspring/decimal/pull/431) +- `Ln` is 3 to 14 times faster [#471](https://github.com/shopspring/decimal/pull/471) +- Slightly faster `IsInteger` and `Ceil` [#299](https://github.com/shopspring/decimal/pull/299) +- `NewFromString` accepts the Unicode minus sign `−` (U+2212) [#428](https://github.com/shopspring/decimal/pull/428) +- Scanning NULL into a `Decimal` returns an error that points to `NullDecimal` or `*Decimal` [#474](https://github.com/shopspring/decimal/pull/474) +- Document that global settings aren't goroutine-safe, and list them with their per-call alternatives in the README [#466](https://github.com/shopspring/decimal/pull/466) +- README: show how to control the JSON format of one field, add `udecimal` to the alternative libraries, and update the Go doc and playground links [#475](https://github.com/shopspring/decimal/pull/475) [#476](https://github.com/shopspring/decimal/pull/476) [#264](https://github.com/shopspring/decimal/pull/264) +- Fix doc examples that were wrong or didn't compile [#468](https://github.com/shopspring/decimal/pull/468) +- Fix benchmarks that measured their setup or the wrong function [#467](https://github.com/shopspring/decimal/pull/467) + +#### BUGFIXES +- `QuoRem` and `DivRound` panic on precision `math.MinInt32` instead of returning a wrong quotient [#472](https://github.com/shopspring/decimal/pull/472) + ## Decimal v1.5.0 #### BEHAVIOR CHANGES