diff --git a/decimal.go b/decimal.go index 2cf6bc0..26277ec 100644 --- a/decimal.go +++ b/decimal.go @@ -1460,6 +1460,73 @@ func (d Decimal) String() string { return d.string(true) } +// Format formats a decimal. +// thousandsSeparator can be empty, in which case the integer value will be displayed without separation. +// if decimalSeparator is empty and the value is a decimal this will panic. +func (d Decimal) Format(thousandsSeparator string, decimalSeparator string, trimTrailingZeros bool) string { + if d.exp >= 0 { + d = d.rescale(0) + } + + abs := new(big.Int).Abs(d.value) + str := abs.String() + + var intPart, 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. + dExpInt := int(d.exp) + if len(str) > -dExpInt { + intPart = str[:len(str)+dExpInt] + fractionalPart = str[len(str)+dExpInt:] + } else { + intPart = "0" + + num0s := -dExpInt - len(str) + fractionalPart = strings.Repeat("0", num0s) + str + } + + if thousandsSeparator != "" { + parts := 1 + (len(intPart)-1)/3 + if parts > 1 { + intParts := make([]string, 1+(len(intPart)-1)/3) + offset := len(intPart) - (len(intParts)-1)*3 + for i := 0; i < len(intParts); i++ { + if i == 0 { + intParts[i] = intPart[0:offset] + } else { + intParts[i] = intPart[(i-1)*3+offset : i*3+offset] + } + } + intPart = strings.Join(intParts, thousandsSeparator) + } + } + + if trimTrailingZeros { + i := len(fractionalPart) - 1 + for ; i >= 0; i-- { + if fractionalPart[i] != '0' { + break + } + } + fractionalPart = fractionalPart[:i+1] + } + if fractionalPart != "" && decimalSeparator == "" { + panic("no decimal separator for non-integer") + } + + number := intPart + if len(fractionalPart) > 0 { + number += decimalSeparator + fractionalPart + } + + if d.value.Sign() < 0 { + return "-" + number + } + + return number +} + // StringFixed returns a rounded fixed-point string with places digits after // the decimal point. // @@ -1889,48 +1956,7 @@ func (d Decimal) StringScaled(exp int32) string { } func (d Decimal) string(trimTrailingZeros bool) string { - if d.exp >= 0 { - return d.rescale(0).value.String() - } - - abs := new(big.Int).Abs(d.value) - str := abs.String() - - var intPart, 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. - dExpInt := int(d.exp) - if len(str) > -dExpInt { - intPart = str[:len(str)+dExpInt] - fractionalPart = str[len(str)+dExpInt:] - } else { - intPart = "0" - - num0s := -dExpInt - len(str) - fractionalPart = strings.Repeat("0", num0s) + str - } - - if trimTrailingZeros { - i := len(fractionalPart) - 1 - for ; i >= 0; i-- { - if fractionalPart[i] != '0' { - break - } - } - fractionalPart = fractionalPart[:i+1] - } - - number := intPart - if len(fractionalPart) > 0 { - number += "." + fractionalPart - } - - if d.value.Sign() < 0 { - return "-" + number - } - - return number + return d.Format("", ".", trimTrailingZeros) } func (d *Decimal) ensureInitialized() { diff --git a/decimal_test.go b/decimal_test.go index 0905ce8..2ae88b4 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -1513,6 +1513,47 @@ func TestDecimal_RoundDownAndStringFixed(t *testing.T) { } } +func TestDecimal_Format(t *testing.T) { + type testData struct { + input string + thousandsSeparator string + decimalSeparator string + trimTrailingZeros bool + expected string + } + tests := []testData{ + {"0", ",", ".", false, "0"}, + {"0", ",", ".", true, "0"}, + {"999", ",", ".", true, "999"}, + {"1000", ",", ".", true, "1,000"}, + {"123", ",", ".", true, "123"}, + {"1234", ",", ".", true, "1,234"}, + {"12345.67", "", ".", true, "12345.67"}, + {"12345.00", ",", ".", true, "12,345"}, + {"12345.00", ",", ".", false, "12,345.00"}, + {"123456.00", ",", ".", false, "123,456.00"}, + {"1234567.00", ",", ".", false, "1,234,567.00"}, + {"1234567.00", ".", ",", false, "1.234.567,00"}, + {"1234567.00", "_", ".", true, "1_234_567"}, + {"-12.00", "_", ".", true, "-12"}, + {"-123.00", "_", ".", true, "-123"}, + {"-1234.00", "_", ".", true, "-1_234"}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + panic(err) + } + + got := d.Format(test.thousandsSeparator, test.decimalSeparator, test.trimTrailingZeros) + if got != test.expected { + t.Errorf("Format %s got %s, expected %s", + d, got, test.expected) + } + } +} + func TestDecimal_BankRoundAndStringFixed(t *testing.T) { type testData struct { input string