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

Physical quantities

On this page

A quantity type holds a measured amount, such as a length, a mass, a temperature or a file's size. Each one (Length, Mass, Temperature ... and DataSize, Bitrate) holds an exact Decimal in its kind's SI base unit, at a fixed scale, a number of decimal places, for each kind.

You choose a unit only when you make a value or show one. A Length is a Length whatever unit built it, so == and comparisons work across units: 1km == 1000metre. Inside a program a quantity has no unit, so metres plus centimetres is just Length + Length.

A quantity is a type built into the language, as Money is. You build a type on one with min: and max:, as in type ShelfLength = Length { ... }. Like Int, a quantity is a representation type: a signature names a type built on it, while a var may be a plain Length (Signatures use named types). Every kind is ordered, Temperature and SoundLevel included.

Kind Base unit Scale (smallest step)
Length metre 6 (a micrometre)
Area square metre 6
Volume cubic metre 9 (a cubic millimetre)
Angle radian 9
Mass kilogram 9 (a microgram)
Pressure pascal 3
Energy joule 3
Power watt 3
Temperature, TemperatureDifference kelvin 3
Speed metre per second 6
Frequency hertz 3
Voltage volt 6
Current ampere 6
Illuminance lux 3
SoundLevel decibel 2
Concentration kilogram per cubic metre 9
DataSize byte 0
Bitrate bit per second 0

Making a quantity

Making a quantity always names a unit. There are four ways:

  • units.of(value, unit), from an Int or a Decimal and a unit of the kind: units.of(5, LengthUnit.kilometre).
  • A literal with a unit suffix, such as 5km, 250g or 2.5kg (Suffixed literals).
  • text.parse("5 km", as: Length), through the contract. It fails unknown_unit for a unit that isn't the kind's, malformed for text with no number, or with a rule of the type.
  • Another value of the same kind, by construction into a named type (RoomWidth(length)), which checks the type's rules. No conversion is needed, since both are already in metres.

A bare number where a quantity is expected is a compile error, type.unit_required, whose fix asks for the unit (5km, or units.of(5, LengthUnit.kilometre)). It can never be declined (Declining a judgement call): a number without its unit is the mistake the type exists to prevent.

A suffix that names no unit, or misses one only by case (5mw, between mW and MW), is type.unit_unknown, which names each unit it was near.

Units

Each kind's units are an enum named for the kind, such as LengthUnit, whose variants are the units' names. A Length has millimetre, centimetre, metre, kilometre, inch, foot, yard, mile and nautical_mile, and so on for each kind. There is one variant for each suffix in the table under Suffixed literals, plus the units that have no suffix: the cubic metre (VolumeUnit.cubic_metre), and a TemperatureDifference's celsius, fahrenheit and kelvin.

A unit's code is its suffix, which text and JSON use too; the cubic metre's code is cubic_metre. text.format(value, locale:, unit:) shows a quantity in one of these units, or in its kind's base unit when you leave unit: out.

What the suffixes mean

  • floz is the US fluid ounce (29.5735295625 ml), since UK labels give millilitres.
  • A degree of angle is π / 180 radians, to 30 digits.
  • psi is a pound-force on a square inch, kcal the thermochemical 4,184 J, t the tonne and knot 1,852 m an hour.

A value is converted exactly where the unit is defined exactly, and rounded half to even to its kind's scale. A Mass is held in kilograms to 9 places, numeric(38, 9), so 1mcg is exact, as a medicine's dose needs. No other kind has a suffix finer than its scale.

A literal's suffix decides its kind

A duration's suffix (ms, s, m, h, d) makes a Duration, and any other suffix makes a quantity, matched exactly. A quantity literal may be negative (-40degC); a duration literal may not.

- on a quantity value is refused, as it is on any value but a number (Expressions and operators). Write the quantity first when you multiply: bracket * 2, never 2 * bracket, as for money.

Quantities as text

A quantity's canonical text is the number in the base unit with no trailing zeros, a space, and the base unit's code: 2.5 kg, 1000 metre.

text.parse reads a number, an optional space, and any of the kind's units by its code, matching case exactly. It fails malformed for text with no number or no unit, and unknown_unit for another kind's unit or a word that is no unit.

Showing quantities to people

text.format's unit: is a unit of the value's kind. The number is the value in that unit, in the locale's digits and separators. It is exact where the conversion ends within the places the kind's scale gives in that unit, and otherwise rounded half to even to one place fewer: 1oz, held as 0.028349523 kg, is shown as 1 oz.

For lengths, areas, volumes and masses, ICU4X has CLDR's unit names, and the locale's short pattern places the unit (252,62 cm). For the other kinds it writes the number, a space and the unit's SI symbol (21 °C, but 90°), until ICU4X has their names.

Display writes a quantity as code makes one, in its base unit: 2.5262metre, or units.of(0.001, VolumeUnit.cubic_metre) for a kind whose base unit has no suffix.

Suffixed literals

Durations already have suffixes, 500ms, 30s, 5m, 2h and 30d (Durations). 5m is five minutes, so you write metres as 5metre, or with units.of. The type a literal is expected to have never changes what its suffix means, because a reviewer can't see that type at the literal.

Suffixes are case-sensitive, as SI writes them, and the longest suffix wins (dB before d, mmHg before mm). A near-miss by case (5mw, between milliwatts and megawatts) is an error naming both. No suffix clashes with a duration's or with another unit's; the last column says what each kind's suffixes avoid:

Kind Suffixes Clash avoided
Length mm, cm, metre, km, inch, ft, yd, mi, nmi m is minutes; in is a keyword (for x in)
Mass mcg, mg, g, kg, t, oz, lb µg isn't typeable everywhere
Volume ml, litre, floz, ukgal, usgal l reads as 1; gal alone is ambiguous
Area sqmm, sqm, sqkm, ha, sqft, acre m2 puts a digit in a suffix
Angle deg, rad d is days; longest wins
Temperature degC, degF, kelvin K reads as thousands ("5K")
Speed mps, kmh, mph, knot ms is milliseconds
Pressure Pa, hPa, kPa, bar, mbar, psi, mmHg h is hours; longest wins (mmHg before mm)
Energy J, kJ, Wh, kWh, kcal cal refused: food labels mean kcal
Power mW, W, kW, MW mW and MW differ only by case
Frequency Hz, kHz, MHz, GHz, bpm bps is bits a second
Voltage, Current mV, V, kV; mA, A
Illuminance, SoundLevel lx; dB d is days; longest wins
Concentration mgL, gL
DataSize B, kB, MB, GB, TB, KiB, MiB, GiB, TiB b (bits) refused, so bytes and bits can't be confused
Bitrate bps, kbps, Mbps, Gbps

TemperatureDifference has no suffix of its own: you get one by subtracting two temperatures, or from units.of. The clinical units, mmHg and bpm among them, are under Groups with the baseline only.

A joinery cuts shelves to a customer's length, in whatever unit they give it, and works out floors and paces from lengths:

example
type ShelfLength = Length {    min: 10cm  // a bound written with its unit, held in metres    max: 3metre}type Plank = Length {}type RoomFloor = Area {}type WalkingPace = Speed {}type WalkTime = Duration {    min: 1s}type Miles = Decimal {    scale: 2}type FormText = Text {}type ShelfUnreadable = Failure {    kind: FailureKind.invalid_value}type ShelfUnitWrong = Failure {    kind: FailureKind.invalid_value}type ShelfOutOfRange = Failure {    kind: FailureKind.invalid_value}/// A shelf with a bracket at each end: lengths add, whatever unit made/// them.function with_brackets(shelf: ShelfLength, end_block: Plank) -> Plank {    return Plank(shelf + end_block * 2)}/// A shelf length a customer typed, with its unit: `90 cm`, `3 ft`.function typed_shelf(    raw: FormText) fails ShelfUnreadable, ShelfUnitWrong, ShelfOutOfRange -> ShelfLength {    var shelf: ShelfLength = attempt text.parse(raw, as: ShelfLength) {        malformed: ShelfUnreadable  // no number, or no unit        unknown_unit: ShelfUnitWrong  // `90 kg`        min: ShelfOutOfRange        max: ShelfOutOfRange    }    return shelf}/// A room's floor: a length times a length is an area.function floor_of(width: Plank, room_depth: Plank) -> RoomFloor {    return RoomFloor(width * room_depth)}/// How fast a walk went: a length over a duration is a speed.function pace_of(stroll: Plank, stroll_time: WalkTime) -> WalkingPace {    return WalkingPace(stroll / stroll_time)}/// A distance in miles, to two places.function in_miles(stroll: Plank) -> Miles {    return Miles(units.in(stroll, LengthUnit.mile, scale: 2, rounding: Rounding.half_even))}/// A plank's length for the reader, in centimetres.function plank_label(plank: Plank, customer: Locale) -> PresentationText {    return text.format(plank, locale: customer, unit: LengthUnit.centimetre)}
example
test "lengths add across units and are shown in any" {    var fitted: Length = 2metre + 45cm + 3inch    assert fitted == 2.5262metre    assert 1km == 1000metre    assert units.of(5, LengthUnit.kilometre) == 5km    assert plank_label(Plank(fitted), Locale.en_gb) == "252.62 cm"    assert plank_label(Plank(fitted), Locale.de_de) == "252,62 cm"    assert text.format(fitted, locale: Locale.en_gb) == "2.5262 m"    assert text.from(2.5kg) == "2.5 kg"    assert in_miles(5km) == 3.11}test "kinds multiply and divide as the table says" {    assert with_brackets(90cm, 2cm) == 94cm    assert floor_of(3metre, 2metre) == 6sqm    assert pace_of(100metre, 8s) == 12.5mps    assert 25degC - 20degC == units.of(5, TemperatureDifferenceUnit.kelvin)    assert units.sum([1km, 500metre]) == 1.5km}test "text is read with its kind's units" {    var shelf: ShelfLength = attempt typed_shelf("90 cm")    assert shelf == 0.9metre    var in_feet: ShelfLength = attempt typed_shelf("3 ft")    assert in_feet == 0.9144metre    assert typed_shelf("90") fails ShelfUnreadable    assert typed_shelf("90 kg") fails ShelfUnitWrong    assert typed_shelf("5 metre") fails ShelfOutOfRange}

Quantity arithmetic

+ and - work within one kind, and * and / by a plain number keep the kind. One kind divided by the same kind gives a plain Decimal, as / does for a named type (Expressions and operators).

Within a kind, +, - and / follow the same named-type rule: the two sides are one named type, or a plain quantity an operator or a units operation made. A divisor's type must exclude zero; for a duration, its type's min: counts.

These products and quotients are defined, and any other mix is a compile error naming the two kinds:

Left Operator Right Gives
Length * Length Area
Area * Length Volume
Area / Length Length
Volume / Area Length
Volume / Length Area
Length / Duration Speed
Speed * Duration Length
Length / Speed Duration
Energy / Duration Power
Power * Duration Energy
Voltage * Current Power
Power / Voltage Current
Power / Current Voltage
Mass / Volume Concentration
Concentration * Volume Mass
DataSize / Duration Bitrate
Bitrate * Duration DataSize
Temperature - Temperature TemperatureDifference
Temperature + or - TemperatureDifference Temperature
  • Rounding. A product is exact, and a quotient is worked out exactly where it ends. Either is then rounded half to even to the result kind's scale, as constructing into a type with a fixed scale rounds (Creating values). For another rounding, use units.in with rounding:.
  • Temperature doesn't add to itself: 20 °C plus 20 °C isn't 40 °C. It adds only a TemperatureDifference. SoundLevel is logarithmic, so it compares but has no + or -.
  • Duration is still the type Durations describes. It takes part in the table above without becoming a quantity.

Storing quantities

A quantity is stored in one numeric column, in the base unit at the kind's scale from the table above: numeric(38, 6) for a Length. The column's comment names its kind and SI unit as the units table writes it, Length in metres (m). A migration sets the comment with the column and changes it with the type. A query compares (<, ==) and orders by a quantity field as it does by a number.

Quantities in JSON

A quantity is sent with its unit named: { "value": "2.5", "unit": "kg" }. A bare number in the base unit wouldn't do, because a client can't tell it from a number in another unit. The number is decimal text, as every Decimal is sent (Values and types).

Jadpo writes the base unit and reads any of the kind's units. A JSON number for value fails as type, and another kind's unit as unknown_unit. json.read and json.write use the same shape, and OpenAPI lists the kind's units as the unit's enum.

units operations

Operation Gives Fails SQL form
units.of(value, unit) The quantity of unit's kind: value, an Int or Decimal, converted exactly to the base unit and rounded half to even to the kind's scale overflow is a defect none
units.in(value, unit, scale:, rounding:) Decimal, the quantity in unit, rounded none
units.sum(items) The total of one kind; zero for none overflow is a defect none

units.in needs both scale: and rounding:, as decimal.round does. units.sum refuses a list of temperatures or sound levels (type.arithmetic_operand), which don't add. A list written out takes its first item's kind.

A shelf on file keeps its length in metres, one column, however it was cut:

example
use joinery/types { CutLength }/// A shelf cut to order.entity CutShelf {    id: Uuid    cut_to: CutLength  // one `numeric(38, 6)` column, in metres}
example
use joinery/entities { CutShelf }use joinery/types { CutLength }test "a shelf keeps its length as it was cut" {    var shelf_made: CutShelf = cut_to_order(CutLength(2.5262metre))    var shelf_on_file: CutShelf = attempt CutShelf.get(shelf_made.id)    assert shelf_on_file.cut_to == 2metre + 45cm + 3inch    assert text.format(        shelf_on_file.cut_to,        locale: Locale.en_gb,        unit: LengthUnit.centimetre    ) == "252.62 cm"    assert text.format(        shelf_on_file.cut_to,        locale: Locale.de_de,        unit: LengthUnit.centimetre    ) == "252,62 cm"}

Percentage, Ratio, Fraction and the % literal are under The standard operator table.

Markdown for agents: /docs/physical-quantities.md.Built from Jadpo main at a707efb.