add Round, Floor, Ceil, and StringFixed. Deprecate StringScaled

This commit is contained in:
Vadim Graboys 2015-06-14 11:53:23 -04:00
parent ac401f1975
commit 0c74aee0c1
2 changed files with 214 additions and 9 deletions

View file

@ -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)

View file

@ -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{}