mirror of
https://github.com/hanzoai/money.git
synced 2026-09-30 22:56:51 +00:00
1f1b71807f7aedf9bfbbb7bb7eaf5a44f47a73b3
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]>
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:
AddSubMulNegCmpSign, same-currency safety (zero-value agnostic) - Allocation:
Split(n)andAllocate(ratios…)— no sub-unit ever lost - Aggregation:
SumMinMaxAvg - 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 inverseMajorString(integer minor units → fixed-scale decimal string), andScaleMinor(minor units × rate → minor units, rounded half away from zero) — the set every billing API needs, none of which touches a float.RateFromFloatmakes a legacy float64 rate exact first.ScaleMinorCeil/ScaleMinorFloorare the same scaling for amounts whose direction is a policy (a fee rounded up, a commission rounded down) — exact, somath.Ceilcannot 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) +
Registercustom - 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.
Description
Exact money (Amount+Currency) for Go on big.Int decimal — fiat + 18-dec crypto, no ceiling
87 KiB
Languages
Go
100%