Skip to content

Commit e1e92bf

Browse files
JohnADvasilbekk
andauthored
TrimTrailingZeros and AvoidScientificNotation bool variables (#389)
* Added varible TrimTrailingZeros * Scientific Notation support on serialization to honor negative precision scenarios --------- Co-authored-by: vasilbekk <vasilbekk@mail.ru> Co-authored-by: vasilbekk <70901384+vasilbekk@users.noreply.github.com>
1 parent 08afb35 commit e1e92bf

2 files changed

Lines changed: 197 additions & 8 deletions

File tree

decimal.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ var PowPrecisionNegativeExponent = 16
6565
// silently lose precision.
6666
var MarshalJSONWithoutQuotes = false
6767

68+
// TrimTrailingZeros specifies whether trailing zeroes should be trimmed from a string representation of decimal.
69+
// If set to true, trailing zeroes will be truncated (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13),
70+
// otherwise trailing zeroes will be preserved (2.00 -> 2.00, 3.11 -> 3.11, 13.000 -> 13.000).
71+
// Setting this value to false can be useful for APIs where exact decimal string representation matters.
72+
var TrimTrailingZeros = true
73+
74+
// AvoidScientificNotation specifies whether scientific notation should be used when decimal is turned
75+
// into a string that has a "negative" precision.
76+
//
77+
// For example, 1200 rounded to the nearest 100 cannot accurately be shown as "1200" because the last two
78+
// digits are unknown. With this set to false, that number would be expressed as "1.2E3" instead.
79+
var AvoidScientificNotation = true
80+
6881
// ExpMaxIterations specifies the maximum number of iterations needed to calculate
6982
// precise natural exponent value using ExpHullAbrham method.
7083
var ExpMaxIterations = 1000
@@ -1476,7 +1489,7 @@ func (d Decimal) InexactFloat64() float64 {
14761489
//
14771490
// -12.345
14781491
func (d Decimal) String() string {
1479-
return d.string(true)
1492+
return d.string(TrimTrailingZeros, AvoidScientificNotation)
14801493
}
14811494

14821495
// StringFixed returns a rounded fixed-point string with places digits after
@@ -1490,10 +1503,12 @@ func (d Decimal) String() string {
14901503
// NewFromFloat(5.45).StringFixed(1) // output: "5.5"
14911504
// NewFromFloat(5.45).StringFixed(2) // output: "5.45"
14921505
// NewFromFloat(5.45).StringFixed(3) // output: "5.450"
1493-
// NewFromFloat(545).StringFixed(-1) // output: "550"
1506+
// NewFromFloat(545).StringFixed(-1) // output: "540"
1507+
//
1508+
// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation.
14941509
func (d Decimal) StringFixed(places int32) string {
14951510
rounded := d.Round(places)
1496-
return rounded.string(false)
1511+
return rounded.string(false, true)
14971512
}
14981513

14991514
// StringFixedBank returns a banker rounded fixed-point string with places digits
@@ -1508,16 +1523,20 @@ func (d Decimal) StringFixed(places int32) string {
15081523
// NewFromFloat(5.45).StringFixedBank(2) // output: "5.45"
15091524
// NewFromFloat(5.45).StringFixedBank(3) // output: "5.450"
15101525
// NewFromFloat(545).StringFixedBank(-1) // output: "540"
1526+
//
1527+
// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation.
15111528
func (d Decimal) StringFixedBank(places int32) string {
15121529
rounded := d.RoundBank(places)
1513-
return rounded.string(false)
1530+
return rounded.string(false, true)
15141531
}
15151532

15161533
// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For
15171534
// more details see the documentation at function RoundCash.
1535+
//
1536+
// Regardless of the `AvoidScientificNotation` option, the returned string will never be in scientific notation.
15181537
func (d Decimal) StringFixedCash(interval uint8) string {
15191538
rounded := d.RoundCash(interval)
1520-
return rounded.string(false)
1539+
return rounded.string(false, true)
15211540
}
15221541

15231542
// Round rounds the decimal to places decimal places.
@@ -1526,7 +1545,7 @@ func (d Decimal) StringFixedCash(interval uint8) string {
15261545
// Example:
15271546
//
15281547
// NewFromFloat(5.45).Round(1).String() // output: "5.5"
1529-
// NewFromFloat(545).Round(-1).String() // output: "550"
1548+
// NewFromFloat(545).Round(-1).String() // output: "550" (with AvoidScientificNotation, "5.5E2" otherwise)
15301549
func (d Decimal) Round(places int32) Decimal {
15311550
if d.exp == -places {
15321551
return d
@@ -1911,10 +1930,17 @@ func (d Decimal) StringScaled(exp int32) string {
19111930
return d.rescale(exp).String()
19121931
}
19131932

1914-
func (d Decimal) string(trimTrailingZeros bool) string {
1915-
if d.exp >= 0 {
1933+
func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string {
1934+
if d.exp == 0 {
19161935
return d.rescale(0).value.String()
19171936
}
1937+
if d.exp >= 0 {
1938+
if avoidScientificNotation {
1939+
return d.rescale(0).value.String()
1940+
} else {
1941+
return d.ScientificNotationString()
1942+
}
1943+
}
19181944

19191945
abs := new(big.Int).Abs(d.value)
19201946
str := abs.String()
@@ -1956,6 +1982,31 @@ func (d Decimal) string(trimTrailingZeros bool) string {
19561982
return number
19571983
}
19581984

1985+
// ScientificNotationString serializes the decimal into standard scientific notation.
1986+
//
1987+
// The notation is normalized to have one non-zero digit followed by a decimal point and
1988+
// the remaining significant digits followed by "E" and the base-10 exponent.
1989+
//
1990+
// A zero, which has no significant digits, is simply serialized to "0".
1991+
func (d Decimal) ScientificNotationString() string {
1992+
exp := int(d.exp)
1993+
intStr := new(big.Int).Abs(d.value).String()
1994+
if intStr == "0" {
1995+
return intStr
1996+
}
1997+
first := intStr[0]
1998+
var remaining string
1999+
if len(intStr) > 1 {
2000+
remaining = "." + intStr[1:]
2001+
exp = exp + len(intStr) - 1
2002+
}
2003+
number := string(first) + remaining + "E" + strconv.Itoa(exp)
2004+
if d.value.Sign() < 0 {
2005+
return "-" + number
2006+
}
2007+
return number
2008+
}
2009+
19592010
func (d *Decimal) ensureInitialized() {
19602011
if d.value == nil {
19612012
d.value = new(big.Int)

decimal_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3661,3 +3661,141 @@ func ExampleNewFromFloat() {
36613661
//0.123123123123123
36623662
//-10000000000000
36633663
}
3664+
3665+
func TestDecimal_String(t *testing.T) {
3666+
type testData struct {
3667+
input string
3668+
expected string
3669+
}
3670+
3671+
tests := []testData{
3672+
{"1.22", "1.22"},
3673+
{"1.00", "1"},
3674+
{"153.192", "153.192"},
3675+
{"999.999", "999.999"},
3676+
{"0.0000000001", "0.0000000001"},
3677+
{"0.0000000000", "0"},
3678+
}
3679+
3680+
for _, test := range tests {
3681+
d, err := NewFromString(test.input)
3682+
if err != nil {
3683+
t.Fatal(err)
3684+
} else if d.String() != test.expected {
3685+
t.Errorf("expected %s, got %s", test.expected, d.String())
3686+
}
3687+
}
3688+
}
3689+
3690+
func TestDecimal_StringWithTrailing(t *testing.T) {
3691+
type testData struct {
3692+
input string
3693+
expected string
3694+
}
3695+
3696+
defer func() {
3697+
TrimTrailingZeros = true
3698+
}()
3699+
3700+
TrimTrailingZeros = false
3701+
tests := []testData{
3702+
{"1.00", "1.00"},
3703+
{"0.00", "0.00"},
3704+
{"129.123000", "129.123000"},
3705+
{"1.0000E3", "1000.0"}, // 1000 to the nearest tenth
3706+
{"10000E-1", "1000.0"}, // 1000 to the nearest tenth
3707+
}
3708+
3709+
for _, test := range tests {
3710+
d, err := NewFromString(test.input)
3711+
if err != nil {
3712+
t.Fatal(err)
3713+
} else if d.String() != test.expected {
3714+
x := d.String()
3715+
fmt.Println(x)
3716+
t.Errorf("expected %s, got %s", test.expected, d.String())
3717+
}
3718+
}
3719+
}
3720+
3721+
func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) {
3722+
type testData struct {
3723+
input string
3724+
expected string
3725+
}
3726+
3727+
defer func() {
3728+
AvoidScientificNotation = true
3729+
}()
3730+
AvoidScientificNotation = false
3731+
3732+
tests := []testData{
3733+
{"1.0E3", "1.0E3"}, // 1000 to the nearest hundred
3734+
{"1.00E3", "1.00E3"}, // 1000 to the nearest ten
3735+
{"1.000E3", "1000"}, // 1000 to the nearest one
3736+
{"1E3", "1E3"}, // 1000 to the nearest thousand
3737+
{"-1E3", "-1E3"}, // -1000 to the nearest thousand
3738+
}
3739+
3740+
for _, test := range tests {
3741+
d, err := NewFromString(test.input)
3742+
if err != nil {
3743+
t.Fatal(err)
3744+
} else if d.String() != test.expected {
3745+
x := d.String()
3746+
fmt.Println(x)
3747+
t.Errorf("expected %s, got %s", test.expected, d.String())
3748+
}
3749+
}
3750+
}
3751+
3752+
func TestDecimal_ScientificNotation(t *testing.T) {
3753+
type testData struct {
3754+
input string
3755+
expected string
3756+
}
3757+
3758+
tests := []testData{
3759+
{"1", "1E0"},
3760+
{"1.0", "1.0E0"},
3761+
{"10", "1.0E1"},
3762+
{"123", "1.23E2"},
3763+
{"1234", "1.234E3"},
3764+
{"-1", "-1E0"},
3765+
{"-10", "-1.0E1"},
3766+
{"-123", "-1.23E2"},
3767+
{"-1234", "-1.234E3"},
3768+
{"0.1", "1E-1"},
3769+
{"0.01", "1E-2"},
3770+
{"0.123", "1.23E-1"},
3771+
{"1.23", "1.23E0"},
3772+
{"-0.1", "-1E-1"},
3773+
{"-0.01", "-1E-2"},
3774+
{"-0.010", "-1.0E-2"},
3775+
{"-0.123", "-1.23E-1"},
3776+
{"-1.23", "-1.23E0"},
3777+
{"1E6", "1E6"},
3778+
{"1e6", "1E6"},
3779+
{"1.23E6", "1.23E6"},
3780+
{"-1E6", "-1E6"},
3781+
{"1E-6", "1E-6"},
3782+
{"1.23E-6", "1.23E-6"},
3783+
{"-1E-6", "-1E-6"},
3784+
{"-1.0E-6", "-1.0E-6"},
3785+
{"12345600", "1.2345600E7"},
3786+
{"123456E2", "1.23456E7"},
3787+
{"0", "0"},
3788+
{"0E1", "0"},
3789+
{"-0", "0"},
3790+
{"-0.000", "0"},
3791+
}
3792+
3793+
for _, test := range tests {
3794+
d, err := NewFromString(test.input)
3795+
if err != nil {
3796+
t.Fatal(err)
3797+
} else if d.ScientificNotationString() != test.expected {
3798+
t.Errorf("expected %s, got %s", test.expected, d.ScientificNotationString())
3799+
}
3800+
}
3801+
}

0 commit comments

Comments
 (0)