Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Units & Quantities

A quantity is a physical measurement, a number paired with a unit: 45 kg, 42 km/h, 9.81 m/s^2, 10 N/m^2. Dolfin writes them as smart literals, exactly like the temporal values of Chapter 9: a keyword followed by human-friendly notation in parentheses.


fact rex a Dog
  weight quantity(45 kg)

Behind the keyword sits a small dimensional-analysis engine. It knows the SI system, resolves prefixes, does unit arithmetic, and reduces everything to the seven SI base dimensions so that quantities written in different units can still be compared.


The quantity(...) smart literal

The general form is a value, then a unit expression:

quantity( <number> <unit-expression> )
WrittenMeans
quantity(45 kg)45 kilograms
quantity(42 km/h)42 kilometres per hour
quantity(9.81 m/s^2)9.81 metres per second squared
quantity(10 N/m^2)10 newtons per square metre
quantity(1.609 km.s^(-1))1.609 kilometres per second
quantity(273.15 K)273.15 kelvin
quantity(1e3 Pa)1000 pascals

The literal is resolved at compile time: the value, the unit, and the dimension vector are computed once, when the file is parsed. A malformed quantity (unknown unit, impossible conversion) is a compile error, not a runtime surprise (see Errors).


Unit expressions

A unit expression is one or more unit tokens combined with . (multiply), / (divide), and ^ (power):


quantity(5 kg)             # a single unit
quantity(42 km/h)          # a quotient
quantity(9.81 m.s^(-2))    # a product with a negative power
quantity(10 N/m^2)         # 'm^2' is metre-squared
  • . multiplies the units on either side: N.m is a newton-metre.
  • / divides: km/h is kilometres per hour. A unit after / has its exponent negated, so km/h and km.h^(-1) mean the same thing.
  • ^n raises a unit to a power. Negative powers use ^(-n) or a bare ^-n: s^(-1), m^2, m.s^-2.

Prefixes

SI prefixes compose with any unit, so you do not enumerate km, cm, mm separately, they are k+m, c+m, m+m:

PrefixkMGcmµ / un
Factor10³10⁶10⁹10⁻²10⁻³10⁻⁶10⁻⁹

kg, mg, MHz, kPa, cm all resolve compositionally. When a token is both a standalone unit and a prefix, the standalone unit wins: m is a metre (not milli-anything), T is a tesla (not tera), h is an hour, d is a day. Prefix decomposition is only tried when a token is not a known unit on its own.

Known units

The built-in registry covers the SI base units, the named derived units, and the common non-SI units:

LayerTokens (selection)
SI basem, kg, s, A, K, mol, cd
SI derivedN, Pa, J, W, Hz, V, Ohm/Ω, F, C, T
Mass (non-SI)g, t (tonne), oz, lb
Length (non-SI)mi, ft, in, yd
Time (non-SI)min, h, d
VolumeL/l (litre), mL/ml
Pressure/energybar, atm, cal, kcal
TemperatureK; °C/degC, °F/degF (standalone only)

An unknown token is a compile error.


User-defined units (unitdef)

The built-in registry above is fixed — it ships with dolfin-units and cannot be extended from a .dlf file. unitdef is the separate mechanism for a project to declare its own units: currencies, or units that aren’t physically commensurable at all (a bunch of carrots is not a length or a mass). It is a top-level declaration, like concept or property:


unitdef USD: scale 0.92 EUR
unitdef family vegetables
unitdef bunch_of_carrots: nominal of vegetables scale 2
unitdef cabbages: nominal of vegetables scale 1

Why unitdef and not unit. unit is already an ordinary identifier — the readability convention in has weight: unit.Mass above is a qualified name, not a reserved word. Reserving unit for this declaration would break that existing convention, so the declaration keyword is unitdef.

There are two independent kinds of unitdef, and they behave very differently. Picking the wrong one for what you’re modelling gets you either silently-wrong arithmetic or arithmetic dolfin unnecessarily refuses.

Derived units (currencies, and anything else on a real dimension)


unitdef USD: scale 0.92 EUR

This declares USD as an ordinary unit: 1 USD = 0.92 EUR. It behaves exactly like declaring km from m — dolfin-units ships EUR as its one built-in currency unit (an arbitrary base pick, scale 1.0, no other currency and no live exchange rate bundled), and a project adds any other currency itself, with whatever rate it wants to use.

Because a derived unit is a real dimension (dolfin-units has a currency axis alongside the seven SI ones), it composes and converts exactly like any other unit:


quantity(25 USD/h)              # composes fine — a salary rate
quantity(100 USD + 8 EUR)       # auto-converts and adds, like `1 km + 500 m`
quantity(100 USD as EUR)        # → 92 EUR

scale <factor> <reference> requires <reference> to already be a resolvable unit (built-in, or another unitdef — declared in any order, even across files: dolfin resolves references in dependency order, not declaration order). A reference that never resolves is a compile error naming the broken unitdef.

Nominal (incommensurable) units and families


unitdef family vegetables
unitdef bunch_of_carrots: nominal of vegetables scale 2
unitdef cabbages: nominal of vegetables scale 1

unitdef family <name> declares an abstract, dimensionless grouping — not a usable unit by itself, just something other unitdefs can belong to. unitdef <name>: nominal of <family> scale <factor> declares a unit that belongs to that family, with a per-unit scale used only when explicitly widening into the family (see below) — it is not a conversion rate between nominal units.

Unlike a derived unit, a nominal unit is not a real dimension: two different nominal units are incommensurable by design, even if they belong to the same family:


quantity(2 bunch_of_carrots + 3 bunch_of_carrots)   # OK: same unit, ordinary add → 5
quantity(2 bunch_of_carrots + 3 cabbages)            # error: incommensurable units

The only way to +/- different nominal units of the same family is an explicit as <family> cast, which widens each operand via its declared scale into the family’s dimensionless base and only then combines them:


quantity(2 bunch_of_carrots + 3 cabbages as vegetables)   # → 7 (2×2 + 3×1)
quantity(3 bunch_of_carrots as vegetables)                 # → 6 (single operand also widens)
quantity(3 bunch_of_carrots as fruit)                      # error: not a member of `fruit`

Casting to a family you aren’t a member of, or casting a non-nominal quantity to a family, is the same INVALID_QUANTITY-class compile error as casting 5 km as s.

*// follow a different rule than +/-: a nominal unit composes freely with an ordinary (non-nominal) unit, same as any other unit — a carrot-cutting machine’s rate is a perfectly good quantity:


quantity(10 bunch_of_carrots / 2 h)   # → 5 bunch_of_carrots/h, a rate

Two nominal units can also multiply/divide against each other, as long as they belong to the same family — the result is nominal-typed but no longer a plain member of the family (bunch_of_carrots * cabbages is a vegetables^2-shaped quantity, not a vegetables):


quantity(2 bunch_of_carrots * 3 cabbages)   # → 6 bunch_of_carrots.cabbages
quantity(6 bunch_of_carrots / 3 cabbages)   # → 2 bunch_of_carrots/cabbages

Multiplying/dividing nominal units from different families is still rejected — there’s no meaningful composite of bunch_of_carrots (vegetables) and some unit from an unrelated family:


quantity(2 bunch_of_carrots * 3 apples)   # error: incommensurable units (different families)
OperationSame unitSame family, no castSame family, as <family>Different family
+ / -❌ error✅ widens via scale❌ error
* / /✅ (nominal, family-typed)n/a (not a valid cast target for *//)❌ error
* / / with an ordinary (non-nominal) unit✅ composes into a rate, e.g. bunch_of_carrots/h

Arithmetic and conversion

A quantity literal may contain a single arithmetic operation between two measurements, evaluated at parse time:


quantity(42 km / 30 min)   # a speed: 1.4 km/min
quantity(10 N / 1 m^2)     # a pressure: 10 N/m^2 (i.e. 10 Pa)

A trailing as converts the result to a target unit, or reduces it to SI base units. The target must be dimensionally compatible:


quantity(42 km/h as m/s)       # → 11.666… m/s
quantity(90 lb as kg)          # → 40.82… kg
quantity(42 km / 30 min as km/h)  # → 84 km/h
quantity(10 N/m^2 as Pa)       # → 10 Pa (named derived unit)
quantity(5 km as SI)           # → 5000 m (reduce to base units)

Converting across incompatible dimensions (quantity(5 km as s), a length to a time) is a compile error.

The dimensional rules are the ordinary ones:

OperationRequirement
+ / -both operands share the same dimension
* / /none (dimensions combine)
as / converttarget dimension equals source dimension

These are the rules for ordinary (physical, and derived/currency) units. A nominal unit follows a different rule for +/-: it requires the same specific unit (not merely the same dimension — nominal units are all dimensionless). *// compose freely with ordinary units (bunch_of_carrots/h) and with other nominal units of the same family (bunch_of_carrots * cabbages); different families still reject. See that section for the as <family> widening cast, which is the only way to +/- two different nominal units.


Dimensions

Every unit reduces to a vector over the seven SI base dimensions:

DimensionLetterSI base unit
LengthLm
MassMkg
TimeTs
Electric currentIA
TemperatureΘK
Amount of substanceNmol
Luminous intensityJcd

A quantity’s dimension is its real type. km/h and mi/h are different units but the same dimension (L1.T-1, a speed), and so they are comparable; kg and m are not. Two quantities are dimensionally compatible when their dimension vectors are equal.

There is an eighth, non-SI axis, currency, used only by EUR and any derived currency unitdef a project declares — it behaves exactly like the seven physical axes above (composes, converts, compares). Nominal units sit outside this vector entirely: they’re dimensionless (all-zero vector) but carry a separate family tag that ordinary dimension-vector equality doesn’t see, which is precisely why they need their own arithmetic rules instead of just being “another dimension”.


Quantities are values, not a range type

There is no quantity primitive type. A quantity is a value; its type is the dimension the value carries. Model a quantity-valued attribute with a numeric range and give it quantity(...) values:


concept Animal:
  has weight: optional float   # the value supplies the unit

fact rex a Dog
  weight quantity(45 kg)

The declared float range says only “a number”. The unit lives in the value, so one attribute can hold quantity(45 kg) for one animal and quantity(90 lb) for another and still compare them correctly. float accepts any quantity (quantity(45 kg), quantity(3 s)) or a bare number alike — nothing checks the dimension.

Dimension-typed properties

To require a specific dimension, declare the range as a physical dimension instead of float. Dimensions are not a keyword or a grammar feature — they are ordinary concepts, declared in units.dlf (see the code samples for this page) (Mass, Length, Time, Speed, Force, Pressure, Energy, Power, Frequency, Area, Volume, Density, Momentum, and the other four SI base dimensions). Drop that file into a project — dolfin has no import statement, but a project is just a directory of .dlf files whose declarations are all merged together — and reference a dimension by name:


concept Animal:
  has weight: unit.Mass   # was: optional float

fact rex a Dog
  weight quantity(45 kg)      # OK — a mass

fact oops a Dog
  weight quantity(3 m)        # error: S008 dimension mismatch (length, not mass)

unit. is a readability convention, not a namespace import: a qualified type name resolves by its last segment, so unit.Mass and a bare Mass are the same reference. Analysis recognises the concept by name and checks every quantity(...) value assigned to that property against the dimension — a mismatched dimension or a bare number (weight 45) is diagnostic S008 DIMENSION_MISMATCH. In Turtle, such a property emits owl:DatatypeProperty with an rdfs:range naming the dimension IRI (e.g. https://dolfin.dev/dimension/M1), not the Mass concept itself — the value is a literal, not an instance of that concept.


Comparisons

A comparison against a quantity, in a rule or query constraint block, is unit-aware:


rule flag_overweight_dog:
  match:
    ?dog a Dog
    ?dog weight [ > quantity(40 kg) ]
  then:
    ?dog a OverweightAnimal

The comparison compares physical magnitudes, not lexical numbers:

  • The two operands must share a dimension. A mismatch (weight in kilograms compared to quantity(3 m)) makes the comparison fail and the row is dropped, it never matches.
  • Compatible operands are compared by their SI magnitude (value × coefficient). So a dog whose weight was written quantity(90 lb) (≈ 40.8 kg) does trip a > quantity(40 kg) threshold, even though 90 and 40 alone would not suggest it.

For the unit-aware path to run, both sides must be quantities. A bare number (weight [ > 40.0 ]) is compared as a plain scalar, with no unit reasoning, so mixing a quantity(...) value with a bare-number threshold does not do what you want. Keep both the stored values and the threshold as quantity(...).

Use > < >= <=, not = / !=. After a quantity is normalised to its SI magnitude, two “equal” measurements almost never match to the bit, so exact (in)equality is unreliable. The linter warns on = / != against a quantity (rule semantic/quantity-exact-comparison). The ordering operators are the sound ones.


Representation in Turtle

Compiling a quantity uses representation “L”: the value is a typed literal whose datatype is a canonical unit IRI, and each distinct unit’s (coefficient, dimension, symbol) is stated once in a definition block.

@prefix dq: <https://dolfin.dev/quantity#> .

:rex :weight "45"^^<https://dolfin.dev/unit/kg> .
:bella :weight "90"^^<https://dolfin.dev/unit/lb> .

# one definition block per distinct unit that appears in the data:
<https://dolfin.dev/unit/kg> dq:coefficient 1.0 ;
                             dq:dimension   "M1" ;
                             dq:symbol      "kg" .
<https://dolfin.dev/unit/lb> dq:coefficient 0.45359237 ;
                             dq:dimension   "M1" ;
                             dq:symbol      "lb" .
  • Unit datatype IRIhttps://dolfin.dev/unit/ + a canonical unit string. The canonical form keeps the written unit (no SI reduction), orders numerator terms before denominator terms, and renders powers as sym / sym2 / sym-1: km/h and km.h^-1 both become unit:km.h-1; m/s becomes unit:m.s-1; N/m^2 becomes unit:N.m-2. It is written as a full IRI, not a prefixed name, because the local part contains . and -.
  • dq: termsdq:coefficient (factor to the SI base magnitude), dq:dimension (the canonical dimension string, base order L M T I Θ N J, e.g. "L1.T-1"), and dq:symbol (the human-readable unit, e.g. "km/h").
  • A dimensionless quantity carries no unit and degrades to a plain "<value>"^^xsd:double.

How comparisons unfold

A quantity comparison in a query does not emit a bare scalar FILTER. It unfolds to standard SPARQL 1.1 that resolves the stored value’s unit and compares SI magnitudes, guarded by the dimension:

?s :topSpeed ?v .
BIND(datatype(?v) AS ?u)
?u dq:coefficient ?c ; dq:dimension ?d .
FILTER(?d = "L1.T-1" && (xsd:double(str(?v)) * ?c) >= 16.666…)

The target magnitude (16.666…) and dimension ("L1.T-1") are computed at compile time from the comparison literal. Rules unfold the same way for the reasoner, using dedicated dq: comparison predicates (dq:greaterThanOrEqual, …) rather than overloaded math: builtins.

Displaying results

A result value keeps its unit datatype, so a client can reconstruct the unit for display: "45"^^unit:kg renders as 45 kg, "42"^^unit:km.h-1 as 42 km/h. The datatype IRI maps back to a symbol via the definition block’s dq:symbol.

This is representation “L”, the default. A self-describing structured-node form (“S”) that inlines dq:value/dq:coefficient/dq:dimension/dq:symbol on a blank node, needing no definition block, is a documented alternative.


Errors

SituationResult
Unknown unit token (quantity(42 zonks))compile error INVALID_QUANTITY (S007)
Incompatible as conversion (5 km as s)compile error INVALID_QUANTITY (S007)
Wrong dimension on a dimension-typed property (has weight: unit.Mass, value quantity(3 m))compile error DIMENSION_MISMATCH (S008)
Bare number on a dimension-typed property (weight 45)compile error DIMENSION_MISMATCH (S008)
Two different nominal units combined without a cast (2 bunch_of_carrots + 3 cabbages)compile error, incommensurable units
*// between nominal units of different families (2 bunch_of_carrots * 3 apples)compile error, incommensurable units
as <family> on a unit that isn’t a member of that familycompile error INVALID_QUANTITY (S007)
Dimension mismatch in a comparisonnot an error, the row is dropped at run time

The first four are reported by dolfin-analysis and surfaced in the editor. The last is a data-dependent runtime condition, so it is handled by dropping the non-matching row rather than by a diagnostic.