decimal

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 10 Imported by: 0

README

decimal

A fixed-point decimal for Go, built for financial values and as a mostly drop-in replacement for shopspring/decimal.

A Decimal is a 128-bit coefficient over a power of ten, held by value: no pointers, no allocation, nothing for the garbage collector to scan. Every value is normalized, so == is value equality and a Decimal is a valid map key.

Why

shopspring/decimal is exact and battle-tested, but every value wraps a *big.Int: parsing, adding or multiplying allocates on the heap, and every live value is a pointer the garbage collector has to chase. On a hot path, such as a crypto market handling a stream of prices and sizes, that is allocation churn and GC work on every message.

This package trades unbounded precision for a fixed 24-byte value type that never touches math/big:

  • No heap allocation to parse, compare, round, add, subtract, multiply or divide, or to render with Append. String and the Marshal* methods allocate once, for the result they return; error paths allocate their error.
  • Nothing for the GC to scan. A Decimal holds no pointers, so it can sit in a local, a struct field or a million-element slice, and the collector skips it.
  • Wide enough for crypto: 39 significant digits and up to 19 fraction digits, so wei-denominated amounts (10⁻¹⁸) fit exactly.

On a per-trade workload (parse price and size, compute notional and fee, accumulate both, render the notional) it runs 1.5× faster with 0 allocations against shopspring's 14. With a million values live on the heap, a full GC cycle costs 18× less and the live heap is less than half the size. See Performance.

go get github.com/jankremlacek/decimal

Supports the two latest Go releases, currently 1.26 and 1.27. The module has no runtime dependencies.

Usage

package main

import (
  "fmt"

  "github.com/jankremlacek/decimal"
)

func main() {
  price := decimal.MustFromString("64500.4")
  qty := decimal.New(125, -2) // 1.25

  total := price.Mul(qty)
  fmt.Println(total)                // 80625.5
  fmt.Println(total.StringFixed(2)) // 80625.50

  share := total.Div(decimal.NewFromInt(3))
  fmt.Println(share) // 26875.1666666666666667

  d, err := decimal.NewFromString("1.5e-8")
  if err != nil {
    panic(err)
  }
  fmt.Println(d, d.Cmp(decimal.Zero)) // 0.000000015 1
}

Model

Range. The representable set is exactly ±coefficient / 10^prec, where coefficient is at most 2¹²⁸−1 and prec is at most 19 (MaxFractionDigits; wei, 10⁻¹⁸, fits with a digit to spare). Both bounds bind together, so magnitude and precision trade against each other:

Shape Largest magnitude
no fraction 340282366920938463463374607431768211455
full 19-digit fraction 34028236692093846346.3374607431768211455
smallest nonzero 0.0000000000000000001

A result that does not fit is an error or a panic, never a silent loss and never a slow fallback; the package does not use math/big.

Arithmetic is exact-or-panic. Add, Sub and Mul return the exact result or panic with ErrRange. Only Div rounds: every quotient rounds half away from zero at sixteen fraction digits, exactly as shopspring does, so a quotient that fits sixteen digits is exact. A zero divisor panics with ErrDivByZero.

Untrusted input returns errors. The constructors (New, NewFromFloat, MustFromString) panic on out-of-range input, which suits literals and constants. NewFromString, the text/JSON/binary/gob decoders and sql.Scanner return an error instead and never panic.

Immutability. Every method works on a copy and never mutates its receiver or arguments. The decoders are the deliberate exception: on successful non-null input they replace the receiver with a freshly parsed canonical value.

Serialization

Form Encoding
String/text Canonical decimal text: no exponent, no trailing fraction zeros. Append renders allocation-free.
JSON Canonical text, quoted — a reader that parses a bare number into float64 loses precision. Decoding accepts quoted and bare numbers; null leaves the receiver untouched.
Binary, gob Compact layout: one header byte (precision plus sign bit), then the coefficient as minimal big-endian bytes — 1 byte for zero, 17 at full width. Callers that persist these bytes own their versioning.
SQL Value writes canonical text; Scan accepts text, []byte, int64 and float64. A column delivered as text round-trips exactly; one delivered as float64 carries only what that float held. Postgres NUMERIC is far wider than the representable set, so a value outside it fails the scan.

Decoded values are normalized on the way in, so they are canonical.

Differences from shopspring/decimal

The parser holds shopspring's accept/reject line, and division rounds the way shopspring's does; the differential fuzzers enforce both. Where behaviour differs, it is on purpose:

  • Bounded range. shopspring is arbitrary precision; values here must fit the range above. Out-of-range results panic (arithmetic) or return ErrRange (parsing and decoding).
  • Exponent cap. shopspring rejects an effective exponent past its MaxDecodeExponent. This parser's cost depends only on the text length, so it accepts e.g. a zero or an in-range value with a long zero tail past that cap.
  • JSON is always quoted; there is no MarshalJSONWithoutQuotes switch.
  • Smaller API. Available: New, NewFromInt, NewFromFloat, NewFromString, MustFromString (shopspring's RequireFromString), Zero, Min, Max, Sum; methods Add, Sub, Mul, Div, Neg, Abs, Cmp, Equal, GreaterThan[OrEqual], LessThan[OrEqual], Sign, IsZero, IsPositive, IsNegative, IsInteger, Exponent, Round, RoundFloor, RoundCeil, RoundFloorSigFigs, Truncate, Floor, Ceil, String, StringFixed, Append, TextLen, InexactFloat64, and the text, JSON, binary, gob and SQL interfaces. Not available: Float64, NewFromFloat32, DivRound, QuoRem, Shift, Equals, IntPart, BigInt, Rat, Pow, Sqrt, Format, NullDecimal and similar.

shopspring_test.go ports shopspring v1.6.0's own test fixtures for every method both packages share; each divergence is marked there.

Performance

go test -bench VsShopspring -benchmem runs both packages side by side. Measured on an Apple M4 Max with Go 1.27.

Workload and GC

Benchmark decimal shopspring
Fill: one trade, end to end 139 ns, 0 allocs 205 ns, 14 allocs (416 B)
GC: one full cycle, 1M live values 0.13 ms, 23 MiB live 2.4 ms, 54 MiB live

Fill renders with the allocation-free Append; shopspring has no equivalent, so it renders with String.

Single operations (ns/op, allocs/op)

Operation decimal shopspring Speed
NewFromString("64500.4") 14.3, 0 28.3, 2 2.0× faster
NewFromString wei (0.000…001) 15.5, 0 166, 4 10.7× faster
NewFromString 39 digits 75.5, 0 274, 5 3.6× faster
String price 17.0, 1 60.0, 2 3.5× faster
MarshalJSON price 18.3, 1 64.2, 3 3.5× faster
Add same precision 15.9, 0 19.3, 2 1.2× faster
Add across precisions 22.9, 0 28.7, 2 1.3× faster
Cmp across precisions 7.8, 0 18.3, 1 2.3× faster
Mul price × size 30.8, 0 20.7, 2 1.5× slower
Div same precision 233, 0 47.4, 4 4.9× slower
Div 38 by 29 digits 1101, 0 157, 8 7.0× slower

Mul and Div are the exceptions. They compute in portable 256-bit arithmetic, where shopspring uses math/big's assembly. Both stay allocation-free, so in a running service, where each shopspring allocation also feeds the next GC cycle, the gap is narrower than the single-operation numbers show.

Speed and zero allocation are the package's reason to exist, but nothing in the test suite enforces them. Check the benchmarks when you touch a hot path.

Contributing

  • make vet test passes, and statement coverage stays at 100%.
  • Immutability, serialization round-trips and shopspring parity have no compiler support. Each is held by a table test; a change that touches one extends its table.
  • A behaviour change keeps both differential fuzzers green under make test-fuzz. An ordinary test run only replays the pinned seeds in testdata/fuzz; it does not fuzz. A fuzzer-found defect is fixed and its input pinned as a regression seed.
  • Values are built only through the constructors and internal normalizers, never a raw struct literal, so the canonical form holds.
  • Production code must not import shopspring/decimal; it is a test-only oracle.

Run make help for the available targets.

License

MIT. shopspring_test.go ports test fixtures from shopspring/decimal, used under its MIT License; the notice is kept in that file.

Documentation

Overview

Package decimal implements a fixed-point decimal for financial values.

A Decimal is coefficient / 10^prec held by value: a 128-bit coefficient, at most nineteen fractional digits, no pointer, no allocation, nothing for the garbage collector to scan. Construction normalizes, so each value has exactly one representation and == is value equality.

The representable range trades precision against magnitude, because the two bounds bind together: the coefficient is at most 2^128−1 = 340282366920938463463374607431768211455 and prec is at most MaxFractionDigits. Carrying no fraction a value reaches ±340282366920938463463374607431768211455; carrying the full nineteen-digit fraction it reaches ±34028236692093846346.3374607431768211455; and the smallest nonzero magnitude is ±0.0000000000000000001. A result that does not fit is an error or a panic, never a slower fallback; this package does not reach for math/big.

Arithmetic is exact-or-panic: operations return the exact value or panic with ErrRange. Only parsing untrusted input (NewFromString) returns an error. Division alone rounds, matching shopspring/decimal: every quotient rounds half away from zero at sixteen fractional digits, so one that fits sixteen digits is exact.

The text and JSON forms are canonical decimal text: no exponent, no trailing fraction zeros, JSON quoted. The binary and gob form is this package's own compact layout; callers that persist it own their versioning.

Example
package main

import (
	"fmt"

	"github.com/jankremlacek/decimal"
)

func main() {
	price := decimal.MustFromString("64500.4")
	qty := decimal.New(125, -2) // 1.25

	total := price.Mul(qty)
	fmt.Println(total)
	fmt.Println(total.StringFixed(2))

	share := total.Div(decimal.NewFromInt(3))
	fmt.Println(share)

	d, err := decimal.NewFromString("1.5e-8")
	if err != nil {
		panic(err)
	}
	fmt.Println(d, d.Cmp(decimal.Zero))
}
Output:
80625.5
80625.50
26875.1666666666666667
0.000000015 1

Index

Examples

Constants

View Source
const MaxDigits = 39

MaxDigits is the longest coefficient a Decimal holds, in base-ten digits: 2^128−1 has 39. Not every 39-digit coefficient fits.

View Source
const MaxFractionDigits = 19

MaxFractionDigits is the widest fraction a Decimal holds: nineteen digits, so every scale factor fits a uint64, and wei (10^-18) is covered with a digit to spare.

Variables

View Source
var (
	// ErrSyntax reports text that does not spell a decimal.
	ErrSyntax = errors.New("decimal: invalid syntax")

	// ErrRange reports a value outside the 128-bit
	// coefficient or the nineteen-digit fraction.
	ErrRange = errors.New("decimal: out of range")

	// ErrDivByZero reports a zero divisor; [Decimal.Div] panics with it.
	ErrDivByZero = errors.New("decimal: division by zero")
)

Functions

This section is empty.

Types

type Decimal

type Decimal struct {
	// contains filtered or unexported fields
}

Decimal is a signed fixed-point decimal: coefficient / 10^prec. The zero value is zero and ready to use.

A Decimal is immutable: every method returns a new value and never mutates its receiver or arguments, so a value is safe to copy and share.

Every value is normalized — the coefficient carries no trailing zeros and zero is never negative — so == compares values and Decimal keys maps.

var Zero Decimal

Zero is the zero value of Decimal, named for comparisons such as d.Cmp(Zero).

func Max

func Max(first Decimal, rest ...Decimal) Decimal

Max returns the greatest of its arguments. first is separate so a call cannot pass none.

func Min

func Min(first Decimal, rest ...Decimal) Decimal

Min returns the least of its arguments. first is separate so a call cannot pass none.

func MustFromString

func MustFromString(v string) Decimal

MustFromString is NewFromString for text known to be well formed. It panics on anything else.

func New

func New(v int64, exp int32) Decimal

New returns v * 10^exp.

It panics when the result does not fit, which suits literals and constants; parse input that may be out of range with NewFromString.

func NewFromFloat

func NewFromFloat(v float64) Decimal

NewFromFloat returns the decimal of the shortest text that round-trips to v, exactly as strconv.FormatFloat with precision -1 renders it.

It panics on NaN, an infinity, or a value needing more than nineteen fractional digits.

func NewFromInt

func NewFromInt(v int64) Decimal

NewFromInt returns v. Every int64 fits, so it never panics.

func NewFromString

func NewFromString(v string) (Decimal, error)

NewFromString parses decimal text: an optional sign, digits with an optional fraction, and an optional power-of-ten exponent, like "-1.5" or "1.5e-8". Either minus may also be U+2212 MINUS SIGN, as in "−1.5" or "1.5e−8". The error wraps ErrSyntax or ErrRange.

func Sum

func Sum(first Decimal, rest ...Decimal) Decimal

Sum returns the arguments added exactly, with Decimal.Add's panic on a sum that does not fit.

func (Decimal) Abs

func (d Decimal) Abs() Decimal

Abs returns |d|.

func (Decimal) Add

func (d Decimal) Add(o Decimal) Decimal

Add returns d + o exactly. It panics with ErrRange when the sum does not fit.

func (Decimal) Append

func (d Decimal) Append(b []byte) []byte

Append writes what Decimal.String renders onto b, allocation-free.

func (Decimal) Ceil

func (d Decimal) Ceil() Decimal

Ceil returns the smallest integer not below d.

func (Decimal) Cmp

func (d Decimal) Cmp(o Decimal) int

Cmp reports -1, 0 or +1 as d sorts before, with, or after o.

func (Decimal) Div

func (d Decimal) Div(o Decimal) Decimal

Div returns d / o rounded half away from zero at sixteen fractional digits, exactly as shopspring rounds. It panics with ErrDivByZero on a zero divisor, and with ErrRange when the quotient does not fit.

func (Decimal) Equal

func (d Decimal) Equal(o Decimal) bool

Equal reports whether d and o hold the same value. Canonical representation makes it plain ==.

func (Decimal) Exponent

func (d Decimal) Exponent() int32

Exponent reports the power of ten the coefficient sits over: the negated fraction width of the canonical form, so zero or negative.

func (Decimal) Floor

func (d Decimal) Floor() Decimal

Floor returns the largest integer not above d.

func (*Decimal) GobDecode

func (d *Decimal) GobDecode(data []byte) error

GobDecode implements encoding/gob.GobDecoder as Decimal.UnmarshalBinary.

func (Decimal) GobEncode

func (d Decimal) GobEncode() ([]byte, error)

GobEncode implements encoding/gob.GobEncoder with the Decimal.MarshalBinary layout.

func (Decimal) GreaterThan

func (d Decimal) GreaterThan(o Decimal) bool

GreaterThan reports whether d is greater than o.

func (Decimal) GreaterThanOrEqual

func (d Decimal) GreaterThanOrEqual(o Decimal) bool

GreaterThanOrEqual reports whether d is greater than or equal to o.

func (Decimal) InexactFloat64

func (d Decimal) InexactFloat64() float64

InexactFloat64 reports the nearest float64, the name conceding what the conversion may cost.

func (Decimal) IsInteger

func (d Decimal) IsInteger() bool

IsInteger reports whether d has no fractional part.

func (Decimal) IsNegative

func (d Decimal) IsNegative() bool

IsNegative reports whether d < 0. Zero is never negative.

func (Decimal) IsPositive

func (d Decimal) IsPositive() bool

IsPositive reports whether d > 0, so false for zero.

func (Decimal) IsZero

func (d Decimal) IsZero() bool

IsZero reports whether d is zero. Zero has one representation, so it is the same as d == Zero.

func (Decimal) LessThan

func (d Decimal) LessThan(o Decimal) bool

LessThan reports whether d is less than o.

func (Decimal) LessThanOrEqual

func (d Decimal) LessThanOrEqual(o Decimal) bool

LessThanOrEqual reports whether d is less than or equal to o.

func (Decimal) MarshalBinary

func (d Decimal) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler.

The layout is one header byte holding the precision with the sign in its top bit, then the coefficient as minimal big-endian bytes. Zero writes no coefficient bytes, so a decimal costs one byte at zero and seventeen at the full 128-bit width.

func (Decimal) MarshalJSON

func (d Decimal) MarshalJSON() ([]byte, error)

MarshalJSON implements encoding/json.Marshaler, writing the value as a JSON string. A decimal is quoted on the wire because a JSON number is a float to most readers, and that would lose the precision this type exists to keep.

func (Decimal) MarshalText

func (d Decimal) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Decimal) Mul

func (d Decimal) Mul(o Decimal) Decimal

Mul returns d * o exactly. It panics with ErrRange when the product does not fit — including a product whose nonzero fraction needs more than nineteen digits.

func (Decimal) Neg

func (d Decimal) Neg() Decimal

Neg returns -d. Zero stays zero, never negative.

func (Decimal) Round

func (d Decimal) Round(places int32) Decimal

Round returns d rounded half away from zero to places fraction digits. Negative places rounds left of the point: Round(-2) rounds to hundreds. It panics with ErrRange when that carries the result out of range.

func (Decimal) RoundCeil

func (d Decimal) RoundCeil(places int32) Decimal

RoundCeil returns d rounded toward positive infinity to places fraction digits. Negative places and range behave as in Decimal.Round.

func (Decimal) RoundFloor

func (d Decimal) RoundFloor(places int32) Decimal

RoundFloor returns d rounded toward negative infinity to places fraction digits. Negative places and range behave as in Decimal.Round.

func (Decimal) RoundFloorSigFigs

func (d Decimal) RoundFloorSigFigs(sigFigs int32) Decimal

RoundFloorSigFigs returns d rounded toward negative infinity to sigFigs significant figures. Zero is zero. Panics on sigFigs < 1.

A floor across a power of ten may carry one figure more than asked: -99999.5 at five figures is -100000. A floor that leaves the range panics with ErrRange.

func (*Decimal) Scan

func (d *Decimal) Scan(value any) error

Scan implements sql.Scanner, accepting the driver shapes a numeric column arrives as: text raw or with one pair of surrounding quotes, an int64, or a float64. Unlike the constructors, a bad value fails the scan with an error — this is a database boundary, never a panic site.

func (Decimal) Sign

func (d Decimal) Sign() int

Sign reports -1, 0 or +1 as d is negative, zero or positive.

func (Decimal) String

func (d Decimal) String() string

String renders the canonical text form: no exponent, no trailing fraction zeros.

func (Decimal) StringFixed

func (d Decimal) StringFixed(places int32) string

StringFixed renders d rounded half away from zero to exactly places fraction digits, zero-padded. Negative places renders the rounded integer: 545 at places -1 is "550".

func (Decimal) Sub

func (d Decimal) Sub(o Decimal) Decimal

Sub returns d - o exactly. It panics with ErrRange when the difference does not fit.

func (Decimal) TextLen

func (d Decimal) TextLen() int

TextLen reports how many bytes Decimal.Append writes.

func (Decimal) Truncate

func (d Decimal) Truncate(places int32) Decimal

Truncate returns d with the fraction cut to places digits, no rounding. Negative places cuts left of the point: Truncate(-2) of 5432 is 5400.

func (*Decimal) UnmarshalBinary

func (d *Decimal) UnmarshalBinary(data []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler. Foreign bytes spelling a non-canonical value normalize on the way in. It returns ErrSyntax for empty data and ErrRange for a precision or coefficient that does not fit.

func (*Decimal) UnmarshalJSON

func (d *Decimal) UnmarshalJSON(b []byte) error

UnmarshalJSON implements encoding/json.Unmarshaler, accepting the quoted and the bare numeric form. A JSON null leaves the receiver untouched. On error the receiver is untouched too.

func (*Decimal) UnmarshalText

func (d *Decimal) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler. It accepts what NewFromString does and leaves the receiver untouched on error.

func (Decimal) Value

func (d Decimal) Value() (driver.Value, error)

Value implements driver.Valuer, rendering the canonical text.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL