Money
Decimal-safe money arithmetic. The framework primitive every billing / invoicing / shop consumer reaches for so the IEEE 754 double-precision 0.1 + 0.2 !== 0.3 rounding error never reaches an invoice line, a tax cell, or a ledger row.
## Why not Number
JavaScript's Number is a binary64 (IEEE 754 double). 0.10 and 0.20 are unrepresentable in binary fraction; the closest binary approximations sum to 0.30000000000000004. Add 10,000 such approximations into a daily revenue total and the cumulative drift is large enough to fail a SOX 404 reconciliation. The framework's defense is to refuse Number at the boundary: Money values carry BigInt minor units (cents / pence / sen / yen / fils) and a currency tag pulled from the ISO 4217 catalog. Every arithmetic operation is integer BigInt math; rounding (where it must happen -- FX conversion, weighted allocation) is bankers' (half-to-even) by default and explicit when not.
## What ships
- b.money.of(amount, currency) -- accepts BigInt minor units OR a decimal-shaped string ("12.50"). Numbers refused at the boundary. - b.money.fromMinorUnits(bigint, currency) -- direct construction. - b.money.parse("12.50 USD") -- bidirectional shape parser ( AND ). - b.money.zero(currency) -- convenience zero. - b.money.convert(money, toCurrency, fxRateProvider, opts?) -- conversion through an operator-injected rate provider. Framework NEVER bakes rates in. - b.money.CURRENCIES -- frozen ISO 4217 catalog (code -> exponent). - b.money.MoneyError -- typed refusal class.
## Allocation
m.allocate([w1, w2, ...]) uses the largest-remainder method: floor each weighted share, then distribute the remainder unit-by- unit to shares with the largest fractional remainder. $10.00 / [1, 1, 1] returns [$3.34, $3.33, $3.33] (sum exact). $100.00 / [60, 40] returns [$60.00, $40.00]. Deterministic; total preserved by construction.
## Rounding
FX conversion rounds half-to-even (bankers'). Opt into half-up at the call site when an operator regime demands it.
## RFC / standards
- ISO 4217 -- currency code + minor-unit catalog. - BCP 47 -- locale tags consumed by format() via Intl.NumberFormat. - IEEE 754 binary64 -- the binary fraction representation we refuse at the API boundary. Documented to make the refusal visible to auditors.
b.money.Money(minorUnits, currency) #
The immutable Money value class. Operators rarely construct directly -- reach for b.money.of (string or BigInt) or b.money.fromMinorUnits (BigInt) instead. The class is exported so instance instanceof b.money.Money is a stable type check when receiving Money values across module boundaries.
Instance methods: add, subtract, multiply, allocate, negate, abs, equals, lessThan, greaterThan, lessThanOrEqual, greaterThanOrEqual, isZero, isNegative, isPositive, toMinorUnits, toString, toJSON, format.
var m = new b.money.Money(1250n, "USD");
m instanceof b.money.Money;
b.money.Money.roundToIncrement(step, opts?) #
{
mode: "half-even" | "half-up" | "half-down" | "ceiling" | "floor", // default: half-even
}
Snap this amount to the nearest multiple of step minor units, returning a new Money in the same currency — the cash-rounding step a coarser denomination needs (CHF to the nearest 0.05, SEK to the nearest 0.10, a 100-unit price step) without leaving the type. Integer-only; immutable; negative amounts (refund previews) round on the correct side of the tie. See b.money.roundMinor for the raw-integer form and the mode semantics.
b.money.of("12.32", "CHF").roundToIncrement(5, { mode: "half-up" }); // → CHF 12.30
b.money.of("19.97", "SEK").roundToIncrement(10); // → SEK 20.00
b.money.roundMinor(minor, step, mode?) #
{
mode: "half-even" | "half-up" | "half-down" | "ceiling" | "floor", // default: half-even
}
Snap a raw minor-unit integer to the nearest multiple of step minor units — the cash-rounding step a coarser cash denomination needs even though the currency's ISO 4217 minor unit is finer (a CHF total to the nearest 5 rappen after the 1/2-rappen coins were retired, a SEK total to the nearest 10 öre, a JPY total to a 100-unit psychological-pricing step). Pure BigInt math — no Number in the value path — and the remainder sign is handled so a negative amount (a refund preview) rounds on the correct side of the tie.
minor accepts a BigInt or a safe integer Number; the return is a BigInt.
b.money.roundMinor(1232n, 5n); // CHF 12.32 → nearest 0.05 → 1230n (12.30)
b.money.roundMinor(25n, 10n, "half-even"); // tie → even multiple → 20n
b.money.roundMinor(25n, 10n, "half-up"); // tie away from zero → 30n
b.money.roundMinor(-25n, 10n, "half-up"); // refund tie away from zero → -30n
b.money.of(amount, currency) #
Build a Money from amount (BigInt minor units OR decimal-shaped string) and an ISO 4217 currency code. Throws MoneyError on bad shape. Numbers are refused at the boundary -- the framework's defense against IEEE 754 binary-fraction drift.
var price = b.money.of("12.50", "USD");
var fee = b.money.of(250n, "USD");
var tip = b.money.of("0", "JPY");
b.money.fromMinorUnits(minorUnits, currency) #
Build a Money directly from a BigInt minor-unit count. The lowest-overhead constructor; useful when restoring from a ledger row or a wire-shape toJSON payload.
var due = b.money.fromMinorUnits(1250n, "USD");
b.money.parse(input) #
Parse a string of the form OR into a Money. The two shapes round-trip with toString() (which emits the amount-first canonical form). Whitespace between amount and code is required; locale-formatted strings (thousands separator, $ glyph) are refused -- operators normalise at the call site.
b.money.parse("12.50 USD");
b.money.parse("USD 12.50");
b.money.parse("12 JPY");
b.money.parse("12.500 KWD");
b.money.zero(currency) #
Return a zero-valued Money in the requested currency. Convenience for fold/sum accumulators.
var total = items.reduce(function (acc, it) { return acc.add(it.price); },
b.money.zero("USD"));
b.money.convert(money, toCurrency, fxRateProvider, opts?) #
{
rounding: "half-even" | "half-up", // default "half-even" (bankers')
}
Convert money to toCurrency through an operator-injected rate provider. The framework NEVER bakes in rates -- operators wire a provider that pulls from an external FX feed (ECB / OANDA / their internal treasury system) and refresh on whatever cadence their regime requires.
The fxRateProvider.rate(from, to) contract returns a decimal- shaped string ("1.085") -- never a Number. Conversion math runs in BigInt with the provider rate's denominator; rounding is half-to-even by default (operator opts into half-up via opts.rounding).
var rates = { rate: function (from, to) { return "0.92"; } };
var eur = b.money.convert(b.money.of("100.00", "USD"), "EUR", rates);
Last updated 2026-08-08T16:39:49.652Z by seeder.