# `Macaw.Data.Unit`
[🔗](https://github.com/dcoai/macaw_data/blob/v0.5.3/lib/macaw/data/unit.ex#L1)

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"}

# `alias_reason`

```elixir
@type alias_reason() ::
  {:ambiguous, String.t(), [{code(), String.t()}, ...]}
  | {:unknown_alias, String.t()}
```

Why `parse_alias/1` refuses a symbol.

# `code`

```elixir
@type code() :: String.t()
```

A UCUM code, such as `ms` or `{request}/s`.

# `component`

```elixir
@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.

# `conversion_reason`

```elixir
@type conversion_reason() ::
  {:dimension | :logarithmic | :currency, code(), code()}
  | {:affine_in_term, code()}
```

Why `conversion/2` refuses a pair of codes.

# `dimension`

```elixir
@type dimension() :: %{
  optional(
    :time
    | :information
    | :length
    | :mass
    | :temperature
    | :angle
    | :charge
    | :luminous_intensity
    | :level
    | :sound_pressure_level
    | {:currency, String.t()}
  ) =&gt; integer()
}
```

A base dimension and its exponent; `%{}` is dimensionless.

# `exact`

```elixir
@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.

# `kind`

```elixir
@type kind() :: :linear | :affine | :logarithmic
```

How a code's values relate to its base: by a factor, an offset or a logarithm.

# `ratio`

```elixir
@type ratio() :: {pos_integer(), pos_integer()}
```

An exact rational factor, `{numerator, denominator}`.

# `reason`

```elixir
@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.

# `conversion`

```elixir
@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"}}

# `convert`

```elixir
@spec convert(number(), code(), code()) :: float()
```

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

# `currency`

```elixir
@spec currency(term()) :: String.t() | nil
```

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

# `dimension`

```elixir
@spec dimension(code()) :: dimension()
```

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}

# `factor`

```elixir
@spec factor(code()) :: exact() | nil
```

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}

# `format_error`

```elixir
@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)|

# `from_telemetry`

```elixir
@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}

# `kind`

```elixir
@spec kind(code()) :: kind()
```

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

# `minor_units`

```elixir
@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]

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, [component(), ...]} | {:error, reason()}
```

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"}}

# `parse_alias`

```elixir
@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"}

# `symbol`

```elixir
@spec symbol(code(), keyword()) :: String.t()
```

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`), default `false`

## Examples

    iex> Macaw.Data.Unit.symbol("us")
    "µs"

    iex> Macaw.Data.Unit.symbol("{request}/s", annotations: true)
    "request/s"

# `valid?`

```elixir
@spec valid?(term()) :: boolean()
```

Whether a string is a code `parse/1` reads.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
