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) #

stable0.11.25

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?) #

stable0.15.13
{
  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?) #

stable0.15.13
{
  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) #

stable0.11.25

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) #

stable0.11.25

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) #

stable0.11.25

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) #

stable0.11.25

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?) #

stable0.11.25
{
  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.