Elex.Units.Catalog (Elex v0.4.2)

View Source

Caller-registered unit categories.

Elex does not ship units. A catalog is what the caller registers with add_category/3 and add_unit/3 (optional conversion and aliases:). Omitting conversion registers "value". Optional aliases: are extra input spellings of a canonical unit. They must be symbol-pattern names, unique catalog-wide, and are not valid as default::

Catalog.add_unit(catalog, :area, "m^2", aliases: ["m2", "sqm"])

Derived add_category may take identity: (a unit formula of the base-hub product) so a named hub such as "N" can name that formula. A matching identity unit must still be registered. identity: is rejected on base categories, and when the formula names a derived category. That formula stores each component category atom. put_units then requires a registered per-1 unit of that nominal dimension.

add_unit/4 takes an explicit conversion string; aliases: defaults to []. derivable: false keeps that unit's category atom in a formula; omitted means derivable and stores no flag. The conversion must be a string (value / 100), not a charlist. A formula-shaped unit name whose components are already registered must use a matching scale (cm at value / 100 means "cm^2" is value / 10000). identity: names the base-hub formula; a matching unit must still be registered (N plus "kg * m | s^2", ha plus "m^2"). add_category!/3 and add_unit!/3 raise ArgumentError instead of returning {:error, reason}.

Summary

Functions

Registers a base or derived category.

Same as add_category/3, but returns the catalog or raises ArgumentError.

Registers a unit with an invertible conversion to the category's conversion-default.

Same as add_unit/3 (optional conversion and aliases:), but returns the catalog or raises ArgumentError.

Resolves a registered unit name or alias to the canonical unit name.

Returns a map of registered category names to their conversion-default unit symbols.

Looks up the category whose dimension vector matches dim.

Looks up the category atom for a registered unit symbol or alias.

Looks up a category's formula as a dimension.

Looks up a registered unit entry by canonical name or alias.

Returns whether name is a base or derived category.

Returns an empty catalog.

Parses a unit formula against this catalog.

Returns true when left and right are the same category for arithmetic.

Returns :ok when every category default: hub is registered on that category and every derived category has an identity unit. An all-base formula needs a unit whose name is the base-hub product. A formula that names a derived category needs a registered per-1 unit whose nominal dimension equals the stored vector. identity: does not substitute for that unit.

Types

category()

@type category() :: %{
  :default => String.t(),
  :additive => boolean(),
  :units => %{optional(String.t()) => unit()},
  optional(:formula) => String.t(),
  optional(:dim) => %{optional(atom()) => integer()},
  optional(:identity) => String.t()
}

t()

@type t() :: %Elex.Units.Catalog{categories: %{optional(atom()) => category()}}

unit()

@type unit() :: %{
  :to_default => String.t(),
  :to_default_ast => term(),
  :from_default_ast => term(),
  optional(:aliases) => [String.t()],
  optional(:inverse) => true,
  optional(:derivable) => false
}

Functions

add_category(catalog, name, opts)

@spec add_category(t(), atom(), keyword()) :: {:ok, t()} | {:error, String.t()}

Registers a base or derived category.

Base and derived categories take default: — the conversion-hub unit (must be registered on that category before put_units/2). default: may not include a denominator coefficient (L | 100 km). Derived categories also take formula: over category names. An all-base formula stores the reduced vector and may take identity: — a unit formula of the base-hub product. A formula that names a derived category stores that category's atom and rejects identity:. A matching identity unit must still be registered before put_units/2: the base-hub product, or a per-1 unit of the nominal dimension when the formula names a derived category. identity: is rejected on base categories. default_result_unit: is rejected; use default:. additive: defaults to true; offset conversions (to_default(0) ≠ 0) and reciprocal conversions (k / value) require additive: false.

Returns

  • {:ok, catalog} - Updated catalog
  • {:error, String.t()} - Registration failed

add_category!(catalog, name, opts)

@spec add_category!(t(), atom(), keyword()) :: t()

Same as add_category/3, but returns the catalog or raises ArgumentError.

add_unit(catalog, category, name, to_default \\ "value", opts \\ [])

@spec add_unit(t(), atom(), String.t(), String.t(), keyword()) ::
  {:ok, t()} | {:error, String.t()}

Registers a unit with an invertible conversion to the category's conversion-default.

The conversion is an Elex expression in value (a string, not a charlist). Omitting it (or passing only aliases:) registers "value". The inverse is derived with Elex.Inverter. A formula unit may not place the same category in both the numerator and the denominator (N * s | s, N * s | hour). A formula-shaped name whose components are already registered must match their combined scale. A denominator coefficient (L | 100 km) expects that scale divided by the coefficient. A second canonical name with the same monomial and the same coefficient is the same unit (L|100km after L | 100 km). A different coefficient is a different unit. An inverse formula (every exponent negated, both dimensions non-empty, no denominator coefficient) is accepted when the category is non-additive and the conversion is a reciprocal (k / value).

aliases: are optional input-only symbol names for this canonical unit. They must match ^[A-Za-z°µμΩΩ][A-Za-z0-9_ΩΩ]*$ and be unique catalog-wide against unit names and other aliases. default: must be a canonical unit name, not an alias.

derivable: false stores the flag on that catalog entry so a formula keeps the category atom (volume for litre). Omitted and derivable: true store no key and expand a derived category to its :dim. Aliases use the canonical entry. The flag is not a field of %Elex.Unit{}.

add_unit!(catalog, category, name, to_default \\ "value", opts \\ [])

@spec add_unit!(t(), atom(), String.t(), String.t(), keyword()) :: t()

Same as add_unit/3 (optional conversion and aliases:), but returns the catalog or raises ArgumentError.

canonical_name(catalog, name)

@spec canonical_name(t(), String.t()) :: {:ok, String.t()} | :error

Resolves a registered unit name or alias to the canonical unit name.

categories(catalog)

@spec categories(t()) :: %{optional(atom()) => String.t()}

Returns a map of registered category names to their conversion-default unit symbols.

category_for_dim(catalog, dim)

@spec category_for_dim(t(), %{optional(atom()) => integer()}) ::
  {:ok, atom()} | :error

Looks up the category whose dimension vector matches dim.

Base categories match %{category => 1}. Derived categories match the stored :dim from their formula.

category_for_unit(catalog, name)

@spec category_for_unit(t(), String.t()) :: {:ok, atom()} | :error

Looks up the category atom for a registered unit symbol or alias.

Returns

  • {:ok, category} - The category the unit belongs to
  • :error - The symbol is not registered

dimension(catalog, category)

@spec dimension(t(), atom()) :: {:ok, Elex.Dimension.t()} | :error

Looks up a category's formula as a dimension.

Base categories are %{category => 1}. Derived categories use the stored :dim from their formula.

fetch_unit(catalog, name)

@spec fetch_unit(t(), String.t()) :: {:ok, unit()} | :error

Looks up a registered unit entry by canonical name or alias.

Returns

  • {:ok, unit} - The unit with conversion ASTs
  • :error - The name is not a registered unit or alias

kind(catalog, name)

@spec kind(t(), atom()) :: {:ok, :base} | {:ok, :derived} | :error

Returns whether name is a base or derived category.

Returns

  • {:ok, :base} - The category has no formula:
  • {:ok, :derived} - The category was registered with formula:
  • :error - The name is not a registered category

new()

@spec new() :: t()

Returns an empty catalog.

parse_formula(catalog, source)

@spec parse_formula(t(), String.t()) ::
  {:ok, Elex.Units.Formula.monomial()}
  | {:ok, Elex.Units.Formula.monomial(), pos_integer()}
  | {:error, String.t()}

Parses a unit formula against this catalog.

Rejects an invalid formula, unknown symbols, a formula that places the same category in both the numerator and the denominator (after expanding derived units), and a formula that cancels to an empty unit.

per of 1 stays {:ok, monomial}, including a compound formula of known symbols that is not one registered name (m | s). A denominator coefficient succeeds as {:ok, monomial, per} only when a registered unit has that monomial and that per. Spacing is not significant (L|100km matches L | 100 km). When per is greater than 1 and nothing matches, the error is unknown unit with the caller's spelling.

same_category_dim?(catalog, left, right)

@spec same_category_dim?(
  t(),
  %{optional(atom()) => integer()},
  %{optional(atom()) => integer()}
) :: boolean()

Returns true when left and right are the same category for arithmetic.

Each side is reduced with Unit.reduce_nominal_power/2. The dimensions match when those reduced powers are equal, including when the unreduced vectors differ (%{volume: 2} and %{length: 6}). A mixed vector such as %{volume: 1, length: -1} is left unchanged.

validate(catalog)

@spec validate(t()) :: :ok | {:error, String.t()}

Returns :ok when every category default: hub is registered on that category and every derived category has an identity unit. An all-base formula needs a unit whose name is the base-hub product. A formula that names a derived category needs a registered per-1 unit whose nominal dimension equals the stored vector. identity: does not substitute for that unit.