Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

Money operations

On this page

The money operations make, take apart, add up, share out and convert amounts of Money. Adding, subtracting and comparing amounts use the ordinary operators; the operations below do the rest. The "Fails" column names the failure a call can give, which you map in an attempt (each mapping line is an arm, Attempt).

Operation Gives Fails SQL form
money.from_minor(amount, currency) Money of amount minor units none
money.of(amount, currency) Money from a Decimal in major units too_many_places (more places than the currency has) none
money.minor(value) Int, the minor units the amount column
money.currency(value) Currency the currency column
money.sum(items, currency:) Money; currency: gives the zero for an empty list currency_mismatch none
money.allocate(total, ratios) List<Money> in proportion to ratios (whole numbers), adding up to total exactly: leftover minor units go one each to the first shares none
money.split(total, parts) List<Money>, parts near-equal shares adding up to total; parts at least 1 none
money.mask(card) MaskedCardNumber of a PaymentCardNumber: its brand by its issuer range (none for a range no listed scheme issues) and its last four none
money.convert(value, rate) Money in the rate's other currency, rounded half to even to its minor unit; rate an ExchangeRate the app was given currency_mismatch (the rate is for another currency) none
money.apply_discount(price, discount) Price, discount taken off its amount and includes_tax kept: an amount off, or that percentage of the amount rounded half to even to a minor unit; more off than the price leaves nothing currency_mismatch (an amount off in another currency) none

Money arithmetic

+, - and comparisons work within one currency. Mixing two currencies is a compile error where both types fix their currency. Where the currencies are known only while the program runs, it is the failure currency_mismatch, which you handle in an attempt.

You can multiply money by a number or a Percentage, and divide it by a Decimal. The result is rounded by the type's rounding:, which is half_even unless the type declares another. Dividing money by an Int is refused: use money.split instead. The full rules are in The standard operator table.

Sharing money out

money.split and money.allocate never round: every minor unit goes to some share, so the shares always add up to the total.

  • money.split's parts is never 0 and never negative. The compiler proves it, as it proves a divisor or a count.
  • money.allocate's ratios are a list of whole numbers. The list is never empty (type.argument_empty) and no ratio is negative (type.argument_negative). Ratios that are all 0 are a defect, as dividing by zero would be.
  • Leftover minor units go one each to the first shares whose ratio isn't 0, so a share whose ratio is 0 always gets nothing.
  • A negative total, such as a refund, shares out as a payment does, and every share is negative.

Calls that need no arm

In some cases a call can't fail as its "Fails" column says, and its attempt needs no arm for that failure:

  • money.sum checks every item's currency against currency:, which is also the total of an empty list. When the items' type fixes that currency, the call needs no arm.
  • money.of's too_many_places needs no arm when the currency is written out and the amount is an Int, a literal with no more decimal places than the currency has, or a type whose scale is no more than the currency's exponent (its number of decimal places).

Converting between currencies

money.convert applies an ExchangeRate your app already holds, so a rate applied the wrong way round is a type error, not a silent bug. Jadpo doesn't fetch rates: a rate is outside data with a time and a source, which a provider package fetches (Exchange rates).

The result is rounded half to even to the target currency's minor unit, whatever the amount type's rounding: says: £10.50 at 1.17 is €12.285, so €12.28.

Money as text

Money's canonical text is the currency code, a space, and the amount in major units with exactly the currency's number of decimal places: GBP 10.50, JPY 500, BHD 1.250. text.parse reads the code in any case. It refuses any other number of places, or an amount past Money's limit, as malformed.

Money has no literal, so Display, the form test reports and failed assertions use, writes an amount as the code that makes one: money.from_minor(1050, Currency.gbp).

Storing money

A money field is stored in two columns: a bigint of minor units named <column>_amount, and a char(3) named <column>_currency holding the ISO 4217 code. It uses both even when the type fixes the currency, so opening the type to other currencies later needs no migration, and a query can group by currency.

A type that fixes the currency also adds a check constraint, <table>_<column>_currency_check, holding the currency column to that code. A migration adds or drops it as the type changes.

Money in JSON

Money is sent in one fixed shape, the amount in integer minor units as Stripe sends it: {"amount": 1050, "currency": "GBP"}. json.read and json.write use the same shape.

  • The amount is always a JSON number. It is never sent as a string, which is how Values and types sends a large Int.
  • JSON past Money's limit is refused as max.
  • The currency is read in any case and written in capitals.
  • OpenAPI describes it by the JSON it sends, as it does every standard type: an object of amount, with the type's bounds, and currency, an enum of the one code when the type fixes it.

A provider that uses another shape, such as "10.50", is mapped in its package's wire:.

Showing money to people

text.format shows money the way a locale writes it, from CLDR's data: the symbol or code, the locale's grouping and decimal separators, the currency's decimal places and where the symbol goes. For example £1,234.50, or 1 234,50 €. It takes three options, each a built-in enum:

  • precision: is a MoneyPrecision. The default, MoneyPrecision.minor, gives the currency's places as CLDR gives them for display. MoneyPrecision.whole gives £1,235, rounding a half away from zero.
  • display: is a MoneyDisplay: symbol (the default), code or name.
  • sign: is a MoneySign: standard or accounting, which shows (£5.00).

Money in a translated message

Inside a translated message, a money field is written {price, number, currency}, one of the messages' placeholder styles. It shows the amount as text.format does with its default options, in the locale the message is written in. Code uses text.format; messages use the placeholder.

It is the only style for a field built on Money, in a message's wording or an email's subject, and it is written exactly so:

  • {price} for an amount, or this style on any other field, is translation.placeholder_style, whose edit writes the right one.
  • Any other spelling, such as {price,number,currency}, is translation.wording_invalid.

A failure's client: fields can't be money: the client app formats those itself, and the export has no money format yet.

An order keeps its total, in pounds, and a deposit in the customer's currency, each in two columns:

example
use bakery/types { CakePrice, Deposit }/// A cake ordered for collection.entity Order {    id: Uuid    total: CakePrice  // columns `total_amount` and `total_currency`    deposit: Deposit?}
example
use bakery/entities { Order }use bakery/types { CakePrice, Deposit }use bakery/actions { pay_deposit }test "an order keeps its amounts as they were written" {    var placed: Order = place_order(CakePrice(money.from_minor(4250, Currency.gbp)))    var order_on_file: Order = attempt Order.get(placed.id)    assert order_on_file.total == money.from_minor(4250, Currency.gbp)    assert order_on_file.deposit == none    var paid: Order = pay_deposit(order_on_file, Deposit(money.from_minor(2000, Currency.eur)))    var order_after_deposit: Order = attempt Order.get(placed.id)    assert order_after_deposit.deposit == paid.deposit    assert order_after_deposit.deposit == Deposit(money.from_minor(2000, Currency.eur))}

Not built yet: a query that compares or orders by a money amount, a one-step update that works one out, and a backfill of a new required money field. Each is reported as toolchain.not_built_yet.

Markdown for agents: /docs/money-operations.md.Built from Jadpo main at a707efb.