Skip to content

Commit

Permalink
Merge pull request #33 from govalues:improve-doc-and-examples
Browse files Browse the repository at this point in the history
decimal: improve documentation and examples
  • Loading branch information
eapenkin authored Dec 18, 2023
2 parents 6db2170 + 242c271 commit 7f51009
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 77 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.18] - 2023-12-18

### Changed

- Improved examples and documentation.

## [0.1.17] - 2023-12-01

### Added
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ This package is designed specifically for use in transactional financial systems

## Features

- **Optimized Performance** - Utilizes uint64 for coefficients, reducing heap
- **Optimized Performance** - Utilizes `uint64` for coefficients, reducing heap
allocations and memory consumption.
- **Immutability** - Once a decimal is set, it remains unchanged.
This immutability ensures safe concurrent access across goroutines.
- **Banker's Rounding** - Methods use half even rounding, also known as "banker's rounding",
- **Banker's Rounding** - Methods use half-to-even rounding, also known as "banker's rounding",
which minimizes cumulative rounding errors commonly seen in financial calculations.
- **No Panics** - All methods are designed to be panic-free.
Instead of potentially crashing your application, they return errors for issues
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
against both the [cockroachdb] and [shopspring] decimal packages.
against the [cockroachdb] and [shopspring] decimal packages.


## Getting Started

Expand Down Expand Up @@ -102,14 +103,16 @@ Comparison with other popular packages:
| Speed | High | Medium | Low[^reason] |
| Mutability | Immutable | Mutable[^reason] | Immutable |
| Memory Footprint | Low | Medium | High |
| Panic Free | Yes | Yes | No |
| Panic Free | Yes | Yes | No[^divzero] |
| Precision | 19 digits | Arbitrary | Arbitrary |
| Default Rounding | Half to even | Half up | Half away from 0 |
| Context | Implicit | Explicit | Implicit |

[^reason]: decimal package was created simply because shopspring's decimal was
too slow and cockroachdb's decimal was mutable.

[^divzero]: [shopspring]'s decimal panics on division by zero.

### Benchmarks

```text
Expand Down
22 changes: 11 additions & 11 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ func (d Decimal) String() string {

// Float64 returns the nearest binary floating-point number rounded
// using [rounding half to even] (banker's rounding).
// See also method [NewFromFloat64].
// See also constructor [NewFromFloat64].
//
// This conversion may lose data, as float64 has a smaller precision
// than the decimal type.
Expand All @@ -515,7 +515,7 @@ func (d Decimal) Float64() (f float64, ok bool) {
// The relationship between the decimal and the returned values can be expressed
// as d = whole + frac / 10^scale.
// This method is useful for converting amounts to [protobuf] format.
// See also method [NewFromInt64].
// See also constructor [NewFromInt64].
//
// If the result cannot be represented as a pair of int64 values,
// then false is returned.
Expand Down Expand Up @@ -553,7 +553,7 @@ func (d Decimal) Int64(scale int) (whole, frac int64, ok bool) {
}

// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
// See also method [Parse].
// See also constructor [Parse].
//
// [encoding.TextUnmarshaler]: https://pkg.go.dev/encoding#TextUnmarshaler
func (d *Decimal) UnmarshalText(text []byte) error {
Expand All @@ -571,7 +571,7 @@ func (d Decimal) MarshalText() ([]byte, error) {
}

// Scan implements the [sql.Scanner] interface.
// See also method [Parse].
// See also constructor [Parse].
//
// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner
func (d *Decimal) Scan(value any) error {
Expand Down Expand Up @@ -802,7 +802,7 @@ func (d Decimal) Coef() uint64 {
}

// Scale returns the number of digits after the decimal point.
// See also methods [Decimal.Prec] and [Decimal.MinScale].
// See also methods [Decimal.Prec], [Decimal.MinScale].
func (d Decimal) Scale() int {
return int(d.scale)
}
Expand Down Expand Up @@ -1011,7 +1011,7 @@ func (d Decimal) Abs() Decimal {
}

// CopySign returns a decimal with the same sign as decimal e.
// CopySign treates zero as positive.
// CopySign treates 0 as positive.
// See also method [Decimal.Sign].
func (d Decimal) CopySign(e Decimal) Decimal {
if d.IsNeg() == e.IsNeg() {
Expand Down Expand Up @@ -1533,7 +1533,7 @@ func (d Decimal) fmaBint(e, f Decimal, minScale int) (Decimal, error) {
// Quo returns the (possibly rounded) quotient of decimals d and e.
//
// Quo returns an error if:
// - the divisor is zero;
// - the divisor is 0;
// - the integer part of the result has more than [MaxPrec] digits.
func (d Decimal) Quo(e Decimal) (Decimal, error) {
return d.QuoExact(e, 0)
Expand Down Expand Up @@ -1640,7 +1640,7 @@ func (d Decimal) quoBint(e Decimal, minScale int) (Decimal, error) {
// reminder r is the same as the sign of the dividend d.
//
// QuoRem returns an error if:
// - the divisor is zero;
// - the divisor is 0;
// - the integer part of the quotient has more than [MaxPrec] digits.
func (d Decimal) QuoRem(e Decimal) (q, r Decimal, err error) {
q, r, err = d.quoRem(e)
Expand Down Expand Up @@ -1677,7 +1677,7 @@ func (d Decimal) quoRem(e Decimal) (q, r Decimal, err error) {
//
// Inv returns an error if:
// - the integer part of the result has more than [MaxPrec] digits;
// - the decimal is zero.
// - the decimal is 0.
func (d Decimal) Inv() (Decimal, error) {
f, err := One.Quo(d)
if err != nil {
Expand All @@ -1692,7 +1692,7 @@ func (d Decimal) Inv() (Decimal, error) {
// 0 if d = e
// +1 if d > e
//
// See also methods [Decimal.CmpAbs] and [Decimal.CmpTotal].
// See also methods [Decimal.CmpAbs], [Decimal.CmpTotal].
func (d Decimal) Cmp(e Decimal) int {
// Special case: different signs
switch {
Expand Down Expand Up @@ -1853,7 +1853,7 @@ type NullDecimal struct {
}

// Scan implements the [sql.Scanner] interface.
// See also method [Parse].
// See also constructor [Parse].
//
// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner
func (n *NullDecimal) Scan(value any) error {
Expand Down
27 changes: 12 additions & 15 deletions decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ func TestNewFromFloat64(t *testing.T) {
{0.00, "0"},
{0.0000000000000000000, "0"},

// Powers of 10
// Smallest non-zero
{math.SmallestNonzeroFloat64, "0.0000000000000000000"},

// Powers of 10
{1e-20, "0.0000000000000000000"},
{1e-19, "0.0000000000000000001"},
{1e-5, "0.00001"},
Expand Down Expand Up @@ -206,15 +208,15 @@ func TestNewFromFloat64(t *testing.T) {

t.Run("error", func(t *testing.T) {
tests := map[string]float64{
"overflow 1": 1e19,
"overflow 2": 1e20,
"overflow 3": math.MaxFloat64,
"overflow 4": -1e19,
"overflow 5": -1e20,
"overflow 6": -math.MaxFloat64,
"nan": math.NaN(),
"inf": math.Inf(1),
"-inf": math.Inf(-1),
"overflow 1": 1e19,
"overflow 2": 1e20,
"overflow 3": math.MaxFloat64,
"overflow 4": -1e19,
"overflow 5": -1e20,
"overflow 6": -math.MaxFloat64,
"special value 1": math.NaN(),
"special value 2": math.Inf(1),
"special value 3": math.Inf(-1),
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -2606,27 +2608,22 @@ func TestDecimal_Clamp(t *testing.T) {
{"0", "-2", "-1", "-1"},
{"0", "-1", "1", "0"},
{"0", "1", "2", "1"},

{"0.000", "0.0", "0.000", "0.000"},
{"0.000", "0.000", "0.0", "0.000"},
{"0.0", "0.0", "0.000", "0.0"},
{"0.0", "0.000", "0.0", "0.0"},

{"0.000", "0.000", "1", "0.000"},
{"0.000", "0.0", "1", "0.0"},
{"0.0", "0.000", "1", "0.0"},
{"0.0", "0.0", "1", "0.0"},

{"0.000", "-1", "0.000", "0.000"},
{"0.000", "-1", "0.0", "0.000"},
{"0.0", "-1", "0.000", "0.000"},
{"0.0", "-1", "0.0", "0.0"},

{"1.2300", "1.2300", "2", "1.2300"},
{"1.2300", "1.23", "2", "1.23"},
{"1.23", "1.2300", "2", "1.23"},
{"1.23", "1.23", "2", "1.23"},

{"1.2300", "1", "1.2300", "1.2300"},
{"1.2300", "1", "1.23", "1.2300"},
{"1.23", "1", "1.2300", "1.2300"},
Expand Down
97 changes: 50 additions & 47 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
Package decimal implements immutable decimal floating-point numbers.
It is designed specifically for use in transactional financial systems.
This package generally adheres to the principles set by [ANSI X3.274-1996 (section 7.4)].
It is specifically designed for use in transactional financial systems.
This package adheres to the principles set by [ANSI X3.274-1996 (section 7.4)].
# Representation
A decimal value is a struct with three fields:
[Decimal] is a struct with three fields:
- Sign: a boolean indicating whether the decimal is negative.
- Coefficient: an unsigned integer representing the numeric value of the decimal
Expand All @@ -24,22 +24,22 @@ The numerical value of a decimal is calculated as:
- -Coefficient / 10^Scale, if Sign is true.
- Coefficient / 10^Scale, if Sign is false.
In such approach, the same numeric value can have multiple representations.
For example, 1, 1.0, and 1.00 all represent the same value, but have different
In this approach, the same numeric value can have multiple representations.
For example, 1, 1.0, and 1.00 all represent the same value but have different
scales and coefficients.
# Constraints
The range of a decimal is determined by its scale.
Here are the ranges for frequently used scales:
| Currency | Scale | Minimum | Maximum |
| ------------ | ----- | ------------------------------------ | ----------------------------------- |
| Japanese Yen | 0 | -9,999,999,999,999,999,999 | 9,999,999,999,999,999,999 |
| US Dollar | 2 | -99,999,999,999,999,999.99 | 99,999,999,999,999,999.99 |
| Omani Rial | 3 | -9,999,999,999,999,999.999 | 9,999,999,999,999,999.999 |
| Bitcoin | 8 | -99,999,999,999.99999999 | 99,999,999,999.99999999 |
| Etherium | 9 | -9,999,999,999.999999999 | 9,999,999,999.999999999 |
| Example | Scale | Minimum | Maximum |
| ------------ | ----- | ------------------------------------ | ----------------------------------- |
| Japanese Yen | 0 | -9,999,999,999,999,999,999 | 9,999,999,999,999,999,999 |
| US Dollar | 2 | -99,999,999,999,999,999.99 | 99,999,999,999,999,999.99 |
| Omani Rial | 3 | -9,999,999,999,999,999.999 | 9,999,999,999,999,999.999 |
| Bitcoin | 8 | -99,999,999,999.99999999 | 99,999,999,999.99999999 |
| Etherium | 9 | -9,999,999,999.999999999 | 9,999,999,999.999999999 |
Subnormal numbers are not supported to ensure peak performance.
Consequently, decimals between -0.00000000000000000005 and 0.00000000000000000005
Expand All @@ -53,18 +53,18 @@ or errors.
The package provides methods for converting decimals:
- [Parse], [Decimal.String]:
convert from and to string.
- [NewFromFloat64], [Decimal.Float64]:
convert from and to float.
- [New], [NewFromInt64], [Decimal.Int64]:
convert from and to int.
- from/to string:
[Parse], [Decimal.String], [Decimal.Format].
- from/to float64:
[NewFromFloat64], [Decimal.Float64].
- from/to int64:
[New], [NewFromInt64], [Decimal.Int64].
See the documentation for each method for more details.
# Operations
Each arithmtic operation is carried out in two steps:
Each arithmetic operation is carried out in two steps:
1. The operation is initially performed using uint64 arithmetic.
If no overflow occurs, the exact result is immediately returned.
Expand All @@ -76,19 +76,19 @@ Each arithmtic operation is carried out in two steps:
If any significant digit is lost, an overflow error is returned.
Step 1 was introduced to improve performance by avoiding heap allocation
of [big.Int] and the complexities associated with [big.Int] arithmetic.
It is expected that in transactional financial systems, for the majority of
arithmetic operations, an exact result will be successfully computed during step 1.
for [big.Int] and the complexities associated with [big.Int] arithmetic.
It is expected that, in transactional financial systems, the majority of
arithmetic operations will successfully compute an exact result during step 1.
The following rules are used to determine the significance of digits during step 2:
- [Decimal.Add], [Decimal.Sub], [Decimal.Mul], [Decimal.FMA], [Decimal.Pow],
[Decimal.Quo], [Decimal.QuoRem], [Decimal.Inv]:
all digits in the integer part are significant, while the digits in the
fractional part are insignificant.
All digits in the integer part are significant, while digits in the
fractional part are considered insignificant.
- [Decimal.AddExact], [Decimal.SubExact], [Decimal.MulExact], [Decimal.FMAExact],
[Decimal.PowExact], [Decimal.QuoExact]:
all digits in the integer part are significant. The significance of digits
All digits in the integer part are significant. The significance of digits
in the fractional part is determined by the scale argument, which is typically
equal to the scale of the currency.
Expand All @@ -99,43 +99,45 @@ an explicit context.
Instead, the context is implicit and can be approximately equated to
the following settings:
| Attribute | Value |
| ----------------------- | ----------------------------------------------- |
| Precision | 19 |
| Maximum Exponent (Emax) | 18 |
| Minimum Exponent (Emin) | -19 |
| Tiny Exponent (Etiny) | -19 |
| Rounding Method | Half To Even |
| Enabled Traps | Division by Zero, Invalid Operation, Overflow |
| Disabled Traps | Inexact, Clamped, Rounded, Subnormal, Underflow |
| Attribute | Value |
| ----------------------- | ----------------------------------------------- |
| Precision | 19 |
| Maximum Exponent (Emax) | 18 |
| Minimum Exponent (Emin) | -19 |
| Tiny Exponent (Etiny) | -19 |
| Rounding Method | Half To Even |
| Enabled Traps | Division by Zero, Invalid Operation, Overflow |
| Disabled Traps | Inexact, Clamped, Rounded, Subnormal, Underflow |
The equality of Etiny and Emin implies that this package does not support
subnormal numbers.
# Rounding
Implicit rounding is applied when a result coefficient exceeds 19 digits.
In such cases, the coefficient is rounded to 19 digits using half-to-even rounding.
Implicit rounding is applied when a result exceeds 19 digits.
In such cases, the result is rounded to 19 digits using half-to-even rounding.
This method ensures that rounding errors are evenly distributed between rounding up
and rounding down.
For all arithmetic operations, except [Decimal.Pow] and [Decimal.PowExact],
For all arithmetic operations, except for [Decimal.Pow] and [Decimal.PowExact],
the result is the one that would be obtained by computing the exact mathematical
result with infinite precision and then rounding it to 19 digits.
[Decimal.Pow] and [Decimal.PowExact] may occasionally produce a result that is
off by one unit in the last place.
off by 1 unit in the last place.
In addition to implicit rounding, the package provides several methods for
explicit rounding:
- [Decimal.Round], [Decimal.Quantize], [Decimal.Rescale]:
round using half-to-even rounding.
- [Decimal.Ceil]:
rounds towards positive infinity.
- [Decimal.Floor]:
rounds towards negative infinity.
- [Decimal.Trunc]:
rounds towards zero.
- half-to-even rounding:
[Decimal.Round], [Decimal.Quantize], [Decimal.Rescale].
- rounding towards positive infinity:
[Decimal.Ceil].
- rounding towards negative infinity:
[Decimal.Floor].
- rounding towards zero:
[Decimal.Trunc].
See the documentation for each method for more details.
# Errors
Expand All @@ -144,7 +146,8 @@ Errors are returned in the following cases:
- Division by Zero.
Unlike the standard library, [Decimal.Quo], [Decimal.QuoRem], and [Decimal.Inv]
do not panic when dividing by 0. Instead, they return an error.
do not panic when dividing by 0.
Instead, they return an error.
- Invalid Operation.
[Decimal.Pow] and [Decimal.PowExact] return an error if 0 is raised to
Expand Down

0 comments on commit 7f51009

Please sign in to comment.