Units of measure
In ADM a number can carry a unit, and the type of that value is its dimension
(Length, Speed, Energy). The compiler checks the
dimensions, so adding metres to seconds does not compile. You pick a unit when you write a
value and again when you convert or print it. In between you work with lengths, speeds and
forces.
application Trip {
use std.units::*
def eta(distance Length, speed Speed) Time {
return distance / speed
}
def new(args string[]) int {
let t = eta(420(km), 110(km/h))
println(t.str(h, digits = 1)) // 3.8 h
let force = 75(kg) * 9.81(m/s²)
println(force.str(N)) // 735.75 N
// let wrong = 420(km) + t // error: cannot add std.units.Length and std.units.Time
return 0
}
}
At run time a quantity is a plain float in SI base units: 1(km)
is the constant 1000.0, and the dimension exists only in the type. A quantity takes
the same memory as a float, and an array of them can be passed to C as an array of
double.
Quantity literals
A unit is written in parentheses directly after a number literal: 100(km),
9.81(m/s²), -5(m), 36(km/h), 50(%).
Only number literals take a unit this way; a variable is converted by multiplying or with
quantity.
The text in the parentheses is unit notation, not an expression:
- names, including
µ,Ω,%,°,′,″and subscript digits; *or·to multiply,/to divide everything after it:kg·m/s²,J/(kg·K);- powers as
^2,^-1,²or⁻¹, and parentheses; - a number directly before a name as a scale:
6.2(L/100km).
Spaces are ignored, and in inside the parentheses is the inch, not the keyword.
Unit symbols are not variables, so you can still name a local m, s
or kg. A literal sees the units of the modules imported where it is written,
so use std.units (whole, by member, or with *) is enough to write
2(m). adm fmt writes units in canonical form:
9.81 (m / s ^ 2) becomes 9.81(m/s²).
Dimensions
A dimension is a type. std.units declares the base dimensions with
@dimension and derives the rest from them with *, /
and integer powers:
@dimension(unit = "m")
type Length
@dimension(unit = "s")
type Time
type Speed = Length / Time
type Acceleration = Speed / Time
type Area = Length ** 2
type Force = Mass * Acceleration
Two dimensions made of the same base dimensions are the same type, so an
Energy can be assigned to a Torque. A result that no declared type
names still has a dimension, written in base dimensions (Mass·Time).
Parameters and fields use the dimension name like any type:
def eta(d Length, v Speed) Time.
Arithmetic
+,-and comparisons need the same dimension.*and/combine dimensions; a number scales a quantity (2 * 8(px),x / 2), and1 / 2(s)is aFrequency.**takes an integer constant:2(cm) ** 2is anArea.- A dimensionless result is a
float:1(km) / 1(m)is1000.0. - The literal
0stands for any quantity except a temperature, solet total Length = 0works. float(q)gives the number in base units; a quantity is never converted to a number implicitly.
def average(parts Length[]) Length {
let total Length = 0
for let p in parts {
total += p
}
return total / parts.len()
}
println(average([1(m), 2(m), 6(m)])) // 3 m
println(float(36(km/h))) // 10 (m/s)
Conversions and printing
A quantity does not remember the unit it was written in: 10(m/s) and
36(km/h) are the same value. You choose a unit again when you convert or print:
v.to(km/h)is afloat: the value expressed in that unit.v.str(),println(v)and interpolation print base units:735.75 kg·m/s².v.str(N)prints in another unit,v.str(km, digits = 2)with a fixed number of decimals.
The unit must measure the value's dimension; distance.to(kg) is a compile error.
let fuel = 6.2(L/100km)
let used Volume = 420(km) * fuel
println(used.str(L)) // 26.04 L
let size = 1(GiB)
println(size.str(MB, digits = 0)) // 1074 MB
let warm = 20(°C) + 5(K)
println(warm.str(°C)) // 25 °C
Numbers from variables and text
quantity(value, unit) turns a number held in a variable into a quantity. Multiplying by a one-unit literal does the same for linear
units. parseUnit(text, unit) reads user input such as "80 km/h",
"45 L" or a bare "45", which it reads in unit; it
fails when the text does not start with a number or names a unit it does not know.
let x = 3.0
println(quantity(x, km)) // 3000 m
println((x * 1(km)).str(km)) // 3 km
let limit = parseUnit("80 km/h", km/h) onerror (err error) {
return 1
}
println(limit) // 22.2222 m/s
parseUnit is declared as def parseUnit<D>(text string, unit Unit<D>) !D:
a parameter of type Unit<D> takes a unit as its argument, and the unit's
dimension becomes D. Your own functions can take units the same way.
Durations
duration is unchanged: nanoseconds, written 2h or
150ms, and 1m is still one minute (duration literals also accept
min). Where a duration meets a quantity in an operator, it counts as a
Time in seconds, so 100(km) / 2h is a Speed and
2h == 2(h) is true. To turn a quantity into a duration, convert it:
let wait = 1.5(min)
let d = wait.to(s) * 1s
println(d) // 1m30s
Offsets and levels
°C is an offset unit: 21(°C) is 294.15 K. Adding two offset
literals, as in 20(°C) + 5(°C), gives a warning because it adds two absolute
temperatures. 20(°C) + 5(K) does not.
dB, dBFS, dBm and dBW are logarithmic
units. Values stay linear: -6(dB) is the gain 0.501, and
10(dBm) is a Power of 10 mW. to and str
convert back to the level.
let gain = quantity(-6, dB)
println(gain) // 0.501187
println(gain.str(dB)) // -6 dB
let rx = 10(dBm)
println(rx.str(mW)) // 10 mW
Declaring units
@dimension(unit = "…") declares a base dimension and its base unit.
@unit("…") on a constant adds a unit to an existing dimension, with the
constant's value as its size. prefixes = Prefixes.SI generates every SI prefix
(mg, kg, µg), prefixes = Prefixes.Data the
decimal and binary data prefixes (kB, KiB), and
offset and log make offset and logarithmic units.
module brewing {
use std.units::*
// a base dimension of its own
@dimension(unit = "IBU")
type Bitterness
// a new unit for an existing dimension
@unit("bbl")
const barrel Volume = 117.35(L)
}
application Brewery {
use (
std.units::*
brewing::*
)
def new(args string[]) int {
let batch = 20(bbl)
println(batch.str(hL, digits = 1)) // 23.5 hL
let hops = 45(IBU) / batch
println(hops.str(IBU/L)) // 0.0191734 IBU/L
return 0
}
}
Two modules may declare the same symbol. If a literal can see both, the compiler reports it as ambiguous. If it can see neither, the error names the module to import.
UI extents
std.ui declares the layout dimensions Pixels (px),
Percent (%), ViewportHeight (vh),
ViewportWidth (vw) and FontRelative
(em), and Extent, a union of those and Length. Style
properties such as padding and margin take an Extent, and a match
can check whether a value is 8(px) or 50(%).
let pad Extent = 8(px)
let width Extent = 50(%)
Catalogue
Everything below comes from use std.units. It covers SI and the units accepted
alongside it, with no imperial units. "SI prefixes" means every prefix from quecto to
quetta; "data prefixes" means k, M, G, T, … and Ki, Mi, Gi, Ti, ….
Base dimensions
| Dimension | Base unit | Other units |
|---|---|---|
Length | m, SI prefixes | au, nmi, pt |
Mass | kg (g with SI prefixes) | t, Da |
Time | s, SI prefixes | min, h, d |
ElectricCurrent | A, SI prefixes | |
Temperature | K, SI prefixes | °C |
Amount | mol, SI prefixes | |
LuminousIntensity | cd, SI prefixes | |
Angle | rad, SI prefixes | °, ′, ″ |
SolidAngle | sr | |
DataSize | B, data prefixes | bit, data prefixes |
Derived dimensions
| Dimension | Is | Named units |
|---|---|---|
Area | Length² | ha |
Volume | Length³ | L, SI prefixes |
Speed | Length / Time | kn |
Acceleration | Speed / Time | g₀ |
Jerk | Acceleration / Time | |
AngularVelocity | Angle / Time | rpm |
AngularAcceleration | AngularVelocity / Time | |
Resolution | Length⁻¹ | dpi |
Force | Mass · Acceleration | N, SI prefixes |
Pressure | Force / Area | Pa, bar, SI prefixes |
Energy = Torque | Force · Length | J, eV, SI prefixes; kWh, kcal |
Power | Energy / Time | W, SI prefixes; dBm, dBW |
Momentum | Mass · Speed | |
Density | Mass / Volume | |
VolumeFlow, MassFlow | Volume / Time, Mass / Time | |
Viscosity | Pressure · Time | |
FuelConsumption | Volume / Length | written L/100km |
Frequency = Radioactivity | Time⁻¹ | Hz, Bq, SI prefixes; bpm |
Charge | ElectricCurrent · Time | C, SI prefixes; mAh |
Voltage | Power / ElectricCurrent | V, SI prefixes |
Resistance, Conductance | Voltage / ElectricCurrent and its inverse | Ω, S, SI prefixes |
Capacitance | Charge / Voltage | F, SI prefixes |
MagneticFlux | Voltage · Time | Wb, SI prefixes |
Inductance | MagneticFlux / ElectricCurrent | H, SI prefixes |
FluxDensity | MagneticFlux / Area | T, SI prefixes |
LuminousFlux | LuminousIntensity · SolidAngle | lm |
Illuminance | LuminousFlux / Area | lx |
Luminance | LuminousIntensity / Area | |
SpecificHeat | Energy / (Mass · Temperature) | |
MolarMass | Mass / Amount | |
Concentration | Amount / Volume | |
AbsorbedDose = EquivalentDose | Energy / Mass | Gy, Sv, SI prefixes |
DataRate | DataSize / Time | bps, data prefixes |
Any compound of these works in a literal or a conversion without a name of its own:
km/h, kg·m/s², J/(kg·K), Mbit/s.
dB and dBFS are dimensionless levels (amplitude ratios).