diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6d2e8d5..980ad97 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e375e61..8903eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 63e4354..62860cf 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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% } ``` @@ -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 diff --git a/decimal.go b/decimal.go index 11e2efe..28a7a75 100644 --- a/decimal.go +++ b/decimal.go @@ -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 @@ -46,7 +42,7 @@ 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 @@ -54,7 +50,7 @@ func newUnsafe(neg bool, coef fint, scale int) Decimal { 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: @@ -158,6 +154,7 @@ 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: @@ -165,11 +162,8 @@ func MustNew(coef int64, scale int) Decimal { // - 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) } @@ -178,13 +172,18 @@ func NewFromInt64(whole, frac int64, scale int) (Decimal, error) { 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) + } } return d, nil } @@ -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) @@ -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. @@ -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 { @@ -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 } @@ -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]. // @@ -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 @@ -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() @@ -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 @@ -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 { @@ -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 @@ -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() @@ -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 @@ -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() @@ -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 @@ -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() @@ -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) @@ -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 @@ -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() diff --git a/decimal_test.go b/decimal_test.go index aaac0d3..4be4332 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -87,6 +87,17 @@ func TestNew(t *testing.T) { }) } +func TestMustNew(t *testing.T) { + t.Run("error", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("MustNew(0, -1) did not panic") + } + }() + MustNew(0, -1) + }) +} + func TestNewFromInt64(t *testing.T) { t.Run("success", func(t *testing.T) { tests := []struct { @@ -95,9 +106,9 @@ func TestNewFromInt64(t *testing.T) { want string }{ {0, 0, 0, "0"}, - {0, 0, 1, "0.0"}, - {0, 0, 2, "0.00"}, - {0, 0, 19, "0.0000000000000000000"}, + {0, 0, 1, "0"}, + {0, 0, 2, "0"}, + {0, 0, 19, "0"}, {-1, -1, 1, "-1.1"}, {-1, -1, 2, "-1.01"}, @@ -107,6 +118,7 @@ func TestNewFromInt64(t *testing.T) { {1, 1, 1, "1.1"}, {1, 1, 2, "1.01"}, {1, 1, 3, "1.001"}, + {1, 100000000, 9, "1.1"}, {1, 1, 18, "1.000000000000000001"}, {100000000000000000, 100000000000000000, 18, "100000000000000000.1"}, {1, 1, 19, "1.000000000000000000"}, @@ -134,7 +146,9 @@ func TestNewFromInt64(t *testing.T) { "different signs 1": {-1, 1, 0}, "fraction range 1": {1, 1, 0}, "scale range 1": {1, 1, -1}, - "scale range 2": {1, 0, 20}, + "scale range 2": {1, 0, -1}, + "scale range 3": {1, 1, 20}, + "scale range 4": {1, 0, 20}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { @@ -368,6 +382,17 @@ func TestParse(t *testing.T) { }) } +func TestMustParse(t *testing.T) { + t.Run("error", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("MustParse(\".\") did not panic") + } + }() + MustParse(".") + }) +} + func TestDecimal_String(t *testing.T) { t.Run("success", func(t *testing.T) { tests := []struct { @@ -2641,6 +2666,19 @@ func TestDecimal_Clamp(t *testing.T) { }) } +func TestNullDecimal_Scan(t *testing.T) { + t.Run("[]byte", func(t *testing.T) { + tests := []string{"."} + for _, tt := range tests { + got := NullDecimal{} + err := got.Scan([]byte(tt)) + if err == nil { + t.Errorf("Scan(%q) did not fail", tt) + } + } + }) +} + /****************************************************** * Fuzzing ******************************************************/ diff --git a/doc.go b/doc.go index d688c9a..d71053d 100644 --- a/doc.go +++ b/doc.go @@ -1,111 +1,165 @@ /* Package decimal implements immutable decimal floating-point numbers. +It is designed specifically for use in transactional financial systems. +This package generally follows principles set by [ANSI X3.274-1996 (section 7.4)]. -This packages is designed specifically for use in transactional financial systems. -The amounts involved in financial transactions typically do not exceed -99,999,999,999,999,999.99, so uint64 is used to store decimal coefficients, -which reduces heap allocations, lowers memory consumption, and improves performance. - -# Features - - - Decimal values are immutable, making them safe to use in multiple goroutines. - - Methods are panic-free and pure, returning errors in cases such as uint64 - overflow or division by zero. - - [Decimal.String] produces simple and straightforward representation without - scientific or engineering notation. - - Arithmetic operations use half-even rounding, also known as "banker's rounding". - - Special values such as NaN, Infinity, or signed zeros are not supported, - ensuring that arithmetic operations always produce well-defined results. - -# Supported Ranges - -The range of a decimal value depends on the size of its coefficient. -Since the coefficient is stored as an uint64, a [Decimal] can have a maximum of -19 digits. -Additionally, the range of the [Decimal] depends on its scale, which determines -the number of decimal places. -Here are some examples of ranges supported for frequently used scales: - - | Scale | Minimum | Maximum | Example | - | ----- | ------------------------------------ | ----------------------------------- | -------------------------- | - | 0 | -9,999,999,999,999,999,999 | 9,999,999,999,999,999,999 | Japanese Yen | - | 2 | -99,999,999,999,999,999.99 | 99,999,999,999,999,999.99 | US Dollar | - | 3 | -9,999,999,999,999,999.999 | 9,999,999,999,999,999.999 | Omani Rial | - | 8 | -99,999,999,999.99999999 | 99,999,999,999.99999999 | Bitcoin | - | 9 | -9,999,999,999.999999999 | 9,999,999,999.999999999 | US Dollar (high-precision) | - | | | | or Etherium | +# Representation + +A decimal value is a struct with three fields: + + - Sign: a boolean indicating whether the decimal is negative. + - Coefficient: an unsigned integer that keeps numeric value of the decimal without + the decimal point. + - Scale: a non-negative integer indicating the position of the decimal point + within the coefficient. + For example, a decimal with a coefficient of 12345 and a scale of 2 represents + the value 123.45. + Conceptually the scale can be understood as the opposite of the exponent in + scientific notation. + For example, a scale of 2 (as in 0.01) corresponds to an exponent of -2. + The range of allowed values for the scale is from 0 to 19. + +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 have the same value, but they +have different scales and coefficients. + +# Constraints + +The range of a decimal value is determined by its scale. +Here are some examples of 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 | + +Subnormal numbers are not supported to ensure peak performance. +Consequently, decimals between -0.00000000000000000005 and 0.00000000000000000005 +(inclusive) are rounded to 0. + +Special values such as NaN, Infinity, or negative zeros are not supported. +This ensures that arithmetic operations always produce either valid decimal values +or errors. + +# Conversions + +The package provides methods for converting decimals: + + - [Parse], [Decimal.String]: + from and to string. + - [NewFromFloat64], [Decimal.Float64]: + from and to float. + - [New], [NewFromInt64], [Decimal.Int64]: + from and to int. + +See the documentation for each method for more details. # Operations -Arithmetic operations in this package are based on [General Decimal Arithmetic] -and usually involve two steps: +Each operation is carried out in two steps: - 1. The operation is first performed using only uint64 variables. - If no overflow occurs, the result is returned. + 1. The operation is initially performed using uint64 arithmetic. + If no overflow occurs, the exact result is immediately returned. If an overflow occurs, the operation proceeds to step 2. - 2. The operation is performed again using [big.Int] variables. - The result is rounded to fit into 19 digits. - If no significant digits are lost during rounding, the result is returned. - If significant digits are lost, an error is returned. + 2. The operation is repeated with increased precision using [big.Int] arithmetic. + The result is then rounded to 19 digits. + If no significant digits are lost during rounding, the inexact result is returned. + If any significant digit is lost, an overflow error is returned. -The purpose of the first step is to optimize the performance of arithmetic -operations and reduce memory consumption. -Since the coefficient is stored as an uint64, arithmetic operations using only -uint64 variables can be performed quickly and efficiently. -It is expected that most of the arithmetic operations will be successfully -completed during the first step. +The 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, majority of the arithmetic +operations will be successfully completed during the step 1. -The following rules are used to determine the significance of digits: +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]: - All digits in the integer part are significant, while the digits in the + all digits in the integer part are significant, while the digits in the fractional part are 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. +# Context + +Unlike many other decimal libraries, this package does not provide +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 | + +Equality of Etiny and Emin implies that this package does not support subnormal numbers. + # Rounding -To fit the results of arithmetic operations into 19 digits, the package -uses half-to-even rounding, which ensures that rounding errors are -evenly distributed between rounding up and rounding down. +Implicit rounding is applied when a result coefficient has more than 19 digits, +in this case the coefficient is rounded to 19 digits using half-to-even rounding, +which ensures that rounding errors are evenly distributed between rounding up +and rounding down. + +For all operations, excluding [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. -In addition to implicit half-to-even rounding, the Decimal package provides -several methods for explicit rounding: +[Decimal.Pow] and [Decimal.PowExact] operations may occasionally produce a result that is +off by one unit in the last place. - - [Decimal.Ceil]: rounds towards positive infinity. - - [Decimal.Floor]: rounds towards negative infinity. - - [Decimal.Trunc]: rounds towards zero. - - [Decimal.Round], [Decimal.Quantize], [Decimal.Rescale]: use half-to-even rounding. +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. # Errors -Arithmetic operations return errors in the following cases: +All methods are panic-free and pure, returning errors in cases such as +overflow, invalid operation or division by zero. + + - Division by Zero. + Unlike the standard library, arithmetic operations do not panic when + dividing by zero. Instead, an error is returned. - 1. Decimal overflow. - This error occurs when significant digits are lost during rounding to fit - 19 digits. - This typically happens when dealing with large numbers or when you requested - large number of digits after the decimal point to be considered signigicant. - Refer to the supported ranges section, if your application needs to handle - numbers that are close to the minimum or maximum values, this package may - not be suitable. - Consider using packages that store coefficients using [big.Int] type, - such as [ShopSpring Decimal] or [CockroachDB Decimal]. + - Invalid Operation. + [Decimal.Pow] may return invalid operation error if zero is raised to + a negative power. - 2. Division by zero. - Unlike the standard library, this package does not panic when dividing by zero. - Instead, it returns an error. + - Overflow Error. + Unlike standard integers, there is no "wrap around" for decimals at certain + sizes, for out-of-range values operations return an overflow error. -Arithmetic operations do not return error in case of decimal underflow. + - Underflow Error. + Operations do not return an error in case of decimal underflow. If result + of an operation is a decimal between -0.00000000000000000005 and + 0.00000000000000000005 inclusive it will be rounded to 0. -[General Decimal Arithmetic]: https://speleotrove.com/decimal/daops.html -[ShopSpring Decimal]: https://pkg.go.dev/github.com/shopspring/decimal -[CockroachDB Decimal]: https://pkg.go.dev/github.com/cockroachdb/apd +[ANSI X3.274-1996 (section 7.4)]: https://speleotrove.com/decimal/dax3274.html [big.Int]: https://pkg.go.dev/math/big#Int */ package decimal diff --git a/doc_test.go b/doc_test.go index 73bec89..c5809b8 100644 --- a/doc_test.go +++ b/doc_test.go @@ -107,20 +107,19 @@ func Example_piApproximation() { // 3.141592653589793238 } -// This example demonstrates the advantage of decimal types for financial calculations. -// It calculates sum 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1. +// This example demonstrates the advantage of decimals for financial calculations. +// It computes the sum 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1. // In decimal arithmetic, the result is exactly 1.0. -// In float64 arithmetic, due to binary floating-point representation, -// the sum slightly deviates from 1.0. -func Example_float64Inaccuracy() { - // Decimal arithmetic +// In float64 arithmetic, the result slightly deviates from 1.0 due to binary +// floating-point representation. +func Example_floatInaccuracy() { d := decimal.MustParse("0.0") e := decimal.MustParse("0.1") for i := 0; i < 10; i++ { d, _ = d.Add(e) } fmt.Println(d) - // float64 arithmetic + f := 0.0 for i := 0; i < 10; i++ { f += 0.1