Skip to content

Commit 6453f65

Browse files
committed
Scientific Notation support on serialization to honor negative precision scenarios
1 parent 0f2c9fe commit 6453f65

File tree

2 files changed

+131
-10
lines changed

2 files changed

+131
-10
lines changed

decimal.go

+46-7
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ var MarshalJSONWithoutQuotes = false
7171
// Setting this value to false can be useful for APIs where exact decimal string representation matters.
7272
var TrimTrailingZeros = true
7373

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+
7481
// ExpMaxIterations specifies the maximum number of iterations needed to calculate
7582
// precise natural exponent value using ExpHullAbrham method.
7683
var ExpMaxIterations = 1000
@@ -1475,7 +1482,7 @@ func (d Decimal) InexactFloat64() float64 {
14751482
//
14761483
// -12.345
14771484
func (d Decimal) String() string {
1478-
return d.string(TrimTrailingZeros)
1485+
return d.string(TrimTrailingZeros, AvoidScientificNotation)
14791486
}
14801487

14811488
// StringFixed returns a rounded fixed-point string with places digits after
@@ -1489,10 +1496,10 @@ func (d Decimal) String() string {
14891496
// NewFromFloat(5.45).StringFixed(1) // output: "5.5"
14901497
// NewFromFloat(5.45).StringFixed(2) // output: "5.45"
14911498
// NewFromFloat(5.45).StringFixed(3) // output: "5.450"
1492-
// NewFromFloat(545).StringFixed(-1) // output: "550"
1499+
// NewFromFloat(545).StringFixed(-1) // output: "540"
14931500
func (d Decimal) StringFixed(places int32) string {
14941501
rounded := d.Round(places)
1495-
return rounded.string(false)
1502+
return rounded.string(false, true)
14961503
}
14971504

14981505
// StringFixedBank returns a banker rounded fixed-point string with places digits
@@ -1509,14 +1516,14 @@ func (d Decimal) StringFixed(places int32) string {
15091516
// NewFromFloat(545).StringFixedBank(-1) // output: "540"
15101517
func (d Decimal) StringFixedBank(places int32) string {
15111518
rounded := d.RoundBank(places)
1512-
return rounded.string(false)
1519+
return rounded.string(false, true)
15131520
}
15141521

15151522
// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For
15161523
// more details see the documentation at function RoundCash.
15171524
func (d Decimal) StringFixedCash(interval uint8) string {
15181525
rounded := d.RoundCash(interval)
1519-
return rounded.string(false)
1526+
return rounded.string(false, true)
15201527
}
15211528

15221529
// Round rounds the decimal to places decimal places.
@@ -1911,10 +1918,17 @@ func (d Decimal) StringScaled(exp int32) string {
19111918
return d.rescale(exp).String()
19121919
}
19131920

1914-
func (d Decimal) string(trimTrailingZeros bool) string {
1915-
if d.exp >= 0 {
1921+
func (d Decimal) string(trimTrailingZeros, avoidScientificNotation bool) string {
1922+
if d.exp == 0 {
19161923
return d.rescale(0).value.String()
19171924
}
1925+
if d.exp >= 0 {
1926+
if avoidScientificNotation {
1927+
return d.rescale(0).value.String()
1928+
} else {
1929+
return d.ScientificNotationString()
1930+
}
1931+
}
19181932

19191933
abs := new(big.Int).Abs(d.value)
19201934
str := abs.String()
@@ -1956,6 +1970,31 @@ func (d Decimal) string(trimTrailingZeros bool) string {
19561970
return number
19571971
}
19581972

1973+
// ScientificNotationString serializes the decimal into standard scientific notation.
1974+
//
1975+
// The notation is normalized to have one non-zero digit followed by a decimal point and
1976+
// the remaining significant digits followed by "E" and the base-10 exponent.
1977+
//
1978+
// A zero, which has no significant digits, is simply serialized to "0".
1979+
func (d Decimal) ScientificNotationString() string {
1980+
exp := int(d.exp)
1981+
intStr := new(big.Int).Abs(d.value).String()
1982+
if intStr == "0" {
1983+
return intStr
1984+
}
1985+
first := intStr[0]
1986+
var remaining string
1987+
if len(intStr) > 1 {
1988+
remaining = "." + intStr[1:]
1989+
exp = exp + len(intStr) - 1
1990+
}
1991+
number := string(first) + remaining + "E" + strconv.Itoa(exp)
1992+
if d.value.Sign() < 0 {
1993+
return "-" + number
1994+
}
1995+
return number
1996+
}
1997+
19591998
func (d *Decimal) ensureInitialized() {
19601999
if d.value == nil {
19612000
d.value = new(big.Int)

decimal_test.go

+85-3
Original file line numberDiff line numberDiff line change
@@ -3689,17 +3689,99 @@ func TestDecimal_StringWithTrailing(t *testing.T) {
36893689
{"0.00", "0.00"},
36903690
{"129.123000", "129.123000"},
36913691
{"1.0000E3", "1000.0"}, // 1000 to the nearest tenth
3692-
{"1.000E3", "1000"}, // 1000 to the nearest one
3693-
{"1.0E3", "1.0E3"}, // 1000 to the nearest hundred
3694-
{"1E3", "1E3"}, // 1000 to the nearest thousand
3692+
{"10000E-1", "1000.0"}, // 1000 to the nearest tenth
36953693
}
36963694

36973695
for _, test := range tests {
36983696
d, err := NewFromString(test.input)
36993697
if err != nil {
37003698
t.Fatal(err)
37013699
} else if d.String() != test.expected {
3700+
x := d.String()
3701+
fmt.Println(x)
37023702
t.Errorf("expected %s, got %s", test.expected, d.String())
37033703
}
37043704
}
37053705
}
3706+
3707+
func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) {
3708+
type testData struct {
3709+
input string
3710+
expected string
3711+
}
3712+
3713+
defer func() {
3714+
AvoidScientificNotation = true
3715+
}()
3716+
AvoidScientificNotation = false
3717+
3718+
tests := []testData{
3719+
{"1.0E3", "1.0E3"}, // 1000 to the nearest hundred
3720+
{"1.00E3", "1.00E3"}, // 1000 to the nearest ten
3721+
{"1.000E3", "1000"}, // 1000 to the nearest one
3722+
{"1E3", "1E3"}, // 1000 to the nearest thousand
3723+
{"-1E3", "-1E3"}, // -1000 to the nearest thousand
3724+
}
3725+
3726+
for _, test := range tests {
3727+
d, err := NewFromString(test.input)
3728+
if err != nil {
3729+
t.Fatal(err)
3730+
} else if d.String() != test.expected {
3731+
x := d.String()
3732+
fmt.Println(x)
3733+
t.Errorf("expected %s, got %s", test.expected, d.String())
3734+
}
3735+
}
3736+
}
3737+
3738+
func TestDecimal_ScientificNotation(t *testing.T) {
3739+
type testData struct {
3740+
input string
3741+
expected string
3742+
}
3743+
3744+
tests := []testData{
3745+
{"1", "1E0"},
3746+
{"1.0", "1.0E0"},
3747+
{"10", "1.0E1"},
3748+
{"123", "1.23E2"},
3749+
{"1234", "1.234E3"},
3750+
{"-1", "-1E0"},
3751+
{"-10", "-1.0E1"},
3752+
{"-123", "-1.23E2"},
3753+
{"-1234", "-1.234E3"},
3754+
{"0.1", "1E-1"},
3755+
{"0.01", "1E-2"},
3756+
{"0.123", "1.23E-1"},
3757+
{"1.23", "1.23E0"},
3758+
{"-0.1", "-1E-1"},
3759+
{"-0.01", "-1E-2"},
3760+
{"-0.010", "-1.0E-2"},
3761+
{"-0.123", "-1.23E-1"},
3762+
{"-1.23", "-1.23E0"},
3763+
{"1E6", "1E6"},
3764+
{"1e6", "1E6"},
3765+
{"1.23E6", "1.23E6"},
3766+
{"-1E6", "-1E6"},
3767+
{"1E-6", "1E-6"},
3768+
{"1.23E-6", "1.23E-6"},
3769+
{"-1E-6", "-1E-6"},
3770+
{"-1.0E-6", "-1.0E-6"},
3771+
{"12345600", "1.2345600E7"},
3772+
{"123456E2", "1.23456E7"},
3773+
{"0", "0"},
3774+
{"0E1", "0"},
3775+
{"-0", "0"},
3776+
{"-0.000", "0"},
3777+
}
3778+
3779+
for _, test := range tests {
3780+
d, err := NewFromString(test.input)
3781+
if err != nil {
3782+
t.Fatal(err)
3783+
} else if d.ScientificNotationString() != test.expected {
3784+
t.Errorf("expected %s, got %s", test.expected, d.ScientificNotationString())
3785+
}
3786+
}
3787+
}

0 commit comments

Comments
 (0)