Skip to content

Commit

Permalink
decimal: implement NullDecimal
Browse files Browse the repository at this point in the history
  • Loading branch information
eapenkin authored Oct 10, 2023
1 parent 0fb7be8 commit b955b87
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 71 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.1.13] - 2023-10-10

### Added

- Implemented `NullDecimal` type.

## [0.1.12] - 2023-10-01

### Changed
Expand Down
39 changes: 18 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ 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.
- **Correctly Rounded** - All arithmetic operations are correctly rounded to the
precision of the result.
- **Testing** - Fuzz testing is used to ensure correctness.
Arithmetic operations are cross-validated against both the [cockroachdb] and
[shopspring] decimal packages.
- **Correctness** - Fuzz testing is used to cross-validate arithmetic operations
against both the [cockroachdb] and [shopspring] decimal packages.

## Getting Started

Expand Down Expand Up @@ -61,7 +58,7 @@ func main() {
fmt.Println(d.Quo(e)) // 8 / 12.5
fmt.Println(d.QuoRem(e)) // 8 // 12.5 and 8 mod 12.5
fmt.Println(d.FMA(e, e)) // 8 * 12.5 + 12.5
fmt.Println(d.Pow(2)) // 8^2
fmt.Println(d.Pow(2)) // 8 ^ 2
fmt.Println(d.Inv()) // 1 / 8
}
```
Expand Down Expand Up @@ -99,21 +96,21 @@ pkg: github.com/govalues/benchmarks
cpu: AMD Ryzen 7 3700C with Radeon Vega Mobile Gfx
```

| Test Case | Expression | govalues | [cockroachdb] v3.2.0 | cockroachdb vs govalues | [shopspring] v1.3.1 | shopspring vs govalues |
| ----------- | -------------------- | -------: | -------------------: | ----------------------: | ------------------: | ---------------------: |
| Add | 2 + 3 | 15.79n | 47.95n | +203.64% | 141.95n | +798.99% |
| Mul | 2 * 3 | 16.61n | 54.66n | +229.18% | 144.95n | +772.93% |
| QuoFinite | 2 / 4 | 64.74n | 381.15n | +488.74% | 645.35n | +896.83% |
| QuoInfinite | 2 / 3 | 595.30n | 1001.50n | +68.23% | 2810.50n | +372.11% |
| Pow | 1.1^60 | 1.31µ | 3.17µ | +142.42% | 20.50µ | +1469.53% |
| Pow | 1.01^600 | 4.36µ | 13.86µ | +217.93% | 44.39µ | +918.44% |
| Pow | 1.001^6000 | 7.39µ | 24.69µ | +234.34% | 656.84µ | +8793.66% |
| Parse | 1 | 17.27n | 78.25n | +353.23% | 128.80n | +646.02% |
| Parse | 123.456 | 39.80n | 211.85n | +432.22% | 237.60n | +496.91% |
| Parse | 123456789.1234567890 | 106.20n | 233.10n | +119.59% | 510.90n | +381.30% |
| String | 1 | 5.45n | 19.91n | +265.49% | 197.85n | +3531.94% |
| String | 123.456 | 42.38n | 74.83n | +76.57% | 229.50n | +441.53% |
| String | 123456789.1234567890 | 77.90n | 210.40n | +170.11% | 328.90n | +322.24% |
| Test Case | Expression | govalues | [cockroachdb] v3.2.0 | [shopspring] v1.3.1 | govalues vs cockroachdb | govalues vs shopspring |
| ----------- | -------------------- | -------: | -------------------: | ------------------: | ----------------------: | ---------------------: |
| Add | 2 + 3 | 15.79n | 47.95n | 141.95n | +203.64% | +798.99% |
| Mul | 2 * 3 | 16.61n | 54.66n | 144.95n | +229.18% | +772.93% |
| QuoFinite | 2 / 4 | 64.74n | 381.15n | 645.35n | +488.74% | +896.83% |
| QuoInfinite | 2 / 3 | 595.30n | 1001.50n | 2810.50n | +68.23% | +372.11% |
| Pow | 1.1^60 | 1.31µ | 3.17µ | 20.50µ | +142.42% | +1469.53% |
| Pow | 1.01^600 | 4.36µ | 13.86µ | 44.39µ | +217.93% | +918.44% |
| Pow | 1.001^6000 | 7.39µ | 24.69µ | 656.84µ | +234.34% | +8793.66% |
| Parse | 1 | 17.27n | 78.25n | 128.80n | +353.23% | +646.02% |
| Parse | 123.456 | 39.80n | 211.85n | 237.60n | +432.22% | +496.91% |
| Parse | 123456789.1234567890 | 106.20n | 233.10n | 510.90n | +119.59% | +381.30% |
| String | 1 | 5.45n | 19.91n | 197.85n | +265.49% | +3531.94% |
| String | 123.456 | 42.38n | 74.83n | 229.50n | +76.57% | +441.53% |
| String | 123456789.1234567890 | 77.90n | 210.40n | 328.90n | +170.11% | +322.24% |

The benchmark results shown in the table are provided for informational purposes only and may vary depending on your specific use case.

Expand Down
55 changes: 42 additions & 13 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strconv"
)

// Decimal type represents a finite floating-point decimal number.
// 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.
// Numeric value of a decimal is equal to:
Expand Down Expand Up @@ -155,9 +155,7 @@ func MustNew(coef int64, scale int) Decimal {
}

// NewFromFloat64 converts a pair of int64 values representing whole and
// fractional parts to a (possibly rounded) decimal.
// The relationship between the values and the returned decimal can be expressed
// as d = whole + frac / 10^scale.
// fractional parts to a (possibly rounded) decimal equal to whole + frac / 10^scale.
// See also method [Decimal.Int64].
//
// NewFromInt64 returns an error:
Expand Down Expand Up @@ -547,17 +545,17 @@ func (d Decimal) MarshalText() ([]byte, error) {
// See also method [Parse].
//
// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner
func (d *Decimal) Scan(v any) error {
func (d *Decimal) Scan(value any) error {
var err error
switch v := v.(type) {
switch value := value.(type) {
case string:
*d, err = Parse(v)
*d, err = Parse(value)
case int64:
*d, err = New(v, 0)
*d, err = New(value, 0)
case float64:
*d, err = NewFromFloat64(v)
*d, err = NewFromFloat64(value)
default:
err = fmt.Errorf("failed to convert from %T to %T", v, Decimal{})
err = fmt.Errorf("failed to convert from %T to %T", value, Decimal{})
}
return err
}
Expand Down Expand Up @@ -842,15 +840,15 @@ func (d Decimal) Round(scale int) Decimal {
// ([MaxPrec] - scale) digits.
func (d Decimal) Pad(scale int) (Decimal, error) {
if scale > MaxScale {
return Decimal{}, fmt.Errorf("zero-padding %v: %w", d, errScaleRange)
return Decimal{}, fmt.Errorf("padding %v with zeros: %w", d, errScaleRange)
}
if scale <= d.Scale() {
return d, nil
}
coef := d.coef
coef, ok := coef.lsh(scale - d.Scale())
if !ok {
return Decimal{}, fmt.Errorf("zero-padding %v with %v digits: %w", d, scale-d.Scale(), overflowError(d.Prec(), d.Scale(), scale))
return Decimal{}, fmt.Errorf("padding %v with zeros: %w", d, overflowError(d.Prec(), d.Scale(), scale))
}
return newDecimalSafe(d.IsNeg(), coef, scale)
}
Expand Down Expand Up @@ -1359,7 +1357,7 @@ func (d Decimal) SubExact(e Decimal, scale int) (Decimal, error) {
func (d Decimal) SubAbs(e Decimal) (Decimal, error) {
f, err := d.Sub(e)
if err != nil {
return Decimal{}, fmt.Errorf("computing [abs(%v - %v)]: %w", d, e, errScaleRange)
return Decimal{}, fmt.Errorf("computing [abs(%v - %v)]: %w", d, e, err)
}
return f.Abs(), nil
}
Expand Down Expand Up @@ -1784,3 +1782,34 @@ func (d Decimal) Clamp(min, max Decimal) (Decimal, error) {
}
return d, nil
}

// NullDecimal represents a decimal that may be null.
type NullDecimal struct {
Decimal Decimal
Valid bool
}

// Scan implements the [sql.Scanner] interface.
// See also method [Parse].
//
// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner
func (n *NullDecimal) Scan(value any) error {
if value == nil {
n.Decimal = Decimal{}
n.Valid = false
return nil
}
n.Valid = true
return n.Decimal.Scan(value)
}

// Value implements the [driver.Valuer] interface.
// See also method [Decimal.String].
//
// [driver.Valuer]: https://pkg.go.dev/database/sql/driver#Valuer
func (n NullDecimal) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Decimal.Value()
}
94 changes: 57 additions & 37 deletions doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,39 +138,39 @@ func Example_piApproximation() {
}

func ExampleMustNew() {
fmt.Println(decimal.MustNew(-123, 3))
fmt.Println(decimal.MustNew(-123, 2))
fmt.Println(decimal.MustNew(-123, 1))
fmt.Println(decimal.MustNew(-123, 0))
// Output:
// -0.123
// -1.23
// -12.3
// -123
fmt.Println(decimal.MustNew(123, 3))
fmt.Println(decimal.MustNew(123, 2))
fmt.Println(decimal.MustNew(123, 1))
fmt.Println(decimal.MustNew(123, 0))
// Output:
// 0.123
// 1.23
// 12.3
// 123
}

func ExampleNew() {
fmt.Println(decimal.New(-123, 3))
fmt.Println(decimal.New(-123, 2))
fmt.Println(decimal.New(-123, 1))
fmt.Println(decimal.New(-123, 0))
fmt.Println(decimal.New(123, 3))
fmt.Println(decimal.New(123, 2))
fmt.Println(decimal.New(123, 1))
fmt.Println(decimal.New(123, 0))
// Output:
// -0.123 <nil>
// -1.23 <nil>
// -12.3 <nil>
// -123 <nil>
// 0.123 <nil>
// 1.23 <nil>
// 12.3 <nil>
// 123 <nil>
}

func ExampleNewFromInt64() {
fmt.Println(decimal.NewFromInt64(-1, -23, 2))
fmt.Println(decimal.NewFromInt64(-1, -23, 3))
fmt.Println(decimal.NewFromInt64(-1, -23, 4))
fmt.Println(decimal.NewFromInt64(-1, -23, 5))
// Output:
// -1.23 <nil>
// -1.023 <nil>
// -1.0023 <nil>
// -1.00023 <nil>
fmt.Println(decimal.NewFromInt64(1, 23, 5))
fmt.Println(decimal.NewFromInt64(1, 23, 4))
fmt.Println(decimal.NewFromInt64(1, 23, 3))
fmt.Println(decimal.NewFromInt64(1, 23, 2))
// Output:
// 1.00023 <nil>
// 1.0023 <nil>
// 1.023 <nil>
// 1.23 <nil>
}

func ExampleNewFromFloat64() {
Expand Down Expand Up @@ -316,23 +316,15 @@ func ExampleDecimal_MarshalText() {

func ExampleDecimal_Scan() {
d := new(decimal.Decimal)
s := "-15.67"
err := d.Scan(s)
if err != nil {
panic(err)
}
_ = d.Scan("-15.67")
fmt.Println(d)
// Output: -15.67
}

func ExampleDecimal_Value() {
d := decimal.MustParse("-15.67")
s, err := d.Value()
if err != nil {
panic(err)
}
fmt.Println(s)
// Output: -15.67
fmt.Println(d.Value())
// Output: -15.67 <nil>
}

func ExampleDecimal_Format() {
Expand Down Expand Up @@ -827,3 +819,31 @@ func ExampleDecimal_WithinOne() {
// true
// false
}

func ExampleNullDecimal_Scan() {
n := new(decimal.NullDecimal)
_ = n.Scan("-15.67")
m := new(decimal.NullDecimal)
_ = m.Scan(nil)
fmt.Println(n)
fmt.Println(m)
// Output:
// &{-15.67 true}
// &{0 false}
}

func ExampleNullDecimal_Value() {
n := decimal.NullDecimal{
Decimal: decimal.MustParse("-15.67"),
Valid: true,
}
m := decimal.NullDecimal{
Decimal: decimal.MustParse("0"),
Valid: false,
}
fmt.Println(n.Value())
fmt.Println(m.Value())
// Output:
// -15.67 <nil>
// <nil> <nil>
}

0 comments on commit b955b87

Please sign in to comment.