Money and payments
Use Money for an amount of money and Currency for its currency. Both
are standard types. An amount is held as a whole number of the
currency's minor units, such as pence or cents, together with its
Currency. It is never a decimal of pounds. Each currency's exponent,
the number of digits after its decimal point, comes from ISO 4217: jpy
0, gbp 2, bhd 3.
Money is a type built into the language, as Duration is, not an
object: an amount has no fields, and money.minor and money.currency
read its two parts. Like Int, it is a representation, so a signature
names a type built on it
(Signatures use named types).
A type built on Money takes these settings:
currency:, aCurrencyvariant, fixes the currency;rounding:, aRoundingvariant, says how*and/round to a minor unit;min:andmax:are whole numbers of minor units.
An amount holds at most 2^53 - 1 minor units either side of zero,
JavaScript's largest safe integer, so the amount sent on the wire is
always a JSON number a client reads exactly. Making or working out an
amount past that is overflow, a defect, as it is for + on Int.
Currency is an enum of the active ISO 4217 codes, written
Currency.gbp, and each release ships the list. It is ISO 4217's list
less the codes with no minor unit (N.A.: the precious metals, the SDR,
the bond-market units, XSU, XUA, XTS and XXX): 164 codes, each
with its exponent. The codes ISO has withdrawn since 2021 (ANG, BGN,
CUC, HRK, SLL, ZWL) are retired variants: your code can read
them but not write them (type.variant_retired). Albania's lek is
Currency.all_, because .all lists an enum's variants; its code is
still ALL.
A Currency is spelt by its code, GBP, on the wire, in settings, in
OpenAPI and in its column, as every standard enum is, so the database
reads as the API does. text.format gives its CLDR display name, such as
"Japanese Yen".
A bakery prices its cakes in pounds, takes orders from abroad in the customer's currency, and shares a bill between friends:
type CakePrice = Money { currency: Currency.gbp // fixed: adding another currency doesn't compile min: 0 // in minor units: pence rounding: Rounding.half_up // how `*` and `/` round to a penny}type Payment = Money {} // any currency, known only while runningtype Pounds = Decimal { scale: 2}type Guests = Int { min: 1 max: 12}type AmountTooPrecise = Failure { kind: FailureKind.invalid_value}type MixedCurrencies = Failure { kind: FailureKind.invalid_value}/// A cake's price, written in pence.function list_price() -> CakePrice { return CakePrice(money.from_minor(1050, Currency.gbp)) // £10.50}/// An amount a customer typed, in major units of their currency.function entered(amount: Pounds, currency: Currency) fails AmountTooPrecise -> Payment { var entered_amount: Money = attempt money.of(amount, currency) { too_many_places: AmountTooPrecise // 10.50 in yen } return Payment(entered_amount)}/// A cake delivered: one currency, so a plain `+`.function delivered(cake: CakePrice, delivery: CakePrice) -> CakePrice { return CakePrice(cake + delivery)}/// The VAT in a price at 20%, rounded half up to a penny.function vat_in(total: CakePrice) -> CakePrice { return CakePrice(total * 0.2)}/// A price before VAT: dividing by a rate.function before_vat(gross: CakePrice) -> CakePrice { return CakePrice(gross / 1.20)}/// A bill shared between friends, every penny to someone.function bill_shares(total: CakePrice, guests: Guests) -> List<Payment> { return list.map(money.split(total, guests), share -> Payment(share))}/// Payments added up, in whatever currency they share.function received(first: Payment, second: Payment) fails MixedCurrencies -> Payment { var total: Money = attempt first + second { currency_mismatch: MixedCurrencies // a pound and a euro } return Payment(total)}/// A price for people in the reader's locale.function price_label(cake: CakePrice, customer: Locale) -> PresentationText { return text.format(cake, locale: customer)}
test "amounts add, round and share in minor units" { var delivery: CakePrice = CakePrice(money.from_minor(250, Currency.gbp)) var gross: CakePrice = CakePrice(money.from_minor(1200, Currency.gbp)) var bill: CakePrice = CakePrice(money.from_minor(1000, Currency.gbp)) assert list_price() == money.from_minor(1050, Currency.gbp) assert delivered(list_price(), delivery) == money.from_minor(1300, Currency.gbp) assert vat_in(list_price()) == money.from_minor(210, Currency.gbp) assert before_vat(gross) == money.from_minor(1000, Currency.gbp) var pence: List<Int> = list.map(bill_shares(bill, 3), share -> money.minor(share)) assert pence == [334, 333, 333]}test "a currency known only while running is checked as it runs" { var in_yen: Payment = attempt entered(500.00, Currency.jpy) assert in_yen == money.from_minor(500, Currency.jpy) assert entered(10.50, Currency.jpy) fails AmountTooPrecise var euros: Payment = Payment(money.from_minor(500, Currency.eur)) var pounds: Payment = Payment(money.from_minor(250, Currency.gbp)) var both: Payment = attempt received(euros, euros) assert both == money.from_minor(1000, Currency.eur) assert received(euros, pounds) fails MixedCurrencies}test "an amount is shown, written and read one way" { var large: CakePrice = CakePrice(money.from_minor(123450, Currency.gbp)) var abroad: Payment = Payment(money.from_minor(123450, Currency.eur)) assert price_label(large, Locale.en_gb) == "£1,234.50" assert text.format(abroad, locale: Locale.en_us) == "€1,234.50" assert text.format(list_price(), locale: Locale.en_gb, precision: MoneyPrecision.whole) == "£11" assert text.from(list_price()) == "GBP 10.50" assert text.format(Currency.jpy, locale: Locale.en_gb) == "Japanese Yen"}
Inside if (money.currency(a) == money.currency(b)), the checker knows
the two currencies match, so + needs no attempt. Comparing a
currency with one written out, as in money.currency(a) == Currency.gbp, proves that currency, so the amount builds a type that
fixes it with no arm for currency:
type Payment = Money {}type SterlingPayment = Money { currency: Currency.gbp}/// Two payments added when they share a currency, the first otherwise.function sum_if_shared(a: Payment, b: Payment) -> Payment { if (money.currency(a) == money.currency(b)) { return Payment(a + b) } return a}/// A payment in pounds, when it is one.function sterling(paid: Payment) -> SterlingPayment? { if (money.currency(paid) == Currency.gbp) { return SterlingPayment(paid) } return none}
Markdown for agents: /docs/money-and-payments.md.Built from Jadpo main at a707efb.