Skip to main content

core.money

Money type with currency-correct arithmetic. Two cogs:

  • currencyCurrency { code, minor_units } plus 25 predefined ISO 4217 constructors.
  • moneyMoney { amount: BigDecimal, currency: Currency } with + - * / split, error type for cross-currency operations.

Built on the V0 numeric quartet (core.text.numeric.{decimal, bigint, bigdecimal, rational}).

Currency

public type Currency is {
code: Text, // ISO 4217 ("USD", "EUR", "JPY")
minor_units: Int, // decimal places (USD=2, JPY=0, BHD=3)
};

Predefined currencies

GroupConstructorsMinor units
Zero-decimaljpy, krw, vnd, clp, isk0
Two-decimalusd, eur, gbp, cad, aud, nzd, chf, cny, inr, brl, mxn, zar, sek, nok, dkk2
Three-decimalbhd, kwd, omr, jod, tnd3
mount core.money.{usd, jpy, bhd};

let dollar = usd();
assert(dollar.minor_units == 2);
assert(jpy().minor_units == 0);

Money

public type Money is {
amount: BigDecimal,
currency: Currency,
};

public type MoneyError is
| CurrencyMismatch(Text, Text) // (lhs.code, rhs.code)
| DivisionByZero
| InvalidSplit;

Arithmetic

+, -, comparison: same-currency only. Cross-currency operations return Err(CurrencyMismatch).

* by scalar (BigDecimal): always succeeds, currency preserved.

/ by scalar: returns Err(DivisionByZero) for zero divisor.

split(money, n): distribute money into n parts so the sum is exactly money (last part absorbs the rounding remainder). Pre-empts the classical "split 1threeways1 three ways → 0.33 × 3 = $0.99" cent loss.

let bill = Money { amount: BigDecimal.from_int(100), currency: usd() };
let shares = split(bill, 3); // [33.34, 33.33, 33.33] USD

Cross-currency rejection at type level

add(a, b) returns Result<Money, MoneyError>. The type system does not silently coerce — currency is part of the value's identity.

let usd_total = add(money_in_usd, money_in_eur);
// usd_total : Result<Money, MoneyError>
// → Err(CurrencyMismatch("USD", "EUR"))

To convert across currencies, build your own conversion layer with an explicit rate source. V0 deliberately omits a rate API — rate sources differ per use case (live FX, historical, bilateral OTC).

V0 boundary — what's in scope, what's not

In scope:

  • Currency-correct add / subtract / scalar-multiply / scalar-divide / fair-split.
  • Cross-currency rejection at type level.
  • 25 ISO 4217 currencies covering the major-decimal-class triple (0, 2, 3 minor units).
  • Arbitrary-precision arithmetic via BigDecimal.

Out of scope (V1 follow-ups):

  • Rate-source abstraction + cross-currency conversion. Depends on a rate-source provider trait the corpus has not yet stabilised.
  • Locale-aware formatting. Current to_text emits the ISO code; thousand-separator / decimal-separator / negative-bracket conventions are caller's responsibility.
  • Runtime ISO 4217 lookup. Currencies are const-only via the predefined constructors; programmatic registration of an arbitrary code is V1.

Cross-references