Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TrimTrailingZeros and AvoidScientificNotation bool variables #389

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
67 changes: 59 additions & 8 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1469,7 +1482,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
Expand All @@ -1483,10 +1496,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
Expand All @@ -1501,16 +1516,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.
Expand All @@ -1519,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
Expand Down Expand Up @@ -1905,10 +1924,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()
Expand Down Expand Up @@ -1950,6 +1976,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)
Expand Down
138 changes: 138 additions & 0 deletions decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3647,3 +3647,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())
}
}
}