bigdecimal

package module
v0.0.0-...-05e9199 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-ruby-bigdecimal/bigdecimal

bigdecimal — go-ruby-bigdecimal

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's BigDecimal — arbitrary-precision decimal arithmetic and MRI-byte-exact formatting. It computes the same values MRI 4.0.5's bigdecimal library does (so 0.1 + 0.2 is exactly 0.3, not the binary-float 0.30000000000000004) and renders them in MRI's exact notation — including the fiddly 0.15e1 scientific to_s default and the to_s("F"/"E"/"+"/" "/"N") format grammar — without any Ruby runtime.

It is the decimal backend for go-embedded-ruby, but a standalone, reusable module — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (the Psych emitter/loader).

What it is — and isn't. Arbitrary-precision decimal arithmetic and the to_s notation are fully deterministic and need no interpreter, so they live here as pure Go ported from MRI 4.0.5. Binding the type into a Ruby object model — Kernel#BigDecimal, operator dispatch, coercion — is the host's job; this library hands back an idiomatic *Decimal value the host wraps.

Features

Faithful port of BigDecimal's value model and formatting, validated against the ruby binary on every supported platform:

  • Construction from a decimal string (BigDecimal("1.5"), exponent / _ / blank-tolerant), an int64 / *big.Int, or a float64 limited to n significant digits (BigDecimal(0.1, 10)), with the WithDigits(n) precision cap.
  • The three specialsNaN, +Infinity, -Infinity — and their arithmetic (∞+∞, ∞−∞ → NaN, ∞×0 → NaN, finite/0, …).
  • Arbitrary-precision add / sub / mul, and division with an explicit precision (div(o, n)) plus the bare / default-precision quotient, the floored div / % / divmod / modulo, the truncated remainder, and integer ** / power.
  • Every rounding modeROUND_UP / DOWN / HALF_UP / HALF_EVEN / HALF_DOWN / CEILING / FLOOR — at any decimal place, including MRI's quirk that ROUND_UP ignores a purely-fractional discarded tail when rounding away whole digits.
  • to_s byte-for-byte — the canonical scientific 0.<digits>e<exp> default, the plain "F" form, the "+" / space sign prefixes, and the "N" digit grouping (integer part from the right, fraction/mantissa from the left).
  • Conversions & introspectionto_i / to_f / to_r / frac / fix / floor(n) / ceil(n) / truncate(n) / round(n, mode) / abs / sign / exponent / precision / split / <=> / ==.

CGO-free, dependency-free (only math/big for the underlying integer math), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).

Install

go get github.com/go-ruby-bigdecimal/bigdecimal

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-bigdecimal/bigdecimal"
)

func main() {
	a, _ := bigdecimal.New("0.1")
	b, _ := bigdecimal.New("0.2")
	fmt.Println(a.Add(b))           // 0.3e0   (exact — not 0.30000000000000004)
	fmt.Println(a.Add(b).ToS("F"))  // 0.3

	x, _ := bigdecimal.New("1")
	y, _ := bigdecimal.New("3")
	fmt.Println(x.Div(y, 10))       // 0.3333333333e0   (10 significant digits)

	d, _ := bigdecimal.New("1234567.891")
	fmt.Println(d.ToS("5F"))        // 12 34567.891
	fmt.Println(d.String())         // 0.1234567891e7

	n, _ := bigdecimal.New("-2.5")
	fmt.Println(n)                  // -0.25e1
	r, _ := bigdecimal.New("2.5")
	fmt.Println(r.Round(0, bigdecimal.RoundHalfEven)) // 0.2e1 (banker's: 2.5 → 2)
}

API

type Decimal struct { /* sign, significand, base-ten exponent, special */ }

func New(s string, opts ...Option) (*Decimal, error) // BigDecimal("1.5") / BigDecimal(s, n)
func FromInt(i int64) *Decimal
func FromBigInt(i *big.Int) *Decimal
func FromFloat(f float64, n int) (*Decimal, error)    // BigDecimal(Float, n)
func WithDigits(n int) Option                         // significant-digit cap

func (d *Decimal) Add(o *Decimal) *Decimal
func (d *Decimal) Sub(o *Decimal) *Decimal
func (d *Decimal) Mul(o *Decimal) *Decimal
func (d *Decimal) Div(o *Decimal, prec int) *Decimal  // prec sig-digits; 0 = default precision
func (d *Decimal) DivE(o *Decimal, prec int) (*Decimal, error) // ErrZeroDivision on finite/0
func (d *Decimal) IDiv(o *Decimal) (*Decimal, error)  // floored integer quotient
func (d *Decimal) DivMod(o *Decimal) (q, r *Decimal, err error)
func (d *Decimal) Mod(o *Decimal) (*Decimal, error)   // floored modulo (sign of o)
func (d *Decimal) Remainder(o *Decimal) (*Decimal, error) // truncated (sign of d)
func (d *Decimal) Pow(n int) *Decimal                 // ** / power
func (d *Decimal) Neg() *Decimal
func (d *Decimal) Abs() *Decimal

func (d *Decimal) Round(n int, mode RoundMode) *Decimal
func (d *Decimal) Floor(n int) *Decimal
func (d *Decimal) Ceil(n int) *Decimal
func (d *Decimal) Truncate(n int) *Decimal
func (d *Decimal) Frac() *Decimal
func (d *Decimal) Fix() *Decimal

func (d *Decimal) Cmp(o *Decimal) int  // -1/0/1, or -2 for NaN (host maps to nil)
func (d *Decimal) Equal(o *Decimal) bool
func (d *Decimal) Sign() int           // 0 NaN, ±1 ±0, ±2 finite, ±3 ±Inf

func (d *Decimal) IsNaN() bool
func (d *Decimal) IsInfinite() int     // +1 / -1 / 0
func (d *Decimal) IsFinite() bool
func (d *Decimal) IsZero() bool

func (d *Decimal) String() string                    // to_s default (0.15e1)
func (d *Decimal) ToS(format string) string           // to_s("F"/"E"/"+"/" "/"N")
func (d *Decimal) Int() *big.Int                       // to_i (truncated)
func (d *Decimal) Float64() float64                    // to_f
func (d *Decimal) Rat() *big.Rat                       // to_r
func (d *Decimal) Exponent() int                       // #exponent
func (d *Decimal) Precision() int                      // #precision
func (d *Decimal) SplitParts() (sign int, digits string, base, exp int) // #split

type RoundMode int
const (
	RoundUp RoundMode = iota; RoundDown; RoundHalfUp; RoundHalfEven
	RoundHalfDown; RoundCeiling; RoundFloor
)

to_s format grammar

ToS(fmt) mirrors MRI's BigDecimal#to_s(fmt): an optional leading + / space sign prefix, an optional digit-grouping count N, and an F/f (plain) or E/e / empty (scientific) selector.

call result for 1234567.891
ToS("") 0.1234567891e7
ToS("F") 1234567.891
ToS("+") +0.1234567891e7
ToS(" ") 0.1234567891e7
ToS("5F") 12 34567.891
ToS("5E") 0.12345 67891e7
ToS("3F") 1 234 567.891

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: a wide corpus of constructions, arithmetic, precision divisions, every rounding mode, to_s formats, specials, split / exponent / sign, and <=> is computed here and against the system ruby, asserting a byte-for-byte match. The oracle scripts $stdout.binmode so Windows text-mode never pollutes the bytes, gate themselves on MRI ≥ 4.0, and skip where ruby is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-bigdecimal/bigdecimal authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package bigdecimal is a pure-Go (no cgo) reimplementation of Ruby's BigDecimal: arbitrary-precision decimal arithmetic and MRI-byte-exact formatting. It is the decimal backend for go-embedded-ruby, but a standalone, reusable module — a sibling of go-ruby-regexp, go-ruby-erb and go-ruby-yaml.

A Decimal is sign + significand (an arbitrary-precision integer) + a base-ten exponent, plus the three IEEE-style specials NaN / +Infinity / -Infinity that MRI's BigDecimal carries. The underlying integer math uses math/big; the decimal semantics — the canonical 0.15e1 scientific to_s, the rounding modes, floor/ceil/truncate at an arbitrary digit, div with an explicit precision, divmod/modulo/remainder, and the to_s("F"/"E"/"+"/" "/"N") format grammar — are this package's own, ported from MRI 4.0.5.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSyntax is returned by New for a string that is not a BigDecimal literal.
	ErrSyntax = errors.New("invalid BigDecimal value")
	// ErrZeroDivision is returned by the fallible division/modulo helpers for a
	// finite-by-zero operation (MRI raises ZeroDivisionError).
	ErrZeroDivision = errors.New("divided by 0")
)

Functions

This section is empty.

Types

type Decimal

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

Decimal is an arbitrary-precision decimal: value = sign * coef * 10**exp for a finite number, where coef is a non-negative significand. The zero Decimal is not valid; build values through New / FromInt / FromBigInt.

A finite value is kept normalised so that coef has no trailing factor of ten (coef == 0 ⇒ exp == 0), which makes the canonical scientific to_s and equality independent of how the value was written. sign is +1 or -1 (a zero keeps the sign it was parsed with, mirroring MRI's distinct +0.0 / -0.0).

func FromBigInt

func FromBigInt(i *big.Int) *Decimal

FromBigInt builds a Decimal from an arbitrary-precision integer.

func FromFloat

func FromFloat(f float64, n int) (*Decimal, error)

FromFloat builds a Decimal from a float64 limited to n significant digits, the MRI BigDecimal(Float, n) case (n must be > 0; MRI requires the precision for a Float argument). It renders the float with n significant digits and reparses, so 0.1 with n digits is the n-digit decimal nearest the binary float.

func FromInt

func FromInt(i int64) *Decimal

FromInt builds a Decimal from an int64 (the exact BigDecimal(Integer) case).

func New

func New(s string, opts ...Option) (*Decimal, error)

New parses a BigDecimal literal — the MRI BigDecimal("…") constructor for a String. It accepts an optional leading sign, a decimal mantissa with an optional fraction and an optional "e"/"E"/"d"/"D" exponent, plus the specials "NaN", "Infinity"/"+Infinity"/"-Infinity" (and MRI's tolerated leading/trailing blanks and an underscore digit separator). With WithDigits(n) the result is rounded to n significant digits. An unparseable string returns ErrSyntax.

func (*Decimal) Abs

func (d *Decimal) Abs() *Decimal

Abs returns |d| (BigDecimal#abs).

func (*Decimal) Add

func (d *Decimal) Add(o *Decimal) *Decimal

Add returns d + o (BigDecimal#+). Special-value combinations follow MRI: NaN propagates, ∞+∞ stays ∞, ∞+(−∞) is NaN.

func (*Decimal) Ceil

func (d *Decimal) Ceil(n int) *Decimal

Ceil returns the value rounded toward +Infinity to n places (BigDecimal#ceil(n)).

func (*Decimal) Cmp

func (d *Decimal) Cmp(o *Decimal) int

Cmp compares d and o, returning -1, 0 or 1 (BigDecimal#<=>). It returns -2 to signal "incomparable" when either operand is NaN (MRI's <=> yields nil there); callers map -2 to nil.

func (*Decimal) Div

func (d *Decimal) Div(o *Decimal, prec int) *Decimal

Div returns d / o rounded to prec significant digits (BigDecimal#div(o, prec) / the bare BigDecimal#/). When prec > 0 the quotient carries exactly that many significant digits, half-up; when prec <= 0 it is computed exactly if the division terminates and otherwise to an MRI-style default working precision.

Division by a finite zero, or any NaN operand, yields the matching special (the caller decides whether that is an error); see DivE for the fallible form.

func (*Decimal) DivE

func (d *Decimal) DivE(o *Decimal, prec int) (*Decimal, error)

DivE is Div with an explicit error for finite-by-zero (ErrZeroDivision), mirroring MRI raising ZeroDivisionError. Special operands still return the matching special with a nil error (∞ / 2, NaN / 1, …).

func (*Decimal) DivMod

func (d *Decimal) DivMod(o *Decimal) (q, r *Decimal, err error)

DivMod returns the floored quotient and the modulo (BigDecimal#divmod): q is floor(d/o) and r is d - q*o, so r has the sign of o.

func (*Decimal) Equal

func (d *Decimal) Equal(o *Decimal) bool

Equal reports value equality (BigDecimal#==). NaN is unequal to everything, including itself.

func (*Decimal) Exponent

func (d *Decimal) Exponent() int

Exponent returns MRI's BigDecimal#exponent: the power of ten n such that the value equals 0.<digits> * 10**n. A zero or non-finite value returns 0.

func (*Decimal) Fix

func (d *Decimal) Fix() *Decimal

Fix returns the integer part toward zero (BigDecimal#fix), as a Decimal.

func (*Decimal) Float64

func (d *Decimal) Float64() float64

Float64 returns the nearest float64 (BigDecimal#to_f). NaN/±Infinity map to the matching IEEE value.

func (*Decimal) Floor

func (d *Decimal) Floor(n int) *Decimal

Floor returns the value rounded toward -Infinity to n places (BigDecimal#floor(n)).

func (*Decimal) Frac

func (d *Decimal) Frac() *Decimal

Frac returns the fractional part (BigDecimal#frac): the value with its integer part removed, keeping the sign. A special returns itself.

func (*Decimal) IDiv

func (d *Decimal) IDiv(o *Decimal) (*Decimal, error)

IDiv returns the integer quotient floor(d / o) as a Decimal (BigDecimal#div with no precision argument): the largest integer not greater than the exact quotient. NaN/∞/zero follow MRI; finite-by-zero returns NaN with ErrZeroDivision.

func (*Decimal) Int

func (d *Decimal) Int() *big.Int

Int returns the value truncated toward zero as a *big.Int (BigDecimal#to_i). A NaN or ±Infinity returns nil (MRI raises; the caller decides).

func (*Decimal) IsFinite

func (d *Decimal) IsFinite() bool

IsFinite reports whether the value is neither NaN nor ±Infinity.

func (*Decimal) IsInfinite

func (d *Decimal) IsInfinite() int

IsInfinite reports whether the value is ±Infinity; +1 for +Inf, -1 for -Inf, 0 for a finite value or NaN (matching MRI's Float#infinite? / BigDecimal#infinite?).

func (*Decimal) IsNaN

func (d *Decimal) IsNaN() bool

IsNaN reports whether the value is NaN.

func (*Decimal) IsZero

func (d *Decimal) IsZero() bool

IsZero reports whether the value is a finite zero.

func (*Decimal) Mod

func (d *Decimal) Mod(o *Decimal) (*Decimal, error)

Mod returns the floored modulo d % o (BigDecimal#% / #modulo): the remainder with the sign of o.

func (*Decimal) Mul

func (d *Decimal) Mul(o *Decimal) *Decimal

Mul returns d * o (BigDecimal#*).

func (*Decimal) Neg

func (d *Decimal) Neg() *Decimal

Neg returns -d (unary minus). A non-NaN value flips sign (including a signed zero, mirroring MRI's distinct +0.0 / −0.0); NaN is unchanged.

func (*Decimal) Pow

func (d *Decimal) Pow(n int) *Decimal

Pow returns d ** n for an integer exponent (BigDecimal#** / #power). A negative exponent yields the reciprocal (to the default division precision); 0**0 is 1; 0 raised to a negative power returns +Infinity (as MRI does).

func (*Decimal) Precision

func (d *Decimal) Precision() int

Precision returns the number of significant decimal digits (MRI 4.0's BigDecimal#precision). A zero returns 0; a non-finite value returns 0.

func (*Decimal) Rat

func (d *Decimal) Rat() *big.Rat

Rat returns the exact value as a *big.Rat (BigDecimal#to_r). A non-finite value returns nil.

func (*Decimal) Remainder

func (d *Decimal) Remainder(o *Decimal) (*Decimal, error)

Remainder returns the truncated remainder d.remainder(o): d - o*trunc(d/o), so it has the sign of d (unlike Mod, which has the sign of o).

func (*Decimal) Round

func (d *Decimal) Round(n int, mode RoundMode) *Decimal

Round returns the value rounded to n decimal places under mode (BigDecimal#round(n, mode)). n > 0 keeps n fractional digits, n == 0 rounds to an integer, n < 0 rounds away |n| whole digits.

func (*Decimal) Sign

func (d *Decimal) Sign() int

Sign returns MRI's BigDecimal#sign code: 0 for NaN, 1/-1 for ±0, 2/-2 for a finite non-zero, 3/-3 for ±Infinity.

func (*Decimal) SplitParts

func (d *Decimal) SplitParts() (sign int, digits string, base, exp int)

SplitParts implements BigDecimal#split: it returns the sign (1/-1, or 0 for NaN), the significant-digit string, the base (always 10) and the point exponent, so the value is sign * ("0." + digits).to_f * 10**exp. For a special the digit string is "NaN" / "Infinity" and exp is 0 (matching MRI).

func (*Decimal) String

func (d *Decimal) String() string

String renders the value in MRI's default BigDecimal#to_s form: the canonical scientific notation 0.<digits>e<exp> (so BigDecimal("1.5").to_s is "0.15e1").

func (*Decimal) Sub

func (d *Decimal) Sub(o *Decimal) *Decimal

Sub returns d - o (BigDecimal#-).

func (*Decimal) ToS

func (d *Decimal) ToS(format string) string

ToS renders the value under MRI's BigDecimal#to_s(fmt) grammar. The format string carries, in order:

  • an optional leading sign flag: '+' prefixes non-negative values with '+', a space prefixes them with ' ' (a negative value always shows '-');
  • an optional run of digits N, which groups the significant digits in blocks of N (separated by a space) — the integer part from the right and the fraction from the left;
  • an optional 'F'/'f' selecting the plain floating form (e.g. "1234.5"), or 'E'/'e' / nothing selecting the scientific form 0.<digits>e<exp>.

The classic forms are ToS("") / ToS("E") (scientific), ToS("F") (plain), ToS("+") / ToS(" ") (sign prefix) and ToS("5F") (5-digit grouping).

func (*Decimal) Truncate

func (d *Decimal) Truncate(n int) *Decimal

Truncate returns the value truncated toward zero to n places (BigDecimal#truncate(n)).

type Option

type Option func(*config)

Option configures New; the variadic mirrors BigDecimal(value, ndigits)'s optional significant-digit limit.

func WithDigits

func WithDigits(n int) Option

WithDigits limits a constructed value to n significant digits, as MRI's BigDecimal(value, n) does. n == 0 means "as many digits as the input has".

type RoundMode

type RoundMode int

RoundMode selects how Round (and the precision-limited Div) breaks a tie or drops digits — the BigDecimal::ROUND_* family.

const (
	// RoundUp rounds away from zero (ROUND_UP).
	RoundUp RoundMode = iota
	// RoundDown truncates toward zero (ROUND_DOWN).
	RoundDown
	// RoundHalfUp rounds a tie away from zero (ROUND_HALF_UP) — the default.
	RoundHalfUp
	// RoundHalfEven rounds a tie to the even neighbour (ROUND_HALF_EVEN, banker's).
	RoundHalfEven
	// RoundHalfDown rounds a tie toward zero (ROUND_HALF_DOWN).
	RoundHalfDown
	// RoundCeiling rounds toward +Infinity (ROUND_CEILING).
	RoundCeiling
	// RoundFloor rounds toward -Infinity (ROUND_FLOOR).
	RoundFloor
)

Jump to

Keyboard shortcuts

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