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'spartsis 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.sumchecks every item's currency againstcurrency:, which is also the total of an empty list. When the items' type fixes that currency, the call needs no arm.money.of'stoo_many_placesneeds no arm when the currency is written out and the amount is anInt, a literal with no more decimal places than the currency has, or a type whosescaleis 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 asmax. - 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, andcurrency, anenumof 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 aMoneyPrecision. The default,MoneyPrecision.minor, gives the currency's places as CLDR gives them for display.MoneyPrecision.wholegives£1,235, rounding a half away from zero.display:is aMoneyDisplay:symbol(the default),codeorname.sign:is aMoneySign:standardoraccounting, 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, istranslation.placeholder_style, whose edit writes the right one.- Any other spelling, such as
{price,number,currency}, istranslation.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:
use bakery/types { CakePrice, Deposit }/// A cake ordered for collection.entity Order { id: Uuid total: CakePrice // columns `total_amount` and `total_currency` deposit: Deposit?}
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.