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

Prices, discounts and rates

On this page

These types hold a price, a discount, a price per kilogram or litre, and the rates a business quotes: tax, basis points and interest. Each is built from Money, Percentage or a quantity:

Type What it holds Sent as Shown as
Price amount, a Money never below 0, and includes_tax, a Bool, so a gross price is never taken for a net one {"amount": {"amount": 1050, "currency": "GBP"}, "includes_tax": true} Its amount, £10.50
Discount A choice: amount_off { amount }, a Money never below 0, or percentage_off { percentage }, from 0% to 100% {"tag": "percentage_off", "percentage": "20%"} Its part, £5.00 or 20%
UnitPrice A choice by the quantity's kind, as the UK's Price Marking Order prices goods: per_mass, per_volume, per_length or per_area, each a price and the amount it is per {"tag": "per_mass", "price": {...}, "per": {"value": "1", "unit": "kg"}} £2.50/1 kg
TaxRate A Percentage never below 0% "20%" 20%
BasisPoints A whole number of hundredths of a percent, of any sign 25, a JSON number 25 bp
InterestRate rate, a Percentage a year that may be below 0%, and basis, an InterestRateBasis: aer, apr or flat {"rate": "5.5%", "basis": "apr"} 5.5% APR

Choices: Discount and UnitPrice

Discount and UnitPrice are each a choice between variants: an enum with data, the language's tagged union, where each variant carries its own fields. You build, match and read one as you would your own enum. Discount.percentage_off { percentage: 20% } builds one, and the match arm Discount.percentage_off(off) -> off.percentage reads it. It is sent in JSON in the same tagged form as your own enums, and shown by the parts its variant has.

The four records, Price, Discount, UnitPrice and InterestRate, have no single text form, so text.from and text.parse refuse them, as they refuse a PostalAddress. None is ordered, and .all refuses a choice, as it refuses any enum with data.

TaxRate and the parts' bounds

A TaxRate is a Percentage that is never below 0%. The standard library declares it as a type built on Percentage with min: 0%, as it declares the types held as text, so building one below 0% breaks min. Its text, JSON, storage and format are Percentage's. It has no maximum, because some duties are set above 100%.

The records' parts keep bounds the same way: a price's Money and an amount off are never below 0, a percentage off is from 0% to 100%, and a unit price's amount is never below 0.

Price

A Price is an amount and includes_tax, so a price with tax is never taken for one without. It is shown as its amount, because CLDR has no words for tax status; your app writes "inc. VAT" itself.

Discount's variants

Discount's variants, amount_off and percentage_off, each hold one named field, because a variant's data is always named fields.

Applying a discount

money.apply_discount(price, discount) takes a Price and gives one, keeping includes_tax. It is an operation in money because the standard operator table has no row for a Discount.

A percentage's share is rounded half to even to a minor unit before it comes off, so a receipt's discount line and its total agree. More off than the price leaves nothing, never less. An amount off in another currency fails currency_mismatch. Which variant a Discount holds is known only while the program runs, so every call needs its attempt.

UnitPrice

A UnitPrice is a price per amount of something, as a shelf label shows it. It is a choice by the quantity's kind, per_mass, per_volume, per_length or per_area, the kinds UK law prices goods by. Each holds a price and the amount it is per (1kg, 100g).

It is shown as the price over the amount in its kind's base unit, £2.50/1 kg, by CLDR's short "per" pattern. A release adds a kind, such as energy per kWh, when an application needs one.

BasisPoints

A BasisPoints is a whole number of hundredths of a percent, of any sign, held within JavaScript's safe integers as an amount of money is. Its text is its digits, -25 for a negative one. It is sent as a JSON number, and shown with bp in every locale: 25 bp. There is no conversion to a Percentage.

InterestRate

An InterestRate is a rate a year, which may be below 0%, and a basis. The basis is an InterestRateBasis, a standard enum of aer, apr and flat. Each value is its own code, and is shown as lenders write it, in every locale: AER, APR, flat.

None of these types has a shape, the set of fields Jadpo uses to spot an app type that copies a standard one (type.standard_exists). None is personal data.

A bakery puts offers on its cakes, works out the VAT on a price before tax, and labels its apples and its loan as the law and the lender write them:

example
type WrongCurrency = Failure {    kind: FailureKind.invalid_value}type ShareOff = Percentage {}type CakeAmount = Money {}type FormText = Text {}type NotARate = Failure {    kind: FailureKind.invalid_value}/// A cake's price with a deal taken off it; an amount off in another/// currency fails.function with_deal(price: Price, deal: Discount) fails WrongCurrency -> Price {    var reduced: Price = attempt money.apply_discount(price, deal) {        currency_mismatch: WrongCurrency    }    return reduced}/// A deal as the shop's window shows it.function deal_label(deal: Discount, customer: Locale) -> PresentationText {    return text.format(deal, locale: customer)}/// The share of the price a deal takes off, where it names one.function share_off(deal: Discount) -> ShareOff? {    return match deal {        Discount.amount_off -> none        Discount.percentage_off(off) -> ShareOff(off.percentage)    }}/// The VAT on a price before tax at `rate`, rounded half to even to a/// penny.function vat_on(untaxed: Price, rate: TaxRate) -> CakeAmount {    return CakeAmount(untaxed.amount * rate)}/// A VAT rate typed into the till's settings.function vat_rate_from(raw: FormText) fails NotARate -> TaxRate {    var rate: TaxRate = attempt text.parse(raw, as: TaxRate) {        malformed: NotARate        invalid: NotARate    }    return rate}/// Apples, priced by the kilogram as the shelf label must show them.function apples() -> UnitPrice {    return UnitPrice.per_mass {        price: money.from_minor(250, Currency.gbp)        per: 1kg    }}/// The rate on the bakery's loan, as the lender quotes it.function loan_rate() -> InterestRate {    return InterestRate {        rate: 6.9%        basis: InterestRateBasis.apr    }}
example
test "a deal comes off a price either way" {    var cake: Price = Price {        amount: money.from_minor(1250, Currency.gbp)        includes_tax: true    }    var five_off: Discount = Discount.amount_off {        amount: money.from_minor(500, Currency.gbp)    }    var fifth_off: Discount = Discount.percentage_off {        percentage: 20%    }    var after_five: Price = attempt with_deal(cake, five_off)    assert after_five.amount == money.from_minor(750, Currency.gbp)    assert after_five.includes_tax    var after_fifth: Price = attempt with_deal(cake, fifth_off)    assert after_fifth.amount == money.from_minor(1000, Currency.gbp)    var in_euros: Discount = Discount.amount_off {        amount: money.from_minor(500, Currency.eur)    }    assert with_deal(cake, in_euros) fails WrongCurrency    assert deal_label(five_off, Locale.en_gb) == "£5.00"    assert deal_label(fifth_off, Locale.en_gb) == "20%"    assert share_off(fifth_off) == 20%    assert share_off(five_off) == none}test "more off than the price leaves nothing" {    var bun: Price = Price {        amount: money.from_minor(300, Currency.gbp)        includes_tax: true    }    var five_off: Discount = Discount.amount_off {        amount: money.from_minor(500, Currency.gbp)    }    var after: Price = attempt with_deal(bun, five_off)    assert after.amount == money.from_minor(0, Currency.gbp)}test "rates are shown as they are quoted" {    var untaxed: Price = Price {        amount: money.from_minor(1000, Currency.gbp)        includes_tax: false    }    var vat: TaxRate = attempt vat_rate_from("20%")    assert vat_on(untaxed, vat) == money.from_minor(200, Currency.gbp)    assert vat_rate_from("-5%") fails NotARate    assert vat_rate_from("20") fails NotARate    assert text.format(apples(), locale: Locale.en_gb) == "£2.50/1 kg"    assert text.format(loan_rate(), locale: Locale.en_gb) == "6.9% APR"    var over_base: BasisPoints = 25    assert text.format(over_base, locale: Locale.en_gb) == "25 bp"    assert text.from(over_base) == "25"}

A percentage off past 100% doesn't compile, as a literal past any type's bounds doesn't:

example
/// Everything off, and more.function clearance() -> Discount {    return Discount.percentage_off {        percentage: 120%    }}
text
error: `120%` isn't a valid `Discount.percentage_off.percentage` --> main.jadpo:4:21  |4 |         percentage: 120%  |                     ^^^^ breaks `max: 100%`  reason: `Discount.percentage_off.percentage` declares `max: 100%`, and the value is above 100%. A literal is checked against every rule while compiling.  next: Use a value that `Discount.percentage_off.percentage` allows (agent)  rule: type.invalid_literal  help: https://jadpo.dev/docs/errors/type.invalid_literalcheck failed: 1 error, 1 file

Storing prices and rates

A TaxRate is stored in one numeric column and a BasisPoints in one bigint. The four records are stored a column per part, as an ExchangeRate is. A Price uses amount, currency and includes_tax. A choice such as Discount stores its variant's name in kind, then a column for each part its variants hold, NULL where the stored variant doesn't have that part:

example
/// A loan the bakery took out, at a rate above the base rate.entity Loan {    id: Uuid    over_base: BasisPoints  // one `bigint` column: 25 for 0.25%    vat_rate: TaxRate  // one `numeric` column: 0.2 for 20%}
example
use bakery/entities { Loan }test "a loan keeps its rate over base as it was written" {    var taken: Loan = take_loan(25, 20%)    var on_file: Loan = attempt Loan.get(taken.id)    assert on_file.over_base == 25    assert on_file.vat_rate == 20%}

Markdown for agents: /docs/prices-discounts-and-rates.md.Built from Jadpo main at a707efb.