zeekayandHanzo Dev 1f1b71807f add ScaleMinorCeil/ScaleMinorFloor: round the amount, not the float
ScaleMinor rounds half away from zero, which is right for an amount that
merely has to be correct and wrong for one somebody decided always falls
one way: a platform fee the house rounds up, an affiliate commission the
house rounds down and keeps the remainder of. Those callers reach for
math.Ceil(float64(units)*rate) instead, and that does not round the
product up — it rounds the FLOAT up, which is a different number.

A float rate is not the rate. 0.07 is held a hair above 0.07, so 700
minor units at 7% lands at 49.000000000000007 and ceilings to 50: a cent
invented on an amount that was exactly 49, charged to a customer who owed
49. 0.29 is held a hair below, so 100 at 29% lands at 28.999999999999996
and floors to 28: a cent destroyed on an amount that was exactly 29, out
of an affiliate's commission. Truncation at least errs consistently and
always toward the house; this errs on whichever side the binary
representation fell, on products that had no fraction to round at all.

Both form the product in big.Int and let only the true fraction decide,
so an exact product comes back unchanged from either — rounding a whole
number is a no-op whichever way you were headed. They name the
infinities, not the magnitudes, because a refund is negative and
"toward +inf" and "away from zero" are not the same instruction there.

Overflow is reported rather than wrapped, the contract ScaleMinor keeps.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 21:24:14 -07:00

money

The one exact money type for Go — an Amount in a Currency, backed by hanzoai/decimal (big.Int fixed-point).

Martin Fowler's Money pattern (never floats, integer-exact, Add/Sub/Split/Allocate, same-currency safety) without the precision ceiling. int64/uint64 money libraries cap around 19 digits (~$9.20 at 18 decimals); this holds a fiat cent and an 18-decimal on-chain balance exactly.

price, _ := money.Parse("6.60", money.USD)
cost := price.Mul(decimal.New(200, 0)).Mul(decimal.MustParse("0.000001")) // "0.00132", never 0
bal, _ := money.FromCents(100).Sub(cost)                                  // "0.99868"

money.FromCents(123456).Display()             // "$1,234.56"
parts, _ := money.FromCents(10000).Allocate(50, 30, 20) // $50 / $30 / $20, nothing lost
a, _ := money.ParseAmount("$1,234.56")        // symbol/code/separator aware
money.FromHUSD(wei)                            // 18-dec on-chain stablecoin

Features

  • Exact, uncapped: big.Int core — fiat cents to 18-decimal crypto wei, no float, no ceiling
  • Arithmetic: Add Sub Mul Neg Cmp Sign, same-currency safety (zero-value agnostic)
  • Allocation: Split(n) and Allocate(ratios…) — no sub-unit ever lost
  • Aggregation: Sum Min Max Avg
  • Exchange: rate table + Convert / ConvertWithRate
  • Parsing: ParseAmount ($1,234.56, EUR 250,75, £99.99), European decimal-comma, ParseDecimal
  • Ledger edge: ParseMinor/ParseCents (decimal string → integer minor units), its exact inverse MajorString (integer minor units → fixed-scale decimal string), and ScaleMinor (minor units × rate → minor units, rounded half away from zero) — the set every billing API needs, none of which touches a float. RateFromFloat makes a legacy float64 rate exact first. ScaleMinorCeil/ScaleMinorFloor are the same scaling for amounts whose direction is a policy (a fee rounded up, a commission rounded down) — exact, so math.Ceil cannot invent a cent on a product that was already whole
  • Formatting: MajorString ("10.99", "1.00", "-19.99" — the wire), Display (symbol + thousands separators — the human), AsMajorUnits (float, display only)
  • Currencies: ISO-4217 set (symbol, numeric code) + crypto (USDC/USDT/BTC/ETH/HUSD) + Register custom
  • Integration: json.Marshaler/Unmarshaler, sql.Scanner/driver.Valuer

Packages

Single package github.com/hanzoai/money (Amount, Currency, Exchange, parse/format/aggregate), on github.com/hanzoai/decimal.

Original work (big.Int core written from scratch, not a fork). BSD-3-Clause.

S
Description
Exact money (Amount+Currency) for Go on big.Int decimal — fiat + 18-dec crypto, no ceiling
Readme
87 KiB
Languages
Go 100%