Visualize.Unit (Visualize v0.3.0)

Copy Markdown View Source

Units named by their UCUM codes (spec/05 §4.1, D-138).

A unit is named by its code in the Unified Code for Units of Measure — UCUM, https://ucum.org/ucum, version 2.1, Regenstrief Institute — the case-sensitive, unambiguous codes OpenTelemetry's semantic conventions use: s, ms, By, kBy, MiBy, %, Cel, kW.h, {request}/s. 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.

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 (k M G m u n Ki Mi Gi) and atoms (s min h d By bit % Hz Cel [degF] K m g W J Pa). 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). 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 metre, the gram, the kelvin), so values scale between two codes of one dimension — 1,500 ms is 1.5 s. Cel and [degF] are UCUM's special units, scales with an offset, and have no factor: conversion between unit systems is out of scope.

Examples

iex> Visualize.Unit.valid?("kW.h")
true

iex> Visualize.Unit.parse("sec")
{:error, {:unknown_atom, "sec"}}

iex> Visualize.Unit.symbol("By/s")
"B/s"

iex> Visualize.Unit.convert(1500, "ms", "s")
1.5

iex> Visualize.Unit.from_telemetry(:millisecond)
{:ok, "ms"}

Summary

Types

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.

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

An exact factor, {numerator, denominator}.

Why a string is not a code.

Functions

A value in from written in to: value × factor(from) / factor(to), for two codes of one dimension that both have a factor. Raises ArgumentError otherwise — between unit systems (Cel and [degF]) above all, which is out of scope.

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 nil for a code holding a special unit (Cel, [degF]). Raises ArgumentError for a string that is not a code.

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/05 §4.1). :native has no fixed size until a definition converts it, and an atom not in the table is refused rather than guessed at.

The components of a code, or why it is not one (spec/05 §4.4).

The display symbol of a code (spec/05 §4.1): 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

code()

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

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

component()

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

dimension()

@type dimension() :: %{
  optional(:time | :information | :length | :mass | :temperature) => integer()
}

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

ratio()

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

An exact factor, {numerator, denominator}.

reason()

@type reason() ::
  :empty
  | {:syntax, String.t()}
  | {:unknown_atom, String.t()}
  | {:not_metric, String.t(), String.t()}

Why a string is not a code.

Functions

convert(value, from, to)

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

A value in from written in to: value × factor(from) / factor(to), for two codes of one dimension that both have a factor. Raises ArgumentError otherwise — between unit systems (Cel and [degF]) above all, which is out of scope.

Examples

iex> Visualize.Unit.convert(2_500_000, "By", "MBy")
2.5

dimension(code)

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

factor(code)

@spec factor(code()) :: ratio() | nil

The exact factor of a code to its base, {numerator, denominator}, or nil for a code holding a special unit (Cel, [degF]). Raises ArgumentError for a string that is not a code.

from_telemetry(unit)

@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/05 §4.1). :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> Visualize.Unit.from_telemetry({:native, :microsecond})
{:ok, "us"}

iex> Visualize.Unit.from_telemetry(:native)
{:error, :native}

parse(code)

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

The components of a code, or why it is not one (spec/05 §4.4).

Examples

iex> Visualize.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> Visualize.Unit.parse("kmin")
{:error, {:not_metric, "k", "min"}}

symbol(code, opts \\ [])

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

The display symbol of a code (spec/05 §4.1): 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> Visualize.Unit.symbol("us")
"µs"

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

valid?(code)

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

Whether a string is a code parse/1 reads.