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.splitUnits(total, weights) #

stable0.18.43

Split an integer into parts proportional to weights, distributing every unit. Largest-remainder method: floor each share, then hand the leftover units to the shares with the largest fractional remainder, ties broken by index. The total is preserved by construction and the result is identical across runtimes — no floating point is involved at any step.

This is the algorithm behind Money.allocate, reachable without a Money. Money requires a currency from b.money.CURRENCIES, which carries the codes the framework has minor-unit data for rather than all of ISO 4217, so a caller holding a currency outside that set could not reach the split at all. Nor could a caller splitting something that is not money — seats, quota, shard weights, a rate limit across workers — which is the same arithmetic with the same requirement that nothing be lost to rounding.

total may be negative: shares floor toward negative infinity so the leftover pass stays positive and the parts still sum to total.

b.money.splitUnits(100n, [1, 1, 1]);
// → [34n, 33n, 33n]

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. That refusal is 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 normalize 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") and 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-10-12T14:21:56.879Z by seeder.