Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decimal: improve test coverage, doc and examples #30

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
85 changes: 55 additions & 30 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
// 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 @@
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,18 +154,16 @@

// 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)
d, err := New(whole, 0)
if err != nil {
return Decimal{}, fmt.Errorf("converting integers: %w", err)
}
Expand All @@ -178,13 +172,18 @@
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 !f.IsZero() {
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)
}

Check warning on line 186 in decimal.go

View check run for this annotation

Codecov / codecov/patch

decimal.go#L185-L186

Added lines #L185 - L186 were not covered by tests
}
return d, nil
}
Expand All @@ -193,8 +192,8 @@
// 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 @@
//
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
Loading