decimal

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: May 7, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package decimal provides arbitrary-precision fixed-point decimal arithmetic.

A value is stored as the pair (i, scale) representing the rational number i / 10^scale. All operations route through a Context that pins the output Scale and rounding Mode, so results stay free of the cumulative drift that plagues binary floating point.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidDecimal is returned by ParseExact and Parse when the input
	// is not a well-formed decimal literal (empty, illegal characters, or
	// more than one decimal point).
	ErrInvalidDecimal = errors.New("invalid decimal")

	// ErrDivisionByZero is returned by Div, and by Pow when raising 0 to a
	// negative integer power (which would require dividing by zero).
	ErrDivisionByZero = errors.New("division by zero")

	// ErrNegativeSqrt is returned by Sqrt when the input is negative.
	ErrNegativeSqrt = errors.New("square root of negative number")

	// ErrNonPositiveLog is returned by Log when the input is zero or
	// negative — neither is in the domain of the real-valued logarithm.
	ErrNonPositiveLog = errors.New("logarithm of non-positive number")

	// ErrInvalidPow is returned by Pow when the base is negative and the
	// exponent is not an integer (the result would be complex-valued).
	ErrInvalidPow = errors.New("invalid power: negative base with non-integer exponent")

	// ErrRoundingNecessary is the panic value raised when an operation is
	// configured with RoundingModeUnnecessary but a non-zero residue would
	// have to be discarded. Recover with:
	//
	//	defer func() {
	//	    if r := recover(); r != nil {
	//	        if err, ok := r.(error); ok && errors.Is(err, decimal.ErrRoundingNecessary) {
	//	            // handle
	//	        }
	//	    }
	//	}()
	ErrRoundingNecessary = errors.New("rounding necessary but mode is Unnecessary")
)

Functions

func Cmp added in v0.1.1

func Cmp(a, b Decimal) int

Cmp compares a and b and returns -1, 0, or 1 for a < b, a == b, or a > b respectively.

The values are aligned to a common scale first, so 1.5, 1.50, and 1.500 all compare as equal. Cmp does not depend on a Context and never mutates its inputs.

Types

type Context

type Context struct {
	// Scale is the number of fractional digits in normalized results.
	Scale int32
	// Mode selects the rounding strategy; see RoundingMode. The zero value
	// is RoundingModeHalfUp.
	Mode RoundingMode
}

Context bundles the target precision and rounding policy used by every Context-aware operation in this package.

Scale is the number of fractional digits in the output (so 10^-Scale is the smallest representable unit). Mode chooses how excess digits are dropped when an operation would otherwise produce more.

The zero value Context{} is integer-precision (Scale = 0) HalfUp rounding, i.e. ordinary "round half away from zero" to whole numbers.

Context is plain data: copy it freely, share it across goroutines, and reuse it in concurrent calls.

func (Context) Normalize

func (c Context) Normalize(d Decimal) Decimal

Normalize re-scales d to c.Scale: it pads with zeros when the input has fewer digits, and rounds according to c.Mode when it has more.

When d already matches c.Scale the original value is returned without allocating a new big.Int.

type Decimal

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

Decimal is a fixed-point decimal number whose value is i / 10^scale.

scale is the number of fractional digits. With scale = 2, an i of 1234 represents 12.34. Both fields are unexported; build a Decimal through NewFromScaledInt, NewFromInt64, Parse, or its variants.

Decimal is a value type. Copying it is cheap and produces a fully independent value — there is no shared mutable state to worry about.

func Add

func Add(ctx Context, a, b Decimal) Decimal

Add returns a + b normalized to ctx.

The two operands are aligned to the larger of their scales before being summed, so addition itself is exact; the only place rounding can sneak in is the final clamp to ctx.Scale (using ctx.Mode) when the aligned scale exceeds it.

func Div

func Div(ctx Context, a, b Decimal) (Decimal, error)

Div returns a / b at ctx.Scale digits of precision, rounded with ctx.Mode.

Division by zero yields ErrDivisionByZero. Because division is an inherently approximate finite-precision operation, the result has a last-digit error of at most 0.5 ULP under HalfUp and at most 1 ULP under Up or Down.

func Exp added in v0.1.1

func Exp(ctx Context, x Decimal) Decimal

Exp returns e^x at ctx.Scale digits, rounded with ctx.Mode.

Defined on the entire real line. Exp(0) short-circuits to 1, and negative inputs are handled as 1 / Exp(|x|).

Algorithm:

  1. Take |x| and apply argument reduction by repeatedly halving until |x| < 0.5 (k iterations). Halving is done by "multiply by 5, scale + 1", which is exact.
  2. Apply the Taylor series 1 + x + x²/2! + … to the reduced argument, stopping once the next term rounds to zero at the working scale.
  3. Square the accumulator k times to recover e^x for the original argument.
  4. If x was negative, take the reciprocal of the final result.

Beware that for very large |x| (say 1000) the result is itself a huge number; both runtime and output size grow with |x|, so the caller should keep that in mind.

func Log added in v0.1.1

func Log(ctx Context, x Decimal) (Decimal, error)

Log returns ln(x) (the natural logarithm) at ctx.Scale digits, rounded with ctx.Mode.

Inputs ≤ 0 yield ErrNonPositiveLog. Log(1) is 0.

Algorithm:

  1. Repeatedly take sqrt(x) until x falls into [0.9, 1.1] (count iterations). This shrinks the convergence radius of the series in step 2 to something tiny.
  2. With y close to 1, compute z = (y - 1) / (y + 1) and apply the atanh series atanh(z) = z + z³/3 + z⁵/5 + ….
  3. ln(y) = 2·atanh(z); then ln(x) = 2^count · ln(y).

Even for |log10(x)| ≈ 100 the sqrt loop only needs ~20 passes, so the total cost scales roughly linearly with ctx.Scale.

func Mul

func Mul(ctx Context, a, b Decimal) Decimal

Mul returns a * b normalized to ctx.

The intermediate result is held at a.scale + b.scale and is exact; rounding (per ctx.Mode) only happens when collapsing the product down to ctx.Scale.

func MustParse

func MustParse(ctx Context, s string) Decimal

MustParse is like Parse but panics on parse failure. Convenient for initializing package-level constants from string literals.

func Neg

func Neg(d Decimal) Decimal

Neg returns -d. The scale is preserved and the result does not depend on any Context.

func NewFromInt64

func NewFromInt64(ctx Context, n int64) Decimal

NewFromInt64 wraps an int64 as a Decimal and normalizes it to ctx.Scale.

For example, with ctx.Scale = 2, NewFromInt64(ctx, 5) returns 5.00 (i = 500, scale = 2).

func NewFromScaledInt

func NewFromScaledInt(i *big.Int, scale int32) Decimal

NewFromScaledInt builds a Decimal from a pre-scaled integer i and the given scale, so the resulting value is i / 10^scale.

A nil i is treated as 0. The returned Decimal does not share storage with the supplied *big.Int, so the caller may keep mutating its own copy without affecting the result.

This constructor performs no rounding or normalization; if you need the value to obey a Context's Scale, call Context.Normalize on it.

func Parse

func Parse(ctx Context, s string) (Decimal, error)

Parse is ParseExact followed by normalization to ctx.Scale using ctx.Mode.

Inputs with more fractional digits than ctx.Scale are rounded according to the rounding mode.

func ParseExact

func ParseExact(s string) (Decimal, error)

ParseExact parses a decimal string and preserves the exact scale found in the input — no rounding is performed.

The input accepts an optional leading sign and digits with or without a decimal point. An empty string, illegal characters, or more than one decimal point all yield ErrInvalidDecimal. Examples:

"123.45" → 12345 / 100
"-0.10"  → -10  / 100  (trailing zero is preserved)
".5"     → 5    / 10
"1."     → 1    / 1

Scientific notation (e.g. "1e5") is not supported. Use Parse instead when you want the result clamped to a Context's Scale.

func Pow added in v0.1.1

func Pow(ctx Context, base, exp Decimal) (Decimal, error)

Pow returns base^exp at ctx.Scale digits, rounded with ctx.Mode.

Behavior splits on whether exp is an integer:

  • Integer exp (including negative and zero): handled by binary exponentiation (square-and-multiply) using only multiplication and division, so there is no Log/Exp approximation in the loop. base may be negative, e.g. Pow(-2, 3) = -8 and Pow(-2, 4) = 16. Pow(0, exp) for exp < 0 returns ErrDivisionByZero; Pow(0, 0) returns 1 by convention.

  • Non-integer exp: computed as exp · ln(base) followed by Exp, so base must be > 0. A non-positive base with a fractional exponent returns ErrInvalidPow (the result would be complex-valued).

"Integer" here means the scaled integer leaves no remainder modulo 10^scale: 2.00 is integer 2, but 2.001 is not.

func Sqrt added in v0.1.1

func Sqrt(ctx Context, x Decimal) (Decimal, error)

Sqrt returns √x at ctx.Scale digits, rounded with ctx.Mode.

A negative x yields ErrNegativeSqrt; Sqrt(0) is 0.

Internally x is scaled up to an integer N = x · 10^(2·ws) where ws = ctx.Scale + 12 guard digits, and big.Int.Sqrt produces the floor of √N. The result is then normalized back to ctx.Scale, so the final-digit error stays under 0.5 ULP in HalfUp mode.

func Sub

func Sub(ctx Context, a, b Decimal) Decimal

Sub returns a - b normalized to ctx. Same semantics as Add but in the other direction.

func (Decimal) Scale

func (d Decimal) Scale() int32

Scale returns the number of fractional digits stored on the Decimal.

12.345 (i = 12345, scale = 3) returns 3. The scale is not collapsed when trailing digits are zero, so 12.30 (i = 1230, scale = 2) still returns 2.

func (Decimal) String

func (d Decimal) String() string

String renders the value in conventional decimal notation.

Trailing zeros implied by scale are preserved exactly: Decimal{i:1230, scale:2} prints as "12.30". When scale is 0 the output has no decimal point. Negative values are prefixed with "-" before the absolute magnitude. The output never contains thousands separators or scientific notation.

type RoundingMode

type RoundingMode uint8

RoundingMode selects the rounding strategy used by Context.Normalize and every operation that produces results at a target scale (Add, Sub, Mul, Div, Sqrt, Exp, Log, Pow).

const (
	// RoundingModeHalfUp is the everyday "round half away from zero" rule.
	// Exact halves move away from zero: 1.235 → 1.24, -1.235 → -1.24.
	// Matches Java BigDecimal.ROUND_HALF_UP and Python decimal.ROUND_HALF_UP.
	// This is the zero value of RoundingMode.
	RoundingModeHalfUp RoundingMode = iota

	// RoundingModeHalfEven is banker's rounding: the IEEE 754 default and
	// Python decimal's default. Behaves like HalfUp except that exact halves
	// pick the neighbour whose last digit is even, eliminating the systematic
	// upward bias of HalfUp on long sums. 1.225 → 1.22, 1.235 → 1.24,
	// -1.225 → -1.22.
	RoundingModeHalfEven

	// RoundingModeDown truncates toward zero (the absolute value never grows).
	// 1.235 and -1.235 both become 1.23 / -1.23 at Scale = 2.
	RoundingModeDown

	// RoundingModeUp rounds away from zero whenever any non-zero residue
	// remains. 1.231 and -1.231 both become 1.24 / -1.24 at Scale = 2.
	RoundingModeUp

	// RoundingModeCeiling rounds toward +∞ whenever any non-zero residue
	// remains. Positive values round away from zero; negative values round
	// toward zero. 1.231 → 1.24, -1.231 → -1.23 at Scale = 2.
	RoundingModeCeiling

	// RoundingModeFloor rounds toward -∞ whenever any non-zero residue
	// remains. Positive values round toward zero; negative values round
	// away from zero. 1.231 → 1.23, -1.231 → -1.24 at Scale = 2.
	RoundingModeFloor

	// RoundingModeHalfDown is "round half toward zero": behaves like HalfUp
	// except that exact halves stay put rather than stepping away. 1.235 →
	// 1.23, 1.236 → 1.24, -1.235 → -1.23. Matches Java BigDecimal.ROUND_HALF_DOWN
	// and Python decimal.ROUND_HALF_DOWN.
	RoundingModeHalfDown

	// RoundingMode05Up implements Python's decimal.ROUND_05UP rule: after
	// truncating toward zero, if the kept last digit is 0 or 5 then any
	// non-zero residue causes a step away from zero; otherwise the residue
	// is dropped. Used in some accounting contexts to avoid producing 5s
	// as final digits unless they are exact.
	RoundingMode05Up

	// RoundingModeUnnecessary asserts that no rounding will be required.
	// If an operation under this mode has to discard a non-zero residue,
	// it panics with ErrRoundingNecessary. Equivalent to Java's
	// RoundingMode.UNNECESSARY (which throws ArithmeticException).
	RoundingModeUnnecessary
)

Constants are ordered by expected usage frequency, so the zero value of Context.Mode is RoundingModeHalfUp — the rule most users mean when they say "round to N decimal places".

Jump to

Keyboard shortcuts

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