Skip to content

Commit

Permalink
decimal: improve test coverage, doc and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
eapenkin committed Nov 21, 2023
1 parent 03405af commit d9fa60c
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 131 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: gofmt -s -w . && git diff --exit-code

- name: Verify dependency consistency
run: go mod tidy && git diff --exit-code
run: go get -u -t . && go mod tidy && git diff --exit-code

- name: Verify generated code
run: go generate ./... && git diff --exit-code
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [0.1.16] - 2023-11-21

### Changed

- Improved examples and documentation.
- Improved test coverage.

## [0.1.15] - 2023-10-31

### Changed
Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This package is designed specifically for use in transactional financial systems
such as overflow or division by zero.
- **Simple String Representation** - Decimals are represented without the complexities
of scientific or engineering notation.
- **Correctness** - Fuzz testing is used to cross-validate arithmetic operations
- **Correctness** - Fuzz testing is used to [cross-validate] arithmetic operations
against both the [cockroachdb] and [shopspring] decimal packages.

## Getting Started
Expand All @@ -39,8 +39,8 @@ go get github.com/govalues/decimal

### Usage

Create decimal values using constructors such as `MustNew` or `MustParse`.
After creating a decimal value, various arithmetic operations can be performed:
Create decimal values using one of the constructors.
After creating a decimal value, various operations can be performed:

```go
package main
Expand All @@ -57,11 +57,6 @@ func main() {
f, _ := decimal.NewFromFloat64(2.567) // f = 2.567
g, _ := decimal.NewFromInt64(7, 896, 3) // g = 7.896

// Conversions
fmt.Println(f.Int64(9)) // 2 567000000
fmt.Println(f.Float64()) // 2.567
fmt.Println(f.String()) // 2.567

// Operations
fmt.Println(d.Add(e)) // 8 + 12.5
fmt.Println(d.Sub(e)) // 8 - 12.5
Expand All @@ -77,6 +72,15 @@ func main() {
fmt.Println(g.Ceil(2)) // 7.90
fmt.Println(g.Floor(2)) // 7.89
fmt.Println(g.Trunc(2)) // 7.89

// Conversions
fmt.Println(f.Int64(9)) // 2 567000000
fmt.Println(f.Float64()) // 2.567
fmt.Println(f.String()) // 2.567

// Formatting
fmt.Printf("%.2f\n", f) // 2.57
fmt.Printf("%.2k\n", f) // 256.70%
}
```

Expand Down Expand Up @@ -164,3 +168,4 @@ This ensures alignment with the project's objectives and roadmap.
[cockroachdb]: https://pkg.go.dev/github.com/cockroachdb/apd
[shopspring]: https://pkg.go.dev/github.com/shopspring/decimal
[specification]: https://speleotrove.com/decimal/telcoSpec.html
[cross-validate]: https://github.com/govalues/decimal-tests/blob/main/decimal_fuzz_test.go
95 changes: 60 additions & 35 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import (
// Decimal represents a finite floating-point decimal number.
// Its zero value corresponds to the numeric value of 0.
// It is designed to be safe for concurrent use by multiple goroutines.
// The numeric value of a decimal is equal to:
//
// coef / 10^scale if !neg
// -coef / 10^scale if neg
type Decimal struct {
neg bool // indicates whether the decimal is negative
scale int8 // position of the floating decimal point
Expand Down Expand Up @@ -46,15 +42,15 @@ var (
errDivisionByZero = errors.New("division by zero")
)

// newUnsafe creates a new decimal without checking for overflow.
// newUnsafe creates a new decimal without checking scale and coefficient.
func newUnsafe(neg bool, coef fint, scale int) Decimal {
if coef == 0 {
neg = false
}
return Decimal{neg: neg, coef: coef, scale: int8(scale)}
}

// newSafe creates a new decimal and checks for overflow.
// newSafe creates a new decimal and checks scale and coefficient.
func newSafe(neg bool, coef fint, scale int) (Decimal, error) {
switch {
case scale < MinScale || scale > MaxScale:
Expand Down Expand Up @@ -158,33 +154,36 @@ func MustNew(coef int64, scale int) Decimal {

// NewFromInt64 converts a pair of int64 values representing whole and
// fractional parts to a (possibly rounded) decimal equal to whole + frac / 10^scale.
// NewFromInt64 removes all trailing zeros from the fractional part.
// See also method [Decimal.Int64].
//
// NewFromInt64 returns an error:
// - if whole and fractional parts have different signs;
// - if scale is negative or greater than [MaxScale];
// - if frac / 10^scale is not within the range (-1, 1).
func NewFromInt64(whole, frac int64, scale int) (Decimal, error) {
if whole > 0 && frac < 0 || whole < 0 && frac > 0 {
return Decimal{}, fmt.Errorf("converting integers: inconsistent signs")
}
// Whole
w, err := New(whole, 0)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
}
// Fraction
f, err := New(frac, scale)
d, err := New(whole, 0)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
}
if !f.WithinOne() {
return Decimal{}, fmt.Errorf("converting integers: inconsistent fraction")
}
// Decimal
d, err := w.Add(f)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
if frac != 0 {
// Fraction
f, err := New(frac, scale)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
}
if !d.IsZero() && d.Sign() != f.Sign() {
return Decimal{}, fmt.Errorf("converting integers: inconsistent signs")
}
if !f.WithinOne() {
return Decimal{}, fmt.Errorf("converting integers: inconsistent fraction")
}
f = f.Trim(0)
d, err = d.Add(f)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
}
}
return d, nil
}
Expand All @@ -193,8 +192,8 @@ func NewFromInt64(whole, frac int64, scale int) (Decimal, error) {
// See also method [Decimal.Float64].
//
// NewFromFloat64 returns an error:
// - if the integer part of the result has more than [MaxPrec] digits;
// - if the float is a special value (NaN or Inf).
// - if the float is a special value (NaN or Inf);
// - if the integer part of the result has more than [MaxPrec] digits.
func NewFromFloat64(f float64) (Decimal, error) {
if math.IsNaN(f) || math.IsInf(f, 0) {
return Decimal{}, fmt.Errorf("converting float: special value %v", f)
Expand Down Expand Up @@ -247,6 +246,7 @@ func (d Decimal) ULP() Decimal {
//
// Parse returns an error:
// - if the integer part of the result has more than [MaxPrec] digits;
// - if the string contains any whitespaces;
// - if the string does not represent a valid decimal number;
// - if the string is longer than 330 bytes;
// - if the exponent is less than -330 or greater than 330.
Expand Down Expand Up @@ -478,10 +478,13 @@ func (d Decimal) String() string {
return string(buf[pos+1:])
}

// Float64 returns a float64 representation of the decimal.
// This conversion may lose data, as float64 has a limited precision
// compared to the decimal type.
// Float64 returns the nearest binary floating-point number rounded
// using [rounding half to even] (banker's rounding).
// This conversion may lose data, as float64 has a smaller precision
// than the decimal type.
// See also method [NewFromFloat64].
//
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
func (d Decimal) Float64() (f float64, ok bool) {
f, err := strconv.ParseFloat(d.String(), 64)
if err != nil {
Expand All @@ -490,14 +493,20 @@ func (d Decimal) Float64() (f float64, ok bool) {
return f, true
}

// Int64 returns a pair of int64 values representing the whole part w and the
// fractional part f of the decimal.
// Int64 returns a pair of int64 values representing the whole and the
// fractional parts of the decimal.
// The relationship between the decimal and the returned values can be expressed
// as d = w + f / 10^scale.
// If the result cannot be accurately represented as a pair of int64 values,
// the method returns false.
// as d = whole + frac / 10^scale.
// If given scale is greater than the scale of the decimal, then the fractional part
// is zero-padded to the right.
// If given scale is smaller than the scale of the decimal, then the fractional part
// is rounded using [rounding half to even] (banker's rounding).
// If the result cannot be represented as a pair of int64 values,
// then false is returned.
// See also method [NewFromInt64].
func (d Decimal) Int64(scale int) (w, f int64, ok bool) {
//
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
func (d Decimal) Int64(scale int) (whole, frac int64, ok bool) {
if scale < MinScale || scale > MaxScale {
return 0, 0, false
}
Expand Down Expand Up @@ -877,7 +886,7 @@ func (d Decimal) Rescale(scale int) (Decimal, error) {
return d, nil
}

// Quantize returns a decimal rounded to the same scale as decimal e.
// Quantize returns a decimal rescaled to the same scale as decimal e.
// The sign and coefficient of decimal e are ignored.
// See also method [Decimal.Rescale].
//
Expand Down Expand Up @@ -1052,6 +1061,7 @@ func (d Decimal) MulExact(e Decimal, scale int) (Decimal, error) {
return f, nil
}

// mulFint computes the product of two decimals using uint64 arithmetic.
func (d Decimal) mulFint(e Decimal, minScale int) (Decimal, error) {
dcoef, ecoef := d.coef, e.coef

Expand All @@ -1070,6 +1080,7 @@ func (d Decimal) mulFint(e Decimal, minScale int) (Decimal, error) {
return newFromFint(neg, dcoef, scale, minScale)
}

// mulBint computes the product of two decimals using *big.Int arithmetic.
func (d Decimal) mulBint(e Decimal, minScale int) (Decimal, error) {
dcoef := d.coef.bint()
ecoef := e.coef.bint()
Expand Down Expand Up @@ -1128,6 +1139,8 @@ func (d Decimal) PowExact(power, scale int) (Decimal, error) {
return e, nil
}

// powFint computes the power of a decimal using uint64 arithmetic.
// powFint does not support negative powers.
func (d Decimal) powFint(power, minScale int) (Decimal, error) {
if power < 0 {
return Decimal{}, errInvalidOperation
Expand Down Expand Up @@ -1173,6 +1186,8 @@ func (d Decimal) powFint(power, minScale int) (Decimal, error) {
return newFromFint(eneg, ecoef, escale, minScale)
}

// powBint computes the power of a decimal using *big.Int arithmetic.
// powBint supports negative powers.
func (d Decimal) powBint(power, minScale int) (Decimal, error) {
inv := false
if power < 0 {
Expand Down Expand Up @@ -1265,6 +1280,7 @@ func (d Decimal) AddExact(e Decimal, scale int) (Decimal, error) {
return f, nil
}

// addFint computes the sum of two decimals using uint64 arithmetic.
func (d Decimal) addFint(e Decimal, minScale int) (Decimal, error) {
dcoef, ecoef := d.coef, e.coef

Expand Down Expand Up @@ -1309,6 +1325,7 @@ func (d Decimal) addFint(e Decimal, minScale int) (Decimal, error) {
return newFromFint(neg, dcoef, scale, minScale)
}

// addBint computes the sum of two decimals using *big.Int arithmetic.
func (d Decimal) addBint(e Decimal, minScale int) (Decimal, error) {
dcoef := d.coef.bint()
ecoef := e.coef.bint()
Expand Down Expand Up @@ -1402,6 +1419,7 @@ func (d Decimal) FMAExact(e, f Decimal, scale int) (Decimal, error) {
return g, nil
}

// fmaFint computes the fused multiply-addition of three decimals using uint64 arithmetic.
func (d Decimal) fmaFint(e, f Decimal, minScale int) (Decimal, error) {
dcoef, ecoef, fcoef := d.coef, e.coef, f.coef

Expand Down Expand Up @@ -1449,6 +1467,7 @@ func (d Decimal) fmaFint(e, f Decimal, minScale int) (Decimal, error) {
return newFromFint(neg, dcoef, scale, minScale)
}

// fmaBint computes the fused multiply-addition of three decimals using *big.Int arithmetic.
func (d Decimal) fmaBint(e, f Decimal, minScale int) (Decimal, error) {
dcoef := d.coef.bint()
ecoef := e.coef.bint()
Expand Down Expand Up @@ -1535,6 +1554,7 @@ func (d Decimal) QuoExact(e Decimal, scale int) (Decimal, error) {
return f, nil
}

// quoFint computes the quotient of two decimals using uint64 arithmetic.
func (d Decimal) quoFint(e Decimal, minScale int) (Decimal, error) {
dcoef, ecoef := d.coef, e.coef

Expand Down Expand Up @@ -1569,6 +1589,7 @@ func (d Decimal) quoFint(e Decimal, minScale int) (Decimal, error) {
return newFromFint(neg, dcoef, scale, minScale)
}

// quoBint computes the quotient of two decimals using *big.Int arithmetic.
func (d Decimal) quoBint(e Decimal, minScale int) (Decimal, error) {
dcoef := d.coef.bint()
ecoef := e.coef.bint()
Expand Down Expand Up @@ -1609,7 +1630,9 @@ func (d Decimal) quoRem(e Decimal) (q, r Decimal, err error) {
if err != nil {
return Decimal{}, Decimal{}, err
}
q = q.Trunc(0) // T-Division

// T-Division
q = q.Trunc(0)

// Reminder
r, err = e.Mul(q)
Expand Down Expand Up @@ -1661,6 +1684,7 @@ func (d Decimal) Cmp(e Decimal) int {
return r
}

// cmpFint compares decimals using uint64 arithmetic.
func (d Decimal) cmpFint(e Decimal) (int, error) {
dcoef, ecoef := d.coef, e.coef

Expand Down Expand Up @@ -1689,6 +1713,7 @@ func (d Decimal) cmpFint(e Decimal) (int, error) {
return 0, nil
}

// cmpBint compares decimals using *big.Int arithmetic.
func (d Decimal) cmpBint(e Decimal) int {
dcoef := d.coef.bint()
ecoef := e.coef.bint()
Expand Down
Loading

0 comments on commit d9fa60c

Please sign in to comment.