zeekayandHanzo Dev dd33800850 feat: ParseMinor/ParseCents — the one exact decimal-string → minor-units conversion
Upstream billing APIs (DigitalOcean, AWS, GCP) report money as a decimal
STRING and every ledger stores an integer, so this conversion sits on the
edge of the money path. It had been rewritten by hand three times — vm,
commerce and ai each carried a private copy — and the copies disagreed
about real inputs: "1,234.56" was 123456 in one and 0 in the other two;
"-5.00" was 0, 0 and -500. Three answers means at least two are wrong,
and nothing in the code said which.

All three shared one root cause: ParseFloat, then multiply by 100. That
loses a cent on ordinary prices (19.99 has no exact binary form, so
f*100 is 1998.999… and truncation gives 1998) and the error is unbounded
above 2^53 minor units. This is the module that already holds exact
decimals and the FromMinor/FromCents constructors; the inverse belongs
here beside them, not three times in three callers.

Two policies the copies got wrong are fixed rather than reproduced:

  - an unreadable amount returns an ERROR, never 0. Zero is a legal
    amount, so a caller that cannot tell "spent nothing" from "could not
    read what was spent" reports a confident $0 and the spend vanishes.
  - a negative parses and STAYS negative. On these APIs it is a credit
    or a refund; clamping it to zero overstates spend by the refund.

int64 overflow is refused too. Wrapping is worse than refusing: it
reports a balance of the wrong sign.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 22:05:56 -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
  • Formatting: Display (symbol + thousands separators), AsMajorUnits
  • 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%