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.

trip.adm
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:

std/units
@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), and 1 / 2(s) is a Frequency.
  • ** takes an integer constant: 2(cm) ** 2 is an Area.
  • A dimensionless result is a float: 1(km) / 1(m) is 1000.0.
  • The literal 0 stands for any quantity except a temperature, so let total Length = 0 works.
  • float(q) gives the number in base units; a quantity is never converted to a number implicitly.
average.adm
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 a float: 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.

conversions.adm
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.

input.adm
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:

wait.adm
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.

levels.adm
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.

brewery.adm
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(%).

layout.adm
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

DimensionBase unitOther units
Lengthm, SI prefixesau, nmi, pt
Masskg (g with SI prefixes)t, Da
Times, SI prefixesmin, h, d
ElectricCurrentA, SI prefixes
TemperatureK, SI prefixes°C
Amountmol, SI prefixes
LuminousIntensitycd, SI prefixes
Anglerad, SI prefixes°, ′, ″
SolidAnglesr
DataSizeB, data prefixesbit, data prefixes

Derived dimensions

DimensionIsNamed units
AreaLength²ha
VolumeLength³L, SI prefixes
SpeedLength / Timekn
AccelerationSpeed / Timeg₀
JerkAcceleration / Time
AngularVelocityAngle / Timerpm
AngularAccelerationAngularVelocity / Time
ResolutionLength⁻¹dpi
ForceMass · AccelerationN, SI prefixes
PressureForce / AreaPa, bar, SI prefixes
Energy = TorqueForce · LengthJ, eV, SI prefixes; kWh, kcal
PowerEnergy / TimeW, SI prefixes; dBm, dBW
MomentumMass · Speed
DensityMass / Volume
VolumeFlow, MassFlowVolume / Time, Mass / Time
ViscosityPressure · Time
FuelConsumptionVolume / Lengthwritten L/100km
Frequency = RadioactivityTime⁻¹Hz, Bq, SI prefixes; bpm
ChargeElectricCurrent · TimeC, SI prefixes; mAh
VoltagePower / ElectricCurrentV, SI prefixes
Resistance, ConductanceVoltage / ElectricCurrent and its inverseΩ, S, SI prefixes
CapacitanceCharge / VoltageF, SI prefixes
MagneticFluxVoltage · TimeWb, SI prefixes
InductanceMagneticFlux / ElectricCurrentH, SI prefixes
FluxDensityMagneticFlux / AreaT, SI prefixes
LuminousFluxLuminousIntensity · SolidAnglelm
IlluminanceLuminousFlux / Arealx
LuminanceLuminousIntensity / Area
SpecificHeatEnergy / (Mass · Temperature)
MolarMassMass / Amount
ConcentrationAmount / Volume
AbsorbedDose = EquivalentDoseEnergy / MassGy, Sv, SI prefixes
DataRateDataSize / Timebps, 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).