From ba54fe14183e144863b9bdadd7beb903d5ca86f7 Mon Sep 17 00:00:00 2001 From: vasilbekk Date: Sat, 1 Oct 2022 13:47:11 +0300 Subject: [PATCH 1/8] Added varible StringTrimTrailingZeros and comment for it --- decimal.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index c614ea79..84dd32f7 100644 --- a/decimal.go +++ b/decimal.go @@ -52,6 +52,14 @@ var DivisionPrecision = 16 // silently lose precision. var MarshalJSONWithoutQuotes = false +// StringTrimTrailingZeros should be set to false if you want the decimal stringify without zeros trailing. +// By default, when decimal is output as a string (for example, in JSON), zeros are truncated from it (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13). +// But this logic can be changed by this variable. +// For example, if you have numeric(10,2) values stored in your database, +// and you want your API response to always be given 2 decimal places (even 2.00, 3.00, 17.00 [not 2,3,17]), +// then this variable is a great way out. +var StringTrimTrailingZeros = true + // ExpMaxIterations specifies the maximum number of iterations needed to calculate // precise natural exponent value using ExpHullAbrham method. var ExpMaxIterations = 1000 @@ -1022,7 +1030,7 @@ func (d Decimal) InexactFloat64() float64 { // -12.345 // func (d Decimal) String() string { - return d.string(true) + return d.string(StringTrimTrailingZeros) } // StringFixed returns a rounded fixed-point string with places digits after From d23c457004b890e4b2604217bdf9ca81365b97f7 Mon Sep 17 00:00:00 2001 From: vasilbekk Date: Sat, 1 Oct 2022 14:02:56 +0300 Subject: [PATCH 2/8] Added unittests --- decimal_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/decimal_test.go b/decimal_test.go index 2b3a99e1..14b135de 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3339,3 +3339,51 @@ 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()) + } + } + + defer func() { + StringTrimTrailingZeros = true + }() + + StringTrimTrailingZeros = false + tests = []testData{ + {"1.00", "1.00"}, + {"0.00", "0.00"}, + {"129.123000", "129.123000"}, + } + + 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()) + } + } + + + +} \ No newline at end of file From 6e54f647f022cc99bd67f8fd995e7db67a2ed2df Mon Sep 17 00:00:00 2001 From: vasilbekk Date: Sat, 1 Oct 2022 14:20:16 +0300 Subject: [PATCH 3/8] Newline in end file --- decimal_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/decimal_test.go b/decimal_test.go index 14b135de..5c34a1ba 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3383,7 +3383,4 @@ func TestDecimal_String(t *testing.T) { t.Errorf("expected %s, got %s", test.expected, d.String()) } } - - - -} \ No newline at end of file +} From e11ab9a8116b1cf16340e05d035cf43190d31f04 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 8 Dec 2024 19:06:24 -0600 Subject: [PATCH 4/8] Changed option var to TrimTrailingZeros and it's description --- decimal.go | 14 ++++++-------- decimal_test.go | 10 +++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/decimal.go b/decimal.go index c081a22e..ed44ed2b 100644 --- a/decimal.go +++ b/decimal.go @@ -65,13 +65,11 @@ var PowPrecisionNegativeExponent = 16 // silently lose precision. var MarshalJSONWithoutQuotes = false -// StringTrimTrailingZeros should be set to false if you want the decimal stringify without zeros trailing. -// By default, when decimal is output as a string (for example, in JSON), zeros are truncated from it (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13). -// But this logic can be changed by this variable. -// For example, if you have numeric(10,2) values stored in your database, -// and you want your API response to always be given 2 decimal places (even 2.00, 3.00, 17.00 [not 2,3,17]), -// then this variable is a great way out. -var StringTrimTrailingZeros = true +// 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 // ExpMaxIterations specifies the maximum number of iterations needed to calculate // precise natural exponent value using ExpHullAbrham method. @@ -1477,7 +1475,7 @@ func (d Decimal) InexactFloat64() float64 { // // -12.345 func (d Decimal) String() string { - return d.string(StringTrimTrailingZeros) + return d.string(TrimTrailingZeros) } // StringFixed returns a rounded fixed-point string with places digits after diff --git a/decimal_test.go b/decimal_test.go index e3a0ac1e..ae3755b1 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3650,7 +3650,7 @@ func ExampleNewFromFloat() { func TestDecimal_String(t *testing.T) { type testData struct { - input string + input string expected string } @@ -3664,7 +3664,7 @@ func TestDecimal_String(t *testing.T) { } for _, test := range tests { - d, err := NewFromString(test.input); + d, err := NewFromString(test.input) if err != nil { t.Fatal(err) } else if d.String() != test.expected { @@ -3673,10 +3673,10 @@ func TestDecimal_String(t *testing.T) { } defer func() { - StringTrimTrailingZeros = true + TrimTrailingZeros = true }() - StringTrimTrailingZeros = false + TrimTrailingZeros = false tests = []testData{ {"1.00", "1.00"}, {"0.00", "0.00"}, @@ -3684,7 +3684,7 @@ func TestDecimal_String(t *testing.T) { } for _, test := range tests { - d, err := NewFromString(test.input); + d, err := NewFromString(test.input) if err != nil { t.Fatal(err) } else if d.String() != test.expected { From 8d841babad13d509eddf8b862e5b608501f60436 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 8 Dec 2024 19:56:57 -0600 Subject: [PATCH 5/8] Separate Decimal string tests --- decimal_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/decimal_test.go b/decimal_test.go index ae3755b1..f76f8498 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3671,13 +3671,20 @@ func TestDecimal_String(t *testing.T) { 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{ + tests := []testData{ {"1.00", "1.00"}, {"0.00", "0.00"}, {"129.123000", "129.123000"}, From 0f2c9fe737962436d583d12fcb16beda16f5e0d1 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 8 Dec 2024 20:05:34 -0600 Subject: [PATCH 6/8] Added tests for proper before-point trailing zero serialization --- decimal_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/decimal_test.go b/decimal_test.go index f76f8498..7f1e0bd8 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3688,6 +3688,10 @@ func TestDecimal_StringWithTrailing(t *testing.T) { {"1.00", "1.00"}, {"0.00", "0.00"}, {"129.123000", "129.123000"}, + {"1.0000E3", "1000.0"}, // 1000 to the nearest tenth + {"1.000E3", "1000"}, // 1000 to the nearest one + {"1.0E3", "1.0E3"}, // 1000 to the nearest hundred + {"1E3", "1E3"}, // 1000 to the nearest thousand } for _, test := range tests { From 6453f65042cded50e50c96b029da831b1ddb3a6f Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 8 Dec 2024 22:39:28 -0600 Subject: [PATCH 7/8] Scientific Notation support on serialization to honor negative precision scenarios --- decimal.go | 53 +++++++++++++++++++++++++---- decimal_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/decimal.go b/decimal.go index ed44ed2b..bb2075c7 100644 --- a/decimal.go +++ b/decimal.go @@ -71,6 +71,13 @@ 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 +// 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 @@ -1475,7 +1482,7 @@ func (d Decimal) InexactFloat64() float64 { // // -12.345 func (d Decimal) String() string { - return d.string(TrimTrailingZeros) + return d.string(TrimTrailingZeros, AvoidScientificNotation) } // StringFixed returns a rounded fixed-point string with places digits after @@ -1489,10 +1496,10 @@ 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" 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 @@ -1509,14 +1516,14 @@ func (d Decimal) StringFixed(places int32) string { // NewFromFloat(545).StringFixedBank(-1) // output: "540" 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. 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. @@ -1911,10 +1918,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 +1970,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 7f1e0bd8..1cb37bb5 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -3689,9 +3689,7 @@ func TestDecimal_StringWithTrailing(t *testing.T) { {"0.00", "0.00"}, {"129.123000", "129.123000"}, {"1.0000E3", "1000.0"}, // 1000 to the nearest tenth - {"1.000E3", "1000"}, // 1000 to the nearest one - {"1.0E3", "1.0E3"}, // 1000 to the nearest hundred - {"1E3", "1E3"}, // 1000 to the nearest thousand + {"10000E-1", "1000.0"}, // 1000 to the nearest tenth } for _, test := range tests { @@ -3699,7 +3697,91 @@ func TestDecimal_StringWithTrailing(t *testing.T) { 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 ae252abe947cc72f48e455afcea55940bba7318a Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 8 Dec 2024 22:48:27 -0600 Subject: [PATCH 8/8] added notes re AvoidScientificNotation --- decimal.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index bb2075c7..cc7fad3f 100644 --- a/decimal.go +++ b/decimal.go @@ -1497,6 +1497,8 @@ func (d Decimal) String() string { // NewFromFloat(5.45).StringFixed(2) // output: "5.45" // 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. func (d Decimal) StringFixed(places int32) string { rounded := d.Round(places) return rounded.string(false, true) @@ -1514,6 +1516,8 @@ 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, true) @@ -1521,6 +1525,8 @@ func (d Decimal) StringFixedBank(places int32) string { // 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, true) @@ -1532,7 +1538,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