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 anIntor aDecimaland a unit of the kind:units.of(5, LengthUnit.kilometre).- A literal with a unit suffix, such as
5km,250gor2.5kg(Suffixed literals). text.parse("5 km", as: Length), through the contract. It failsunknown_unitfor a unit that isn't the kind's,malformedfor 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
flozis the US fluid ounce (29.5735295625 ml), since UK labels give millilitres.- A degree of angle is π / 180 radians, to 30 digits.
psiis a pound-force on a square inch,kcalthe thermochemical 4,184 J,tthe tonne andknot1,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:
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)}
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
scalerounds (Creating values). For another rounding, useunits.inwithrounding:. Temperaturedoesn't add to itself: 20 °C plus 20 °C isn't 40 °C. It adds only aTemperatureDifference.SoundLevelis logarithmic, so it compares but has no+or-.Durationis 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:
use joinery/types { CutLength }/// A shelf cut to order.entity CutShelf { id: Uuid cut_to: CutLength // one `numeric(38, 6)` column, in metres}
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.