Packages
ex_cldr_units
3.1.2
3.20.5
3.20.4
3.20.3
3.20.2
3.20.1
3.20.0
3.19.2
3.19.1
3.19.0
3.18.1
3.18.0
3.17.2
3.17.1
3.17.0
3.16.5
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.3
3.13.2
3.13.1
3.13.0
3.12.2
3.12.1
3.12.0
3.11.0
3.10.0
3.9.2
3.9.1
3.9.0
3.8.0
3.8.0-rc.2
3.8.0-rc.1
3.8.0-rc.0
3.7.1
3.7.0
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.0
3.4.0-rc.0
3.3.1
3.3.0
3.3.0-rc.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.1
3.0.0
3.0.0-rc.0
2.8.1
2.8.0
2.7.0
2.6.1
2.6.0
2.5.3
2.5.2
2.5.1
2.5.0
2.4.0
2.3.3
2.3.2
retired
2.3.1
2.3.0
2.2.0
2.1.0
2.0.0
1.3.0
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
retired
1.0.1
retired
1.0.0
retired
1.0.0-rc.0
retired
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Unit formatting (volume, area, length, ...), conversion and arithmetic functions based upon the Common Locale Data Repository (CLDR).
Current section
Files
Jump to
Current section
Files
lib/cldr/unit/parser.ex
defmodule Cldr.Unit.Parser do
@moduledoc """
Parse unit strings into composable
unit structures. These structures can
then be used to produced localized output,
or to be converted to another unit of the
same unit category.
"""
alias Cldr.Unit.Conversions
alias Cldr.Unit.Conversion
alias Cldr.Unit.Alias
alias Cldr.Unit.Prefix
alias Cldr.Unit
@doc """
Parses a unit name expressed as a
string and returns the parsed
name or an error.
## Arguments
* `unit_string` is a unit name (such as
"meter") as a `String.t()`
## Returns
* `{:ok, normalized_unit}` or
* `{:error, {exception, reason}}`
## Notes
A normalised unit is a `2-tuple` with
the first element a list of standard units
that are before the first "per" in the
unit name. The second element is a list
of standard units after the first "per"
(if any).
The structure of the standard unit is
`{standard_unit, conversion_to_base_unit}`.
This function is not normally called by
consumers of this library. It is called by
`Cldr.Unit.validate_unit/1` which is the
main public API.
## Example
iex> Cldr.Unit.Parser.parse_unit "kilogram per light year"
{:ok,
{[
{:kilogram,
%Cldr.Unit.Conversion{
base_unit: [:kilogram],
factor: Ratio.new(144115188075855875, 144115188075855872),
offset: 0
}}
],
[
{:light_year,
%Cldr.Unit.Conversion{
base_unit: [:meter],
factor: 9460730000000000,
offset: 0
}}
]}}
"""
@per "_per_"
def parse_unit(unit_string) when is_binary(unit_string) do
unit_string
|> Cldr.Unit.normalize_unit_name
|> String.split(@per, parts: 2)
|> Enum.map(&parse_subunit/1)
|> wrap(:ok)
rescue
e in [Cldr.UnknownUnitError, Cldr.Unit.UnknownBaseUnitError] ->
{:error, {e.__struct__, e.message}}
end
@doc """
Returns the canonical unit name
for a unit
## Arguments
* `unit_string` is any string representing
a unit such as `light_year_per_week`.
## Returns
* `{:ok, canonical_name}` or
* `{:error, {exception, reason}}`
## Examples
iex> Cldr.Unit.Parser.canonical_unit_name "meter"
{:ok, :meter}
iex> Cldr.Unit.Parser.canonical_unit_name "meter meter"
{:ok, :square_meter}
iex> Cldr.Unit.Parser.canonical_unit_name "meter per kilogram"
{:ok, "meter_per_kilogram"}
iex> Cldr.Unit.Parser.canonical_unit_name "meter kilogram"
{:ok, "kilogram_meter"}
iex> Cldr.Unit.Parser.canonical_unit_name "meter kilogram per fluxom"
{:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"fluxom\\""}}
"""
def canonical_unit_name(unit_string) when is_binary(unit_string) do
with {:ok, parsed} <- parse_unit(unit_string) do
{:ok, canonical_unit_name(parsed)}
end
end
def canonical_unit_name({numerator, denominator}) do
to_string(canonical_subunit_name(numerator)) <>
@per <>
to_string(canonical_subunit_name(denominator))
end
def canonical_unit_name(numerator) do
canonical_subunit_name(numerator)
end
@doc """
Returns the canonical unit name
for a unit or raises on error
## Arguments
* `unit_string` is any string representing
a unit such as `light_year_per_week`.
## Returns
* `{:ok, canonical_name}` or
* raises an exception
## Examples
iex> Cldr.Unit.Parser.canonical_unit_name! "meter"
:meter
iex> Cldr.Unit.Parser.canonical_unit_name! "meter meter"
:square_meter
iex> Cldr.Unit.Parser.canonical_unit_name! "meter per kilogram"
"meter_per_kilogram"
iex> Cldr.Unit.Parser.canonical_unit_name! "meter kilogram"
"kilogram_meter"
=> Cldr.Unit.Parser.canonical_unit_name! "meter kilogram per fluxom"
** (CaseClauseError) no case clause matching: {:error,
{Cldr.UnknownUnitError, "Unknown unit was detected at \"fluxom\""}}
"""
def canonical_unit_name!(unit_string) when is_binary(unit_string) do
case canonical_unit_name(unit_string) do
{:ok, unit_name} -> unit_name
{:eror, {exception, reason}} -> raise exception, reason
end
end
@doc """
Returns the canonical base unit name
for a unit.
The base unit is the common unit through which
conversions are passed.
## Arguments
* `unit_string` is any string representing
a unit such as `light_year_per_week`.
## Returns
* `{:ok, canonical_base_unit}` or
* `{:error, {exception, reason}}`
## Examples
iex> Cldr.Unit.Parser.canonical_base_unit "meter"
{:ok, :meter}
iex> Cldr.Unit.Parser.canonical_base_unit "meter meter"
{:ok, :square_meter}
iex> Cldr.Unit.Parser.canonical_base_unit "meter per kilogram"
{:ok, "meter_per_kilogram"}
iex> Cldr.Unit.Parser.canonical_base_unit "yottagram per mile scandinavian"
{:ok, "kilogram_per_meter"}
"""
def canonical_base_unit(unit_string) when is_binary(unit_string) do
with {:ok, parsed} <- parse_unit(unit_string) do
canonical_base_unit(parsed)
end
end
def canonical_base_unit([{_, {numerator, denominator}}]) do
canonical_base_unit({numerator, denominator})
end
def canonical_base_unit({numerator, denominator}) do
[numerator, denominator]
|> Enum.map(&canonical_base_subunit/1)
|> Enum.join(@per)
|> canonical_unit_name
end
def canonical_base_unit(numerator) do
numerator
|> canonical_base_subunit
|> canonical_unit_name
end
defp canonical_subunit_name([{unit_name, _}]) do
unit_name
end
defp canonical_subunit_name([{first, _}, {second, _} | rest]) do
canonical_subunit_name([{to_string(first) <> "_" <> to_string(second), nil} | rest])
end
@doc """
Returns the canonical base unit name
for a unit.
The base unit is the common unit through which
conversions are passed.
## Arguments
* `unit_string` is any string representing
a unit such as `light_year_per_week`.
## Returns
* `canonical_base_unit` or
* raises an exception
## Examples
iex> Cldr.Unit.Parser.canonical_base_unit! "meter"
:meter
iex> Cldr.Unit.Parser.canonical_base_unit! "meter meter"
:square_meter
iex> Cldr.Unit.Parser.canonical_base_unit! "meter per kilogram"
"meter_per_kilogram"
iex> Cldr.Unit.Parser.canonical_base_unit! "yottagram per mile scandinavian"
"kilogram_per_meter"
"""
def canonical_base_unit!(unit_string) when is_binary(unit_string) do
case canonical_base_unit(unit_string) do
{:ok, unit_name} -> unit_name
{:eror, {exception, reason}} -> raise exception, reason
end
end
defp parse_subunit(unit_string) when is_binary(unit_string) do
unit_string
|> String.replace(@per, "")
|> split_into_units
|> expand_power_instances()
|> combine_power_instances()
|> Enum.map(&resolve_base_unit/1)
|> Enum.sort(&unit_sorter/2)
end
# We wrap in a tuple since a nested list can
# create ambiguous processing in other places
defp wrap([numerator, denominator], tag) do
{tag, {numerator, denominator}}
end
defp wrap([numerator], tag) do
{tag, numerator}
end
defp canonical_base_subunit([conversion]) do
extract_base_unit(conversion)
end
defp canonical_base_subunit([head | rest]) do
extract_base_unit(head) <> "_" <> canonical_base_subunit(rest)
end
defp extract_base_unit({_unit_name, [{_, %{base_unit: base_units}}]}) do
base_units
|> Enum.map(&Atom.to_string/1)
|> Enum.join("_")
end
defp extract_base_unit({_unit_name, %{base_unit: base_units}}) do
base_units
|> Enum.map(&Atom.to_string/1)
|> Enum.join("_")
end
defp extract_base_unit({_unit_name, subunits}) when is_list(subunits) do
canonical_base_subunit(subunits)
end
@unit_strings Conversions.conversions()
|> Map.keys()
|> Cldr.Map.stringify_values()
|> Enum.uniq()
|> Enum.sort(fn a, b -> String.length(a) > String.length(b) end)
# In order to tokenize a unit string it needs to be split
# at the boundaries of known units - after we strip
# any SI prefixes and any power (square, cubic) prefixes.
#
# The process is:
#
# 1. Replace any aliases
#
# 2. Ignore "square" and "cubic" prefixes, they
# are just passed through for later use
#
# 3. For each known unit, defined as a key on
# the map returned by `Cldr.Unit.Conversions.conversions/0`,
# sorted in descending order of length so we match
# longest first, generate a function matching the head
# of the string. This will match any unit except those
# with an SI prefix
#
# 4. Match the beginning of the string to an SI prefix and
# then match the remaining string. Reassemble the prefix
# to the base unit before returning.
defp split_into_units("") do
[]
end
for {unit_alias, unit} <- Alias.aliases() do
defp split_into_units(<<unquote(to_string(unit_alias)), rest::binary>>) do
split_into_units(unquote(to_string(unit)) <> rest)
end
end
for unit <- @unit_strings do
defp split_into_units(<<unquote(unit), rest::binary>>) do
[unquote(unit) | split_into_units(rest)]
end
end
for {prefix, _power} <- Prefix.power_units() do
defp split_into_units(<<unquote(prefix) <> "_", rest::binary>>) do
[unquote(prefix) | split_into_units(rest)]
end
end
for {prefix, _scale} <- Prefix.si_factors() do
defp split_into_units(<<unquote(prefix), rest::binary>>) do
[head | rest] = split_into_units(rest)
[unquote(prefix) <> head | rest]
end
end
defp split_into_units(<<"_", rest::binary>>) do
split_into_units(rest)
end
defp split_into_units(other) do
raise Cldr.UnknownUnitError, "Unknown unit was detected at #{inspect(other)}"
end
# In order to correctly identify the units with their
# correct power (square or cubic) any existing power units
# have to be expanded so that later on we can group
# them with any single units of the same name.
defp expand_power_instances([]) do
[]
end
for {prefix, power} <- Prefix.power_units() do
defp expand_power_instances([unquote(prefix), unit | rest]) do
List.duplicate(unit, unquote(power)) ++ expand_power_instances(rest)
end
defp expand_power_instances([<<unquote(prefix) <> "_" <> unit>> | rest]) do
List.duplicate(unit, unquote(power)) ++ expand_power_instances(rest)
end
end
defp expand_power_instances([unit | rest]) do
[unit | expand_power_instances(rest)]
end
# Reassemble the power units by grouping and
# combining with a square or cubic prefix
# if there is more than one instance
defp combine_power_instances(units) do
units
|> Enum.group_by(& &1)
|> Enum.map(fn
{k, v} when length(v) == 1 -> k
{k, v} when length(v) == 2 -> "square_#{k}"
{k, v} when length(v) == 3 -> "cubic_#{k}"
{k, v} -> raise(Cldr.UnknownUnitError,
"Unable to parse more than square and cubic powers. The requested unit " <>
"would require a base unit of #{inspect k} to the power of #{length(v)}")
end)
end
# For each unit, resolve its base unit. First
# ignore any power prefix or any SI unit prefix and then
# look up the base unit. Afterwards take a note of any
# scale or power that need to be abplied to the base unit
# to reflect the power and/or SI unit.
for {prefix, scale} <- Prefix.si_factors() do
defp resolve_base_unit(<<unquote(prefix), base_unit::binary>> = unit) do
with {_, conversion} <- resolve_base_unit(base_unit) do
factor = Ratio.mult(conversion.factor, unquote(Macro.escape(scale)))
{Unit.maybe_translatable_unit(unit), %{conversion | factor: factor}}
else
{:error, {exception, reason}} -> raise(exception, reason)
end
end
end
for {prefix, power} <- Prefix.power_units() do
defp resolve_base_unit(<<unquote(prefix) <> "_", subunit::binary>> = unit) do
with {_, conversion} <- resolve_base_unit(subunit) do
factor = Ratio.pow(conversion.factor, unquote(power))
base_unit = [String.to_atom(unquote(prefix)) | conversion.base_unit]
{Unit.maybe_translatable_unit(unit), %{conversion | base_unit: base_unit, factor: factor}}
else
{:error, {exception, reason}} -> raise(exception, reason)
end
end
end
defp resolve_base_unit(unit) when is_binary(unit) do
with {:ok, conversion} <- Conversions.conversion_for(unit) do
{Unit.maybe_translatable_unit(unit), conversion}
else
{:error, {exception, reason}} -> raise(exception, reason)
end
end
# Units are sorted in the order present in the base units
# list. Within any base unit, SI prefixes are sorted by the
# largest first. Therefore the sort keys may be an integer
# for a simple unit with no prefix or a tuple with the
# integer ranking for the unit and an integer ranking for
# the SI prefix.
defp unit_sorter(a, b) do
case {unit_sort_key(a), unit_sort_key(b)} do
{{key, order_1}, {key, order_2}} -> order_1 < order_2
{{key_1, _order_1}, key_2} when is_integer(key_2) -> key_1 < key_2
{key_1, {key_2, _order_2}} when is_integer(key_1) -> key_1 < key_2
{key_1, key_2} when is_integer(key_1) and is_integer(key_2) -> key_1 < key_2
_other -> true
end
end
for {prefix, _power} <- Prefix.power_units() do
defp unit_sort_key({<<unquote(prefix) <> "_", unit::binary>>, conversion}) do
unit_sort_key({unit, conversion})
end
end
for {prefix, order} <- Prefix.si_sort_order() do
defp unit_sort_key({<<unquote(prefix), unit::binary>>, conversion}) do
{unit_sort_key({unit, conversion}), unquote(order)}
end
end
defp unit_sort_key({_unit, %Conversion{base_unit: [base_unit]}}) do
Map.fetch!(base_units_in_order(), base_unit)
end
defp unit_sort_key({_unit, %Conversion{base_unit: [_prefix, base_unit]}}) do
Map.fetch!(base_units_in_order(), base_unit)
end
@base_units_in_order Cldr.Config.units()
|> Map.get(:base_units)
|> Enum.map(&elem(&1, 1))
|> Enum.with_index()
|> Map.new()
defp base_units_in_order do
@base_units_in_order
end
end