Units named by their UCUM codes (spec/03 §1–§7, D-138, D-155).
A unit is named by its code in the Unified Code for Units of Measure — UCUM,
https://ucum.org/ucum, Regenstrief Institute — the case-sensitive, unambiguous codes
OpenTelemetry's semantic conventions use: s, ms, By, kBy, MiBy, %, Cel,
kW.h, {request}/s, [lb_av], mm[Hg]. One code per unit is the point: a source that
says "s" and one that says "sec" must not draw two labels, so a code is checked,
never guessed at. Every atom and prefix here is UCUM's, with UCUM's definition, checked
against the UCUM essence table (ucum-essence.xml, version 2.2, 2024-06-17).
This module reads the subset of UCUM's grammar (§2.1 of the specification, Syntax
rules) that measurement data uses — products ., quotients / (left-associative,
§2.1.3), signed integer exponents, integer factors and {annotation} blocks (§2.1.5)
— over a fixed table of prefixes (da h k M G T d c m u n p Ki Mi Gi Ti) and atoms:
time, information, ratios, temperature, SI and customary length, area, volume and mass,
speed, power, energy, pressure, angle, charge, current, potential, illuminance, the
mole and the logarithmic bel. A symbol is matched whole before it is split into a
prefix and an atom (§2.1.4), so min is the minute, and a prefix is refused on an atom
UCUM does not call metric (kmin, k[in_i]). Parentheses and UCUM's other atoms are
not read; the table grows by proposal.
A code has a dimension and an exact factor to its base (the second, the byte, the
meter, the gram, the kelvin, the radian, the coulomb): a rational, times a power of π
for a code holding the degree. So values scale between two codes of one dimension —
1,500 ms is 1.5 s — and convert exactly, value × scale + offset (conversion/2).
Cel and [degF] are UCUM's affine special units, scales with an offset: they have no
factor and convert by their offsets. B and B[SPL] are its logarithmic ones; each has a
level dimension of its own here, so a decibel scales to a bel and never to a linear
unit. A currency never converts: an exchange rate is data.
A person types symbols, not codes: parse_alias/1 reads them, and refuses an ambiguous
one (KB, gal) naming the codes it could mean.
Examples
iex> Macaw.Data.Unit.valid?("kW.h")
true
iex> Macaw.Data.Unit.parse("sec")
{:error, {:unknown_atom, "sec"}}
iex> Macaw.Data.Unit.symbol("By/s")
"B/s"
iex> Macaw.Data.Unit.convert(1500, "ms", "s")
1.5
iex> Macaw.Data.Unit.parse_alias("mph")
{:ok, "[mi_i]/h"}
iex> Macaw.Data.Unit.from_telemetry(:millisecond)
{:ok, "ms"}
Summary
Types
Why parse_alias/1 refuses a symbol.
A UCUM code, such as ms or {request}/s.
One component of a code: a prefixed atom, an integer factor or an annotation standing alone, with its exponent — negated when the component divides.
Why conversion/2 refuses a pair of codes.
A base dimension and its exponent; %{} is dimensionless.
An exact factor: a rational, or {numerator, denominator, k} — the rational times
π to the non-zero power k, for a code holding the degree.
How a code's values relate to its base: by a factor, an offset or a logarithm.
An exact rational factor, {numerator, denominator}.
Why a string is not a code, or a symbol not an alias.
Functions
The exact conversion from one code to another (spec/03 §6, D-155):
{:ok, {scale, offset}}, so that a value in from is value × scale + offset in to
— scale an exact factor ({n, d}, or {n, d, k} for n/d·πᵏ), offset an exact
signed rational {n, d} — or why the two do not convert.
A value in from written in to (spec/03 §6): value × scale + offset of
conversion/2, as a float — the scale and offset exact, and the arithmetic rounded
once for an integer value. Raises ArgumentError naming the reason for a pair
conversion/2 refuses.
The ISO 4217 code a code is in when it is a currency alone — {iso4217:USD} is
"USD" — else nil: a rate ({iso4217:USD}/h), any other unit, or a string that is not
a code (spec/03 §4).
The dimension of a code: each base dimension's exponent, %{} when dimensionless.
Raises ArgumentError for a string that is not a code.
The exact factor of a code to its base: {numerator, denominator}, or
{numerator, denominator, k} — that rational times πᵏ — for a code holding the degree,
or nil for a code holding an affine unit (Cel, [degF]). Raises ArgumentError
for a string that is not a code.
A refusal of parse/1, parse_alias/1 or conversion/2 as a line a person reads.
The code of a Telemetry.Metrics unit — its unit atom, or the to of a {from, to}
conversion — with no dependency on telemetry_metrics (spec/03 §7). :native has
no fixed size until a definition converts it, and an atom not in the table is refused
rather than guessed at.
How a code's values relate to its base (spec/03 §2): :affine for a code holding
Cel or [degF], :logarithmic for one holding B or B[SPL], else :linear.
Raises ArgumentError for a string that is not a code.
An ISO 4217 code's minor units — the decimals an amount in it is written to — or nil
for a code with none (XAU). Raises ArgumentError for a code not in the table.
The components of a code, or why it is not one (spec/03 §1).
The code a person means by a symbol they typed (spec/03 §3, D-155): °C, lb,
mph, kWh, KiB. The alias table is read first, so a person's B is the byte and
not the bel; a symbol the table does not hold is read by parse/1, a code being its
own alias. An ambiguous symbol is refused with the codes it could mean, never guessed.
Surrounding whitespace is ignored.
The display symbol of a code (spec/03 §5): B for By, µs for us, °C for
Cel, kW·h for kW.h, m² for m2; the factor 1 and annotations are dropped, so
1 and {request} are "".
Whether a string is a code parse/1 reads.
Types
@type alias_reason() :: {:ambiguous, String.t(), [{code(), String.t()}, ...]} | {:unknown_alias, String.t()}
Why parse_alias/1 refuses a symbol.
@type code() :: String.t()
A UCUM code, such as ms or {request}/s.
@type component() :: %{ prefix: String.t() | nil, atom: String.t() | nil, factor: pos_integer() | nil, annotation: String.t() | nil, exponent: integer() }
One component of a code: a prefixed atom, an integer factor or an annotation standing alone, with its exponent — negated when the component divides.
@type conversion_reason() :: {:dimension | :logarithmic | :currency, code(), code()} | {:affine_in_term, code()}
Why conversion/2 refuses a pair of codes.
@type dimension() :: %{ optional( :time | :information | :length | :mass | :temperature | :angle | :charge | :luminous_intensity | :level | :sound_pressure_level | {:currency, String.t()} ) => integer() }
A base dimension and its exponent; %{} is dimensionless.
@type exact() :: ratio() | {pos_integer(), pos_integer(), integer()}
An exact factor: a rational, or {numerator, denominator, k} — the rational times
π to the non-zero power k, for a code holding the degree.
@type kind() :: :linear | :affine | :logarithmic
How a code's values relate to its base: by a factor, an offset or a logarithm.
@type ratio() :: {pos_integer(), pos_integer()}
An exact rational factor, {numerator, denominator}.
@type reason() :: :empty | {:syntax, String.t()} | {:unknown_atom, String.t()} | {:not_metric, String.t(), String.t()} | {:unknown_currency, String.t()}
Why a string is not a code, or a symbol not an alias.
Functions
@spec conversion(code(), code()) :: {:ok, {exact(), {integer(), pos_integer()}}} | {:error, reason() | conversion_reason()}
The exact conversion from one code to another (spec/03 §6, D-155):
{:ok, {scale, offset}}, so that a value in from is value × scale + offset in to
— scale an exact factor ({n, d}, or {n, d, k} for n/d·πᵏ), offset an exact
signed rational {n, d} — or why the two do not convert.
Two codes of one dimension convert by their factors. The affine units Cel and
[degF] convert with their offsets, standing alone. A logarithmic unit converts only
to its own level. A currency never converts. A refusal is {:dimension, from, to},
{:logarithmic, from, to}, {:currency, from, to}, {:affine_in_term, code}, or the
reason parse/1 refuses a code.
Examples
iex> Macaw.Data.Unit.conversion("Cel", "[degF]")
{:ok, {{9, 5}, {32, 1}}}
iex> Macaw.Data.Unit.conversion("[lb_av]", "kg")
{:ok, {{45359237, 100000000}, {0, 1}}}
iex> Macaw.Data.Unit.conversion("dB[SPL]", "Pa")
{:error, {:logarithmic, "dB[SPL]", "Pa"}}
A value in from written in to (spec/03 §6): value × scale + offset of
conversion/2, as a float — the scale and offset exact, and the arithmetic rounded
once for an integer value. Raises ArgumentError naming the reason for a pair
conversion/2 refuses.
Examples
iex> Macaw.Data.Unit.convert(2_500_000, "By", "MBy")
2.5
iex> Macaw.Data.Unit.convert(100, "Cel", "[degF]")
212.0
iex> Macaw.Data.Unit.convert(30, "dB", "B")
3.0
The ISO 4217 code a code is in when it is a currency alone — {iso4217:USD} is
"USD" — else nil: a rate ({iso4217:USD}/h), any other unit, or a string that is not
a code (spec/03 §4).
Examples
iex> Macaw.Data.Unit.currency("{iso4217:EUR}")
"EUR"
iex> Macaw.Data.Unit.currency("{iso4217:EUR}/kW/h")
nil
The dimension of a code: each base dimension's exponent, %{} when dimensionless.
Raises ArgumentError for a string that is not a code.
Examples
iex> Macaw.Data.Unit.dimension("[psi]")
%{mass: 1, length: -1, time: -2}
iex> Macaw.Data.Unit.dimension("dB[SPL]")
%{sound_pressure_level: 1}
The exact factor of a code to its base: {numerator, denominator}, or
{numerator, denominator, k} — that rational times πᵏ — for a code holding the degree,
or nil for a code holding an affine unit (Cel, [degF]). Raises ArgumentError
for a string that is not a code.
Examples
iex> Macaw.Data.Unit.factor("[ft_i]")
{381, 1250}
iex> Macaw.Data.Unit.factor("deg")
{1, 180, 1}
@spec format_error(reason() | alias_reason() | conversion_reason()) :: String.t()
A refusal of parse/1, parse_alias/1 or conversion/2 as a line a person reads.
Examples
iex> Macaw.Data.Unit.format_error({:ambiguous, "gal", [{"[gal_us]", "US gallon"}, {"[gal_br]", "imperial gallon"}]})
~s|"gal" is ambiguous: [gal_us] (US gallon) or [gal_br] (imperial gallon)|
@spec from_telemetry(atom() | {atom(), atom()}) :: {:ok, code()} | {:error, :native | {:unknown, term()}}
The code of a Telemetry.Metrics unit — its unit atom, or the to of a {from, to}
conversion — with no dependency on telemetry_metrics (spec/03 §7). :native has
no fixed size until a definition converts it, and an atom not in the table is refused
rather than guessed at.
Examples
iex> Macaw.Data.Unit.from_telemetry({:native, :microsecond})
{:ok, "us"}
iex> Macaw.Data.Unit.from_telemetry(:native)
{:error, :native}
How a code's values relate to its base (spec/03 §2): :affine for a code holding
Cel or [degF], :logarithmic for one holding B or B[SPL], else :linear.
Raises ArgumentError for a string that is not a code.
Examples
iex> Macaw.Data.Unit.kind("dB")
:logarithmic
@spec minor_units(String.t()) :: non_neg_integer() | nil
An ISO 4217 code's minor units — the decimals an amount in it is written to — or nil
for a code with none (XAU). Raises ArgumentError for a code not in the table.
Examples
iex> Enum.map(~w(USD JPY BHD XAU), &Macaw.Data.Unit.minor_units/1)
[2, 0, 3, nil]
The components of a code, or why it is not one (spec/03 §1).
Examples
iex> Macaw.Data.Unit.parse("km/h")
{:ok,
[
%{prefix: "k", atom: "m", factor: nil, annotation: nil, exponent: 1},
%{prefix: nil, atom: "h", factor: nil, annotation: nil, exponent: -1}
]}
iex> Macaw.Data.Unit.parse("kmin")
{:error, {:not_metric, "k", "min"}}
@spec parse_alias(String.t()) :: {:ok, code()} | {:error, alias_reason()}
The code a person means by a symbol they typed (spec/03 §3, D-155): °C, lb,
mph, kWh, KiB. The alias table is read first, so a person's B is the byte and
not the bel; a symbol the table does not hold is read by parse/1, a code being its
own alias. An ambiguous symbol is refused with the codes it could mean, never guessed.
Surrounding whitespace is ignored.
Examples
iex> Macaw.Data.Unit.parse_alias("°F")
{:ok, "[degF]"}
iex> Macaw.Data.Unit.parse_alias("KB")
{:error, {:ambiguous, "KB", [{"kBy", "1,000 bytes"}, {"KiBy", "1,024 bytes"}]}}
iex> Macaw.Data.Unit.parse_alias("kPa")
{:ok, "kPa"}
The display symbol of a code (spec/03 §5): B for By, µs for us, °C for
Cel, kW·h for kW.h, m² for m2; the factor 1 and annotations are dropped, so
1 and {request} are "".
Options
:annotations- write an annotation standing alone as its text ({request}/s→request/s), defaultfalse
Examples
iex> Macaw.Data.Unit.symbol("us")
"µs"
iex> Macaw.Data.Unit.symbol("{request}/s", annotations: true)
"request/s"
Whether a string is a code parse/1 reads.