Data & Enterprise
Finance & Money
Money isn't a number. 10 USD and 10 EUR both hold
ten, but they aren't the same money — and a plain decimal can't hold a price
exactly (0.1 + 0.2 is 0.30000000000000004). Zornux
ships a finance library that models money as an
exact domain value, so accounting code adds up to the penny.
Making money
Import the library and create a Money value with
finance.money. The amount is exact from text — no decimal ever
touches it — and is held as a whole number of the currency's
minor units (cents, whole yen, thousandths of a dinar):
import finance
create price = finance.money("12.50", "USD") # exact from text
create fee = finance.of_minor(99, "USD") # 99 cents = $0.99
show finance.format(price) # $12.50
Money is an ordinary value from a library, not new syntax — the language gains neither a money keyword nor a second numeric kind. Because the amount is whole minor units, every add and subtract is exact where a plain number would drift.
Currency-safe arithmetic
Arithmetic is functions, not operators — and currencies never
mix. Adding dollars to euros is refused, not silently summed; you
convert first (below). finance.multiply rounds back to whole
minor units under a policy you state.
create subtotal = finance.add(price, fee) # same currency — ok
create doubled = finance.multiply(price, 2) # $25.00
create taxed = finance.multiply(price, "1.08") # rounds to minor units
show finance.compare(price, fee) # 1 (price is larger)
show finance.is_zero(price) # false
| Function | Result |
|---|---|
finance.add(a, b) / finance.subtract(a, b) | Sum or difference — same currency only. |
finance.multiply(money, factor) | Scale by a rate or quantity, rounded to minor units. |
finance.negate(money) / finance.abs(money) | Flip the sign / drop the sign. |
finance.compare(a, b) | -1, 0, or 1 — same currency only. |
finance.is_zero / is_negative / is_positive(money) | Sign tests, as a Truth. |
finance.minor_units / currency_code / currency_info(money) | The raw minor-unit count, the ISO code, or the full currency record. |
finance.format(money) | Render $1,234.50 — options turn the symbol, grouping, or code on or off. |
Splitting loses nothing
Dividing money must never lose or invent a penny.
finance.split_evenly splits into equal parts, and
finance.allocate distributes by weighted ratios — both hand out
leftover minor units by largest remainder, so the parts always sum back to
the whole.
create parts = finance.split_evenly(finance.money("10.00", "USD"), 3)
# [$3.34, $3.33, $3.33] — sums back to exactly $10.00
create shares = finance.allocate(finance.money("100.00", "USD"), [2, 1, 1])
# [$50.00, $25.00, $25.00]
Rounding is a value
Rounding happens only where it's unavoidable, and always under a policy you
name — never a hidden default. finance.rounding turns a policy
name into a value you can name once and reuse; a typo is caught the moment
you make it, not at some later calculation.
create bankers = finance.rounding("half_even") # a RoundingPolicy value
create total = finance.multiply(price, "1.075", bankers)
| Policy | Rounds… |
|---|---|
half_even (default) | To nearest; ties to the even digit (banker's rounding). |
half_up | To nearest; ties away from zero. |
up / down | Away from / toward zero. |
ceiling / floor | Toward positive / negative infinity. |
Every function that rounds — multiply, convert, and the tax functions — also accepts a bare policy name like "half_up". Make a finance.rounding value when you want to state the rule once and reuse it everywhere.
Tax, both ways
Tax is easy to get subtly wrong by rounding twice. A tax is just a name and a
rate (finance.tax), or a bare rate; the library rounds once and
always reconciles, whether the tax is added to a net price
or extracted from a gross one.
create vat = finance.tax("VAT", "0.20") # 20%
# Sales-tax style — add tax onto a net price.
show finance.format(finance.tax_on(price, vat)) # the tax alone
show finance.format(finance.add_tax(price, vat)) # net + tax
# VAT style — pull the tax out of a tax-inclusive price.
create gross = finance.money("100.00", "GBP")
show finance.format(finance.remove_tax(gross, vat)) # net inside
show finance.format(finance.tax_included_in(gross, vat)) # tax portion
For an inclusive price, the net is rounded and the tax is taken as the remainder — net = gross − tax — so net + tax equals the gross exactly, even when £100.00 splits into £83.33 and £16.67.
Exchange & conversion
Because currencies don't mix, converting is explicit. Convert at a one-off rate, or make a directional exchange rate to hold and reuse — applying it to the wrong currency is refused rather than silently wrong.
# One-off conversion — one USD buys 'rate' units of the target.
create in_jpy = finance.convert(price, "JPY", "155.0") # exact across scales
# A reusable, directional rate.
create usd_eur = finance.exchange_rate("USD", "EUR", "0.92")
create in_eur = finance.convert_with(price, usd_eur)
create eur_usd = finance.invert_rate(usd_eur) # EUR → USD
Money as data
A Money value serializes to JSON as
{ amount, currency, minor_units } — with the amount written as
text, so JSON's own floating-point numbers can never lose a penny. It reads
back the same way, ready to store or send.
show to_json(price)
# { "amount": "12.50", "currency": "USD", "minor_units": 1250 }
The library uses finance.currency_info, finance.abs, finance.split_evenly, and finance.rounding precisely so it never collides with the global currency, absolute, split, and round you already use elsewhere.
Next: settings and secrets — Configuration.