mirror of
https://github.com/shopspring/decimal.git
synced 2024-11-22 20:40:48 +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.
|
||||
var Zero = New(0, 1)
|
||||
|
||||
var tenInt = big.NewInt(10)
|
||||
var zeroInt = big.NewInt(0)
|
||||
var oneInt = big.NewInt(1)
|
||||
var fiveInt = big.NewInt(5)
|
||||
var tenInt = big.NewInt(10)
|
||||
|
||||
// Decimal represents a fixed-point decimal. It is immutable.
|
||||
// number = value * 10 ^ exp
|
||||
|
@ -332,6 +334,10 @@ func (d Decimal) Float64() (f float64, exact bool) {
|
|||
// -12.345
|
||||
//
|
||||
func (d Decimal) String() string {
|
||||
return d.string(true)
|
||||
}
|
||||
|
||||
func (d Decimal) string(trimTrailingZeros bool) string {
|
||||
if d.exp >= 0 {
|
||||
return d.rescale(0).value.String()
|
||||
}
|
||||
|
@ -351,6 +357,7 @@ func (d Decimal) String() string {
|
|||
fractionalPart = strings.Repeat("0", num0s) + str
|
||||
}
|
||||
|
||||
if trimTrailingZeros {
|
||||
i := len(fractionalPart) - 1
|
||||
for ; i >= 0; i-- {
|
||||
if fractionalPart[i] != '0' {
|
||||
|
@ -358,6 +365,7 @@ func (d Decimal) String() string {
|
|||
}
|
||||
}
|
||||
fractionalPart = fractionalPart[:i+1]
|
||||
}
|
||||
|
||||
number := intPart
|
||||
if len(fractionalPart) > 0 {
|
||||
|
@ -371,9 +379,64 @@ func (d Decimal) String() string {
|
|||
return number
|
||||
}
|
||||
|
||||
// StringScaled first scales the decimal then calls .String() on it.
|
||||
func (d Decimal) StringScaled(exp int32) string {
|
||||
return d.rescale(exp).String()
|
||||
// StringFixed returns a rounded fixed-point string with places digits after
|
||||
// the decimal point.
|
||||
//
|
||||
// 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.
|
||||
|
@ -447,6 +510,12 @@ func (d Decimal) MarshalText() (text []byte, err error) {
|
|||
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() {
|
||||
if d.value == nil {
|
||||
d.value = new(big.Int)
|
||||
|
|
136
decimal_test.go
136
decimal_test.go
|
@ -2,6 +2,7 @@ package decimal
|
|||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"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) {
|
||||
a := Decimal{}
|
||||
b := Decimal{}
|
||||
|
|
Loading…
Reference in a new issue