mirror of
https://github.com/shopspring/decimal.git
synced 2024-11-23 04:40:49 +01:00
add Round, Floor, Ceil, and StringFixed. Deprecate StringScaled
This commit is contained in:
parent
ac401f1975
commit
0c74aee0c1
2 changed files with 214 additions and 9 deletions
77
decimal.go
77
decimal.go
|
@ -46,8 +46,10 @@ var DivisionPrecision = 16
|
||||||
// Zero constant, to make computations faster.
|
// Zero constant, to make computations faster.
|
||||||
var Zero = New(0, 1)
|
var Zero = New(0, 1)
|
||||||
|
|
||||||
var tenInt = big.NewInt(10)
|
var zeroInt = big.NewInt(0)
|
||||||
var oneInt = big.NewInt(1)
|
var oneInt = big.NewInt(1)
|
||||||
|
var fiveInt = big.NewInt(5)
|
||||||
|
var tenInt = big.NewInt(10)
|
||||||
|
|
||||||
// Decimal represents a fixed-point decimal. It is immutable.
|
// Decimal represents a fixed-point decimal. It is immutable.
|
||||||
// number = value * 10 ^ exp
|
// number = value * 10 ^ exp
|
||||||
|
@ -332,6 +334,10 @@ func (d Decimal) Float64() (f float64, exact bool) {
|
||||||
// -12.345
|
// -12.345
|
||||||
//
|
//
|
||||||
func (d Decimal) String() string {
|
func (d Decimal) String() string {
|
||||||
|
return d.string(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal) string(trimTrailingZeros bool) string {
|
||||||
if d.exp >= 0 {
|
if d.exp >= 0 {
|
||||||
return d.rescale(0).value.String()
|
return d.rescale(0).value.String()
|
||||||
}
|
}
|
||||||
|
@ -351,6 +357,7 @@ func (d Decimal) String() string {
|
||||||
fractionalPart = strings.Repeat("0", num0s) + str
|
fractionalPart = strings.Repeat("0", num0s) + str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if trimTrailingZeros {
|
||||||
i := len(fractionalPart) - 1
|
i := len(fractionalPart) - 1
|
||||||
for ; i >= 0; i-- {
|
for ; i >= 0; i-- {
|
||||||
if fractionalPart[i] != '0' {
|
if fractionalPart[i] != '0' {
|
||||||
|
@ -358,6 +365,7 @@ func (d Decimal) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fractionalPart = fractionalPart[:i+1]
|
fractionalPart = fractionalPart[:i+1]
|
||||||
|
}
|
||||||
|
|
||||||
number := intPart
|
number := intPart
|
||||||
if len(fractionalPart) > 0 {
|
if len(fractionalPart) > 0 {
|
||||||
|
@ -371,9 +379,64 @@ func (d Decimal) String() string {
|
||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringScaled first scales the decimal then calls .String() on it.
|
// StringFixed returns a rounded fixed-point string with places digits after
|
||||||
func (d Decimal) StringScaled(exp int32) string {
|
// the decimal point.
|
||||||
return d.rescale(exp).String()
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// NewFromFloat(0).StringFixed(2) // output: "0.00"
|
||||||
|
// NewFromFloat(0).StringFixed(0) // output: "0"
|
||||||
|
// NewFromFloat(5.45).StringFixed(0) // output: "5"
|
||||||
|
// 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"
|
||||||
|
//
|
||||||
|
func (d Decimal) StringFixed(places int32) string {
|
||||||
|
rounded := d.Round(places)
|
||||||
|
return rounded.string(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round rounds the decimal to places decimal places.
|
||||||
|
// If places < 0, it will round the integer part to the nearest 10^(-places).
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// NewFromFloat(5.45).Round(1).String() // output: "5.5"
|
||||||
|
// NewFromFloat(545).Round(-1).String() // output: "550"
|
||||||
|
//
|
||||||
|
func (d Decimal) Round(places int32) Decimal {
|
||||||
|
almost := d.rescale(-(places + 1))
|
||||||
|
if almost.value.Sign() < 0 {
|
||||||
|
almost.value.Sub(almost.value, fiveInt)
|
||||||
|
} else {
|
||||||
|
almost.value.Add(almost.value, fiveInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, m := almost.value.DivMod(almost.value, tenInt, new(big.Int))
|
||||||
|
almost.exp += 1
|
||||||
|
if almost.value.Sign() < 0 && m.Cmp(zeroInt) != 0 {
|
||||||
|
almost.value.Add(almost.value, oneInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return almost
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal) Floor() Decimal {
|
||||||
|
exp := big.NewInt(10)
|
||||||
|
exp.Exp(exp, big.NewInt(int64(-d.exp)), nil)
|
||||||
|
z := new(big.Int).Div(d.value, exp)
|
||||||
|
return Decimal{value: z, exp: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal) Ceil() Decimal {
|
||||||
|
exp := big.NewInt(10)
|
||||||
|
exp.Exp(exp, big.NewInt(int64(-d.exp)), nil)
|
||||||
|
z, m := new(big.Int).DivMod(d.value, exp, new(big.Int))
|
||||||
|
if m.Cmp(zeroInt) != 0 {
|
||||||
|
z.Add(z, oneInt)
|
||||||
|
}
|
||||||
|
return Decimal{value: z, exp: 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
@ -447,6 +510,12 @@ func (d Decimal) MarshalText() (text []byte, err error) {
|
||||||
return []byte(d.String()), nil
|
return []byte(d.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: buggy, unintuitive, and DEPRECATED! Use StringFixed instead.
|
||||||
|
// StringScaled first scales the decimal then calls .String() on it.
|
||||||
|
func (d Decimal) StringScaled(exp int32) string {
|
||||||
|
return d.rescale(exp).String()
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Decimal) ensureInitialized() {
|
func (d *Decimal) ensureInitialized() {
|
||||||
if d.value == nil {
|
if d.value == nil {
|
||||||
d.value = new(big.Int)
|
d.value = new(big.Int)
|
||||||
|
|
136
decimal_test.go
136
decimal_test.go
|
@ -2,6 +2,7 @@ package decimal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -189,6 +190,141 @@ func TestDecimal_rescale(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecimal_Floor(t *testing.T) {
|
||||||
|
type testData struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
tests := []testData{
|
||||||
|
{"1.999", "1"},
|
||||||
|
{"1", "1"},
|
||||||
|
{"1.01", "1"},
|
||||||
|
{"0", "0"},
|
||||||
|
{"0.9", "0"},
|
||||||
|
{"0.1", "0"},
|
||||||
|
{"-0.9", "-1"},
|
||||||
|
{"-0.1", "-1"},
|
||||||
|
{"-1.00", "-1"},
|
||||||
|
{"-1.01", "-2"},
|
||||||
|
{"-1.999", "-2"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
d, _ := NewFromString(test.input)
|
||||||
|
expected, _ := NewFromString(test.expected)
|
||||||
|
got := d.Floor()
|
||||||
|
if !got.Equals(expected) {
|
||||||
|
t.Errorf("Floor(%s): got %s, expected %s", d, got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecimal_Ceil(t *testing.T) {
|
||||||
|
type testData struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
tests := []testData{
|
||||||
|
{"1.999", "2"},
|
||||||
|
{"1", "1"},
|
||||||
|
{"1.01", "2"},
|
||||||
|
{"0", "0"},
|
||||||
|
{"0.9", "1"},
|
||||||
|
{"0.1", "1"},
|
||||||
|
{"-0.9", "0"},
|
||||||
|
{"-0.1", "0"},
|
||||||
|
{"-1.00", "-1"},
|
||||||
|
{"-1.01", "-1"},
|
||||||
|
{"-1.999", "-1"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
d, _ := NewFromString(test.input)
|
||||||
|
expected, _ := NewFromString(test.expected)
|
||||||
|
got := d.Ceil()
|
||||||
|
if !got.Equals(expected) {
|
||||||
|
t.Errorf("Ceil(%s): got %s, expected %s", d, got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecimal_RoundAndStringFixed(t *testing.T) {
|
||||||
|
type testData struct {
|
||||||
|
input string
|
||||||
|
places int32
|
||||||
|
expected string
|
||||||
|
expectedFixed string
|
||||||
|
}
|
||||||
|
tests := []testData{
|
||||||
|
{"1.454", 0, "1", ""},
|
||||||
|
{"1.454", 1, "1.5", ""},
|
||||||
|
{"1.454", 2, "1.45", ""},
|
||||||
|
{"1.454", 3, "1.454", ""},
|
||||||
|
{"1.454", 4, "1.454", "1.4540"},
|
||||||
|
{"1.454", 5, "1.454", "1.45400"},
|
||||||
|
{"1.554", 0, "2", ""},
|
||||||
|
{"1.554", 1, "1.6", ""},
|
||||||
|
{"1.554", 2, "1.55", ""},
|
||||||
|
{"0.554", 0, "1", ""},
|
||||||
|
{"0.454", 0, "0", ""},
|
||||||
|
{"0.454", 5, "0.454", "0.45400"},
|
||||||
|
{"0", 0, "0", ""},
|
||||||
|
{"0", 1, "0", "0.0"},
|
||||||
|
{"0", 2, "0", "0.00"},
|
||||||
|
{"0", -1, "0", ""},
|
||||||
|
{"5", 2, "5", "5.00"},
|
||||||
|
{"5", 1, "5", "5.0"},
|
||||||
|
{"5", 0, "5", ""},
|
||||||
|
{"500", 2, "500", "500.00"},
|
||||||
|
{"545", -1, "550", ""},
|
||||||
|
{"545", -2, "500", ""},
|
||||||
|
{"545", -3, "1000", ""},
|
||||||
|
{"545", -4, "0", ""},
|
||||||
|
{"499", -3, "0", ""},
|
||||||
|
{"499", -4, "0", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
// add negative number tests
|
||||||
|
for _, test := range tests {
|
||||||
|
expected := test.expected
|
||||||
|
if expected != "0" {
|
||||||
|
expected = "-" + expected
|
||||||
|
}
|
||||||
|
expectedStr := test.expectedFixed
|
||||||
|
if strings.ContainsAny(expectedStr, "123456789") && expectedStr != "" {
|
||||||
|
expectedStr = "-" + expectedStr
|
||||||
|
}
|
||||||
|
tests = append(tests,
|
||||||
|
testData{"-" + test.input, test.places, expected, expectedStr})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
d, err := NewFromString(test.input)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test Round
|
||||||
|
expected, err := NewFromString(test.expected)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
got := d.Round(test.places)
|
||||||
|
if !got.Equals(expected) {
|
||||||
|
t.Errorf("Rounding %s to %d places, got %s, expected %s",
|
||||||
|
d, test.places, got, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test StringFixed
|
||||||
|
if test.expectedFixed == "" {
|
||||||
|
test.expectedFixed = test.expected
|
||||||
|
}
|
||||||
|
gotStr := d.StringFixed(test.places)
|
||||||
|
if gotStr != test.expectedFixed {
|
||||||
|
t.Errorf("(%s).StringFixed(%d): got %s, expected %s",
|
||||||
|
d, test.places, gotStr, test.expectedFixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecimal_Uninitialized(t *testing.T) {
|
func TestDecimal_Uninitialized(t *testing.T) {
|
||||||
a := Decimal{}
|
a := Decimal{}
|
||||||
b := Decimal{}
|
b := Decimal{}
|
||||||
|
|
Loading…
Reference in a new issue