diff --git a/decimal.go b/decimal.go index b23d053..7df4dd2 100644 --- a/decimal.go +++ b/decimal.go @@ -910,6 +910,60 @@ func (d Decimal) Truncate(precision int32) Decimal { return d } +// RoundToSignificantFigures returns a copy of a decimal rounded to specified number of significant figures. +// For negative or zero figures the function returns zero. +// Result is normalized without trailing zeros. +// +// Example: +// +// NewFromString("5.45").RoundToSignificantFigures(2).String() // output: "5.5" +// NewFromString("545").RoundToSignificantFigures(2).String() // output: "550" +// +func (d Decimal) RoundToSignificantFigures(figures int32) Decimal { + if figures <= 0 { + return Zero + } + d = d.Normalize() + twoMant := big.NewInt(0).Set(d.value) + twoMant.Abs(twoMant) + twoMant.Mul(twoMant, twoInt) + upper := big.NewInt(int64(figures)) + upper.Exp(tenInt, upper, nil) + upper.Mul(upper, twoInt) + upper.Sub(upper, oneInt) + m := int64(0) + for twoMant.Cmp(upper) >= 0 { + upper.Mul(upper, tenInt) + m++ + } + if int64(d.exp)+m > int64(math.MaxInt32) { + panic(fmt.Sprintf("exponent %d overflows an int32", int64(d.exp)+m)) + } + return d.Round(-d.exp - int32(m)).Normalize() +} + +// Normalize normalizes decimal value. +// It strips all the trailing zeros. +// More specifically if a mantissa is representable as m*10^n it returns a decimal with a mantissa m and its exponent incremented by n. +func (d Decimal) Normalize() Decimal { + d.ensureInitialized() + quo := big.NewInt(0).Set(d.value) + prevQuo := big.NewInt(0).Set(d.value) + rem := big.NewInt(0) + n := int64(0) + for { + quo.QuoRem(quo, tenInt, rem) + if rem.Cmp(zeroInt) != 0 || quo.Cmp(zeroInt) == 0 { + if int64(d.exp)+n > int64(math.MaxInt32) { + panic(fmt.Sprintf("exponent %d overflows an int32", int64(d.exp)+n)) + } + return Decimal{value: prevQuo, exp: d.exp + int32(n)} + } + prevQuo.Set(quo) + n++ + } +} + // UnmarshalJSON implements the json.Unmarshaler interface. func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { if string(decimalBytes) == "null" { diff --git a/decimal_test.go b/decimal_test.go index 64f0552..4bf2120 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2577,6 +2577,105 @@ func TestTan(t *testing.T) { } } +func TestNormalize(t *testing.T) { + tbl := []struct { + inp Decimal + res Decimal + }{ + {New(0, 0), New(0, 0)}, + {New(10, 0), New(1, 1)}, + {New(100, 0), New(1, 2)}, + {New(11, 0), New(11, 0)}, + {New(111, 0), New(111, 0)}, + {New(-10, 0), New(-1, 1)}, + {New(-100, 0), New(-1, 2)}, + {New(-11, 0), New(-11, 0)}, + {New(-111, 0), New(-111, 0)}, + {New(0, 5), New(0, 0)}, + {New(10, 5), New(1, 6)}, + {New(100, 5), New(1, 7)}, + {New(11, 5), New(11, 5)}, + {New(111, 5), New(111, 5)}, + {New(-10, 5), New(-1, 6)}, + {New(-100, 5), New(-1, 7)}, + {New(-11, 5), New(-11, 5)}, + {New(-111, 5), New(-111, 5)}, + {New(0, -5), New(0, 0)}, + {New(10, -5), New(1, -4)}, + {New(100, -5), New(1, -3)}, + {New(11, -5), New(11, -5)}, + {New(111, -5), New(111, -5)}, + {New(-10, -5), New(-1, -4)}, + {New(-100, -5), New(-1, -3)}, + {New(-11, -5), New(-11, -5)}, + {New(-111, -5), New(-111, -5)}, + } + for _, i := range tbl { + if norm := i.inp.Normalize(); norm.Cmp(i.res) != 0 { + t.Errorf("unexpected normalization result, expected: %v, got: %v", i.res, norm) + } + } +} + +func TestRoundToSignificantFigures(t *testing.T) { + type testData struct { + inp Decimal + fig int32 + res Decimal + } + strtbl := []struct { + inp string + fig int32 + res string + }{ + {"5.45", 2, "5.5"}, + {"545", 2, "550"}, + {"0", 0, "0"}, + {"0", 1, "0"}, + {"0", -1, "0"}, + {"1", 0, "0"}, + {"1", 1, "1"}, + {"1", 2, "1"}, + {"1", -1, "0"}, + {"10", 1, "10"}, + {"10", 0, "0"}, + {"100", 1, "100"}, + {"110", 1, "100"}, + {"110", 2, "110"}, + {"995", 1, "1000"}, + {"995", 2, "1000"}, + {"994", 2, "990"}, + {"995", 3, "995"}, + {"995", 4, "995"}, + {"0.995", 1, "1"}, + {"0.995", 2, "1"}, + {"0.994", 2, "0.99"}, + {"0.995", 3, "0.995"}, + {"0.995", 4, "0.995"}, + {"994.99999999999999999999999999", 2, "990"}, + {"994.999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", 2, "990"}, + {"995.999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", 2, "1000"}, + {"0.101", 1, "0.1"}, + {"0.101", 2, "0.1"}, + {"0.101", 3, "0.101"}, + } + tbl := []testData{{Decimal{}, 1, Decimal{}}} + for _, x := range strtbl { + tbl = append(tbl, testData{RequireFromString(x.inp), x.fig, RequireFromString(x.res)}) + // Symmetric negative tests + if x.inp != "0" { + x.inp = "-" + x.inp + x.res = "-" + x.res + tbl = append(tbl, testData{RequireFromString(x.inp), x.fig, RequireFromString(x.res)}) + } + } + for _, x := range tbl { + if rnd := x.inp.RoundToSignificantFigures(x.fig); rnd.Cmp(x.res) != 0 { + t.Errorf("unexpected rounding to significant figures result, expected: %s, got: %v", x.res, rnd) + } + } +} + func ExampleNewFromFloat32() { fmt.Println(NewFromFloat32(123.123123123123).String()) fmt.Println(NewFromFloat32(.123123123123123).String())