Awesome Go

dec128

CategoryFinancial
SubcategoryFinancial
Stars52

High performance 128-bit fixed-point decimal numbers

About dec128

This large README is shown as a plain-text preview. Read the full README at the source

[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua)

# dec128

[![Go Reference](https://pkg.go.dev/badge/github.com/jokruger/dec128.svg)](https://pkg.go.dev/github.com/jokruger/dec128)
[![lint](https://github.com/jokruger/dec128/actions/workflows/lint.yml/badge.svg)](https://github.com/jokruger/dec128/actions/workflows/lint.yml)
[![codecov](https://codecov.io/gh/jokruger/dec128/graph/badge.svg?token=TQWE8PA4AN)](https://codecov.io/gh/jokruger/dec128)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)

Zero-dependency 128-bit fixed-point decimal numbers in Go, built for money, ledgers and banking arithmetic:
exact SQL `NUMERIC` semantics, no heap allocation, no panics and no returned errors.

`dec128` is named for its coefficient: a 128-bit integer with a decimal scale, the same fixed-point model as
Apache Arrow's and Parquet's `Decimal128`. It is not IEEE 754 `decimal128`, which is a floating-point format; that is
supported as an interchange encoding only. The arithmetic follows SQL `NUMERIC` as implemented by PostgreSQL, and
the test suite checks it against PostgreSQL digit for digit.

### The core, and the completeness set

Two claims run through this README and they apply to different parts of the library, so it is worth separating them
before anything else.

**The core is the financial one** -- the decimal type itself, `Add`, `Sub`, `Mul`, `Div`, comparison, rounding,
allocation, splitting, parsing, printing and the interchange codecs. It is what `dec128` is for and what its promises
are about: fixed 24-byte layout, **zero heap allocation**, and every result exact or rounded by a single documented
decision. `TestAllocationGates` holds every one of these operations to zero allocations per call.

**The completeness set is the scientific one** -- roots, rational powers, and the transcendentals `Exp`, `Ln`, `Ln1p`,
`Expm1`, `Log10`, `Log2` and `Pow`. These are here so the type is a complete numeric type rather than because a ledger
needs them, and they do not carry the core's promises:

| | allocates | exactness |
|---|---|---|
| the core, and `PowIntRound` | never | exact, or one documented rounding (`PowIntRound` is faithfully rounded) |
| `NthRootRound`, `PowRational` | yes | **correctly rounded** -- the decision is an exact integer comparison |
| `Exp`, `Ln`, `Ln1p`, `Expm1`, `Log10`, `Log2`, `Pow` | yes, unless `Pow` gets an integer exponent or the logarithm gets an exact power of its base | **faithfully rounded** -- within one unit in the last place, measured, not proven |
| `Prod` and its forms | yes | exact in the middle, **one rounding** at the end |

They allocate because their exact intermediate is wider than any fixed register, so they run on `math/big`. Both lists
are maintained in the package documentation and pinned from both sides by the test suite: everything not on the
allocating list is held to zero allocations, and everything on it is checked to really allocate. If you are counting
allocations in a hot path, stay in the core; the completeness set is for the calculation you do once.

## Key Objectives / Features

- [x] High performance
- [x] Zero dependencies
- [x] Zero memory allocation throughout the core (see above; the scientific completeness set allocates)
- [x] 38 significant digits (39 near the maximum), up to 19 of them after the decimal point
- [x] Fixed 24-byte layout with no indirection (128-bit coefficient, scale, sign/state)
- [x] No panic or error arithmetics (use NaN instead)
- [x] Immutability (methods return new instances)
- [x] Basic arithmetic operations required for financial calculations (specifically for banking and accounting)
- [x] Easy to use
- [x] Easy to integrate with external systems (e.g. databases, accounting systems, JSON, etc.)
- [x] Financially correct rounding: eight rounding modes, chosen per call or globally
- [x] Results that do not fit are rounded to fit, never silently wrong; NaN only when the integer part cannot be held
- [x] Correct comparison of numbers encoded in different scales (e.g. 1.0 == 1.00)
- [x] Correct handling of NaN values (e.g. NaN + 1 = NaN)
- [x] Conversion to canonical representation (e.g. 1.0000 -> 1)
- [x] Conversion to fixed string representation (e.g. 1.0000 -> "1.0000")
- [x] Conversion to human-readable string representation (e.g. 1.0000 -> "1")
- [x] Scientific notation: parsed by `FromString`, printed on request (e.g. "1.5e3" -> 1500, 12345 -> "1.2345e+4")
- [x] Per-call scale and rounding mode for every operation (`AddRound`, `SubRound`, `MulRound`, `DivRound`, `SqrtRound`, `PowIntRound`) -- a documented subset that reads no global configuration, so the same inputs give the same bytes in every process
- [x] Fused multiply-add (`MulAddRound`), fused multiply-divide (`MulDivRound`), fused add-and-divide (`AddQuoRound`) and a wide exact accumulator (`Accumulator`, with `AddMul`): a chain of multiply-accumulate rounds once, not once per term
- [x] Exact products with one rounding (`Prod`, `ProdRound`, `ProdSlice`, `ProdSliceRound`): chain-linking a year of daily factors drifts by several units in the last place if folded with `Mul`, and by nothing here
- [x] Integer powers with guard digits (`PowIntRound`): `(1+r)^360` correctly rounded rather than carrying a dozen roundings
- [x] Exact splitting of an amount (`Allocate`, `Split`, `AllocateResidual`, `SplitResidual`): the shares sum to the whole, digit for digit, by largest remainder or with the difference going to a named party
- [x] Rounding to a negative number of places, to a multiple and to a number of significant digits (`RoundToPlaces`, `RoundToMultiple`, `RoundToSignificant`) for cash rounding, disclosure rules and quoted rates
- [x] The n-th root (`NthRootRound`), correctly rounded: the inverse of compounding, for converting an effective rate to a periodic one
- [x] Column checks before the insert (`FitsNumeric`, `SignificantDigits`, `IntegerDigits`) and `RescaleRoundInexact` to make a value fit and say what it cost
- [x] `fmt.Formatter`, so `%.2f` on a `Dec128` means what it says
- [x] Interchange codecs: PostgreSQL `numeric` binary, IEEE 754 decimal128 (BID), Arrow/Parquet int128, and the SQL driver `Decompose`/`Compose` pair
- [x] An exact `math/big` bridge (`Rat`, `BigInt`, `FromRat`): a `Dec128` is a rational, and the conversion says so in both directions
- [x] The General Decimal Arithmetic operations the specification names: `Log10`, `Log2`, `Logb`, `CopySign`, `CmpTotal`, `RemainderNear` and `Inv`
- [x] Configurable SQL `NULL` / JSON `null` handling that round-trips (`SetNullValue`)

## Install

Run `go get github.com/jokruger/dec128`

## Requirements

This library requires Go version `>=1.24` (as declared in `go.mod`).

## Ecosystem

- [**pgxdec128**](https://github.com/jokruger/pgxdec128) — a [pgx](https://github.com/jackc/pgx) v5 `pgtype.Codec`
  that reads and writes PostgreSQL `numeric` and `numeric[]` columns as `Dec128` straight from the binary protocol,
  with no `pgtype.Numeric`, no `math/big.Int`, no text round trip and no allocation.
- [**kavun**](https://github.com/jokruger/kavun) — an embeddable scripting language for Go whose `decimal` type is a
  `Dec128`, giving rules and decisioning scripts exact money arithmetic instead of a float workaround.
- [**fin128**](https://github.com/jokruger/fin128) — deterministic financial mathematics on `Dec128`: day counts with
  exact (unrounded) year fractions, accrual, time value of money, depreciation, discount instruments and rate solving,
  with no `float64`, and byte-identical results on amd64 and arm64.
- [**go-decimal-benchmark**](https://github.com/jokruger/go-decimal-benchmark) — the comparative harness the numbers
  in [Benchmarks](#benchmarks) come from.

## Development

```bash
make test       # run tests with coverage
make view       # open the HTML coverage report
make lint       # run golangci-lint (installs the pinned version on first use)
make lint-fix   # run golangci-lint with --fix
```

Linting is configured in `.golangci.yml` and enforced in CI by `.github/workflows/lint.yml`.

## Documentation

https://pkg.go.dev/github.com/jokruger/dec128

## Usage

```go
package main

import (
    "fmt"
    "github.com/jokruger/dec128"
)

func main() {
    principal := dec128.FromString("1000.00")
    annualRate := dec128.FromString("5.0")
    days := 30

    // multiply first and divide once at the end: dividing early throws away digits
    // that the later multiplications would have needed
    accrued := principal.Mul(annualRate).MulInt(days).Div(dec128.FromInt64(36500))

    if err := accrued.ErrorDetails(); err != nil {
        panic(err) // a chain reports failure once, at the end
    }

    accrued = accrued.RoundBank(2)

    fmt.Printf("Principal: %v\n", principal.StringFixed())
    fmt.Printf("Annual Interest Rate: %v\n", annualRate.String())
    fmt.Printf("Days: %v\n", days)
    fmt.Printf("Accrued Interest: %v\n", accrued.String())

    total := principal.Add(accrued)
    fmt.Printf("Total after %v days: %v\n", days, total.StringFixed())
}
```

## The contract

**Arithmetic never panics and never returns an error.** A failed operation returns a NaN carrying the reason, and NaN
propagates, so a whole calculation is one expression checked once at the end:

```go
total := principal.Mul(rate).Add(fee).RoundBank(2)
if err := total.ErrorDetails(); err != nil {
    return err
}
```

`ErrorDetails` returns a plain `error` and is the terminal check for a chain; `IsNaN` is the boolean form. Nothing
forces you to call either — unlike an ignored `err` there is no compiler or linter backstop, so make the check part of
your review habits.

Four things that surprise people:

- **`IsZero`, `IsNegative` and `IsPositive` all return `false` for NaN.** A guard written as `if !d.IsZero()` does *not*
  catch a failed calculation. Test `IsNaN` first when the difference matters.
- **`MarshalJSON` and `Value` both encode NaN as the string `"NaN"`.** A JSON consumer that accepts strings takes it
  without complaint, and PostgreSQL accepts `'NaN'` for a `numeric` column. Check `IsNaN` before a failed calculation
  reaches storage.
- **A result that does not fit is rounded, not NaN.** The scale is reduced to the largest at which the integer part fits
  and the dropped digits are rounded with the configured mode (truncation by default). NaN means the integer part itself
  does not fit. That is the design, not a failure -- see [Safe operating range](#safe-operating-range) for where it
  starts. `SetLossPolicy` decides whether a dropped digit is acceptable at all.
- **A product of two small values can silently become zero.** The smallest non-zero value is `1e-19`, so
  `1e-10 * 1e-10` is `0` under the default policy. `SetLossPolicy(LossNaNOnUnderflow)` turns that into
  `NaN(Underflow)` while leaving `1/3` a value.
- **`SetDefaultScale`, `SetArithmeticRounding`, `SetLossPolicy`, `SetTrimOutput` and `SetNullValue` are
  process-global.** Set them once during initialization; changing them while other goroutines calculate is a data
  race. `SetArithmeticRounding` also writes the loss policy, so call `SetLossPolicy` second.

## Safe operating range

A `Dec128` carries **38 significant decimal digits** -- 39 while the coefficient is at or below
`340282366920938463463374607431768211455` -- shared freely between the integer and the fractional part, with **at most
19 of them after the decimal point**. That single budget is the whole size contract.

> **In one sentence:** if every operand has at most 9 decimal places and every value, intermediate products included,
> stays below `10^20`, then `Add`, `Sub` and `Mul` are exact and nothing is ever rounded.

`TestSafeZoneClaim` holds the library to that promise. Outside the zone dec128 still works; it just starts trading
fractional digits for integer digits. Two envelopes bound it.

**The top -- precision shrinks as magnitude grows.** All 19 fractional places are available while

```
|x| <= 34028236692093846346.3374607431768211455
```

whose integer part is about `3.4e19`, 3.69 times the largest `int64`. Above that one place is lost per decade, down to
none at `3.4e38`.

**The bottom -- the smallest non-zero value is `1e-19`.** `FromString("1e-20")` returns `NaN(ScaleOutOfRange)`;
arithmetic that would produce a smaller magnitude returns zero.

| decimal places | largest value | `a*b` stays exact while \|a*b\| <= |
|---|---|---|
| 0 | 3.40e38 | 3.40e38 |
| 2 (cents) | 3.40e36 | 3.40e34 |
| 4 | 3.40e34 | 3.40e30 |
| 6 | 3.40e32 | 3.40e26 |
| 8 (satoshi) | 3.40e30 | 3.40e22 |
| 9 | 3.40e29 | 3.40e20 |
| 10 | 3.40e28 | never -- every product rounds |
| 18 (wei) | 3.40e20 | never |
| 19 | 3.40e19 | never |

`Add` and `Sub` never raise the scale, so they stay exact while the running total fits the *largest value* column.
`Mul` adds the scales: a product is exact only when `s1 + s2 <= 19` **and** it fits the third column. At a uniform
working scale that means **9 places or fewer**; from 10 places up every multiplication rounds. `Div` and `Sqrt` compute
to `DefaultScale()` places and are exact only when the result terminates within that many digits -- so they are bounded
by the scale cap rather than by the coefficient, and `1/3` has 19 significant digits, not 38.

**Where this bites.** The top end is unreachable for money: `3.40e36` at two decimal places is many orders of magnitude
beyond any real balance. The bottom end is not. The same `s1 + s2 <= 19` rule that ends exact multiplication also makes
small products collapse:

```go
dec128.FromString("0.000000001").Mul(dec128.FromString("0.000000001"))   // 0.000000000000000001, exact
dec128.FromString("0.0000000001").Mul(dec128.FromString("0.0000000001")) // 0 -- 1e-20 is not representable
```

Rates, probabilities and per-unit factors around `1e-10` reach it. Chain such factors in one expression rather than
storing each intermediate, rescale into a larger unit, or set `SetLossPolicy(LossNaNOnUnderflow)` to make the collapse
loud. `MaxAtScale` and `QuantumAtScale` report the two ends of the range at any scale.

## Scale and rounding

Scale is preserved rather than normalized, so `1.5` and `1.50` are distinct representations that compare equal.
`Add`/`Sub` take the larger operand scale, `Mul` adds them, and a zero result keeps that scale too
(`1.50 - 1.50` is `0.00`). When the exact result needs more than 19 places or more than 128 bits, the scale is reduced
to the largest that fits and the digits below it are rounded with the mode set by `SetArithmeticRounding`
(`ROUND_TOWARD_ZERO` by default) -- unless `SetLossPolicy` says the loss is unacceptable.

### Loss policy

A rounding mode picks a *direction*; whether arithmetic may discard a digit at all is the separate `LossPolicy`,
because losing digits is not one condition but three. An integer part that will not fit at scale 0 is an **overflow**
and is always `NaN(Overflow)`. A result that keeps its significant digits and drops only the tail is **inexact**, which
`1/3` and `Sqrt(2)` are at every scale. A result that rounds to zero from non-zero operands has lost every significant
digit it had: that is an **underflow**, and it is the one case where a plausible-looking value hides a total loss of
information.

| policy | `1/3` | `1e-10 * 1e-10` | `1e20 * 1e20` |
|---|---|---|---|
| `LossRound` (default) | `0.3333333333333333333` | `0` | `NaN(Overflow)` |
| `LossNaNOnUnderflow` | `0.3333333333333333333` | `NaN(Underflow)` | `NaN(Overflow)` |
| `LossNaNOnInexact` | `NaN(Inexact)` | `NaN(Inexact)` | `NaN(Overflow)` |

`LossNaNOnUnderflow` is the recommended setting for money: a non-zero amount never silently becomes zero, and an
inexact division still returns a value. `SetArithmeticRounding(ROUND_NAN)` is the deprecated spelling of
`SetLossPolicy(LossNaNOnInexact)`; it still behaves exactly as it did in v1.1, and a program that sets both must call
`SetLossPolicy` second.

`Div` and `Sqrt` compute at the default scale (`SetDefaultScale`, 19 by default) and give an exact result its *ideal*
scale: `1/2` is `0.5`, `1.00/2` is `0.50`, `1/3` keeps all 19 places. `QuoRem` does not consult the default: the
quotient is always an integer.

`MulRound`, `DivRound` and `SqrtRound` take the scale and the rounding mode per call and return exactly that scale,
computed once with an exact rounding decision:

```go
rate := annual.DivRound(dec128.FromInt64(365), 12, dec128.ROUND_HALF_AWAY_FROM_ZERO) // 0.013698630137
fee := amount.MulRound(rate, 2, dec128.ROUND_BANK)
```

The `Round*` methods round to at most the given number of places and leave a shorter value alone;
`RescaleRound(places, mode)` is the "exactly n places" operation, the equivalent of PostgreSQL's `round(x, n)`
and `trunc(x, n)`:

```go
dec128.FromString("1.5").RoundBank(2)                            // 1.5
dec128.FromString("1.5").RescaleRound(2, dec128.ROUND_BANK)      // 1.50
dec128.FromString("200").DivRound(dec128.FromInt64(3), 2, dec128.ROUND_HALF_AWAY_FROM_ZERO) // 66.67
```

### What the globals reach, and what they do not

The five `Set*` functions are process-global and are meant to be set once at start-up, if at all. It is worth being
precise about their reach, because the natural reading -- that they are "this application's scale and rounding" -- is
not what they are:

* **`SetDefaultScale` is not a maximum scale.** The maximum is `MaxScale`, a compile-time constant of 19, and nothing
  configures it. The default scale is only the scale `Div`, `Sqrt` and `Inv` compute to when no scale is given. It
  does not touch `Add`, `Sub` or `Mul`, whose scale comes from the operands -- `Mul` adds the scales -- and is reduced
  only when the exact result will not fit.
* **`SetArithmeticRounding` is not the application's rounding rule.** For `Add`, `Sub` and `Mul` it fires only when
  digits *must* go, past 19 places or past 128 bits, which is the overflow path and not the everyday one. For `Div`
  and `Sqrt` it applies to every call, because those round by nature.
* **`SetLossPolicy`** decides whether discarding a digit is allowed at all, and it is the one of the three worth
  setting for money.

So the shape a calculation wants is: **keep the intermediates wide, round once at the end, at the boundary.**

```go
// start-up: nothing, or at most the loss policy
dec128.SetLossPolicy(dec128.LossNaNOnUnderflow) // a non-zero amount never silently becomes zero

// the calculation: exact throughout, no business rounding yet
gross := qty.Mul(unitPrice)      // exact: the scales add
vat := gross.MulPercent(vatRate) // exact: two places more again
total := gross.Add(vat)          // exact

// the boundary: the business scale and the business rounding, once
out := total.RescaleRound(6, dec128.ROUND_BANK)
```

Setting `SetDefaultScale(6)` to say "this application works to six places" does something other than it looks like:
it leaves `Mul` alone and makes every `Div` throw away thirteen digits the later steps would have used, so `1/3` is
`0.333333` and every step after it inherits that. Leave it at 19 and ask for six places where six places is the
answer -- `DivRound(x, 6, mode)` for a quotient that is presented, `RescaleRound(6, mode)` at the end of a chain.
Likewise `SetArithmeticRounding(ROUND_BANK)` changes the direction taken when a result has run out of digits, not how
the application's amounts are rounded; if banker's rounding is the business rule, it belongs in the `*Round` call that
produces the presented value. Both are per-call arguments for a reason, and a library that has to produce the same
bytes in every process should not read the globals at all -- see the global-free subset below.

### A worked example: six places, banker's rounding

Take the common fintech requirement: every amount in and out of the application is held at **six decimal places and

Frequently Asked Questions

What is dec128?

dec128 is a Financial library for the Go programming language. High performance 128-bit fixed-point decimal numbers

How do I install dec128?

Install dec128 with the Go module system using `go get jokruger/dec128`. Check the repository for the current installation instructions.

What category does dec128 belong to?

dec128 is listed under Financial, specifically Financial.

← Back to Financial