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.ex
defmodule Cldr.Unit do
@moduledoc """
Supports the CLDR Units definitions which provide for the localization of many
unit types.
The primary public API defines:
* `Cldr.Unit.to_string/3` which, given a unit or unit list will
output a localized string
* `Cldr.Unit.known_units/0` identifies the available units for localization
* `Cldr.Unit.{add, sub, mult, div}/2` to support basic unit mathematics between
units of compatible type (like length or volume)
* `Cldr.Unit.compare/2` to compare one unit to another unit as long as they
are convertable.
* `Cldr.Unit.convert/2` to convert one unit to another unit as long as they
are convertable.
* `Cldr.Unit.localize/3` will convert a unit into the units preferred for a
given locale and usage
* `Cldr.Unit.preferred_units/3` which, for a given unit and locale,
will return a list of preferred units that can be applied to
`Cldr.Unit.decompose/2`
* `Cldr.Unit.decompose/2` to take a unit and return a list of units decomposed
by a list of smaller units.
"""
alias Cldr.Unit
alias Cldr.{Locale, LanguageTag, Substitution}
alias Cldr.Unit.{Math, Alias, Parser, Conversion, Conversions, Preference, Prefix}
@enforce_keys [:unit, :value, :base_conversion, :usage, :format_options]
defstruct unit: nil,
value: 0,
base_conversion: [],
usage: :default,
format_options: []
@type unit :: atom() | String.t()
@type usage :: atom()
@type style :: atom()
@type value :: Cldr.Math.number_or_decimal() | Ratio.t()
@type conversion :: Conversion.t() | {[Conversion.t(), ...], [Conversion.t(), ...]} | list()
@type locale :: Locale.locale_name() | LanguageTag.t()
@type t :: %__MODULE__{
unit: unit(),
value: value(),
base_conversion: conversion(),
usage: usage(),
format_options: []
}
@default_style :long
@styles [:long, :short, :narrow]
defdelegate convert(unit_1, to_unit), to: Conversion
defdelegate convert!(unit_1, to_unit), to: Conversion
defdelegate preferred_units(unit, backend, options), to: Preference
defdelegate preferred_units!(unit, backend, options), to: Preference
defdelegate add(unit_1, unit_2), to: Math
defdelegate sub(unit_1, unit_2), to: Math
defdelegate mult(unit_1, unit_2), to: Math
defdelegate div(unit_1, unit_2), to: Math
defdelegate add!(unit_1, unit_2), to: Math
defdelegate sub!(unit_1, unit_2), to: Math
defdelegate mult!(unit_1, unit_2), to: Math
defdelegate div!(unit_1, unit_2), to: Math
defdelegate round(unit, places, mode), to: Math
defdelegate round(unit, places), to: Math
defdelegate round(unit), to: Math
defdelegate compare(unit_1, unit_2), to: Math
@app_name Cldr.Config.app_name()
@data_dir [:code.priv_dir(@app_name), "/cldr/locales"] |> :erlang.iolist_to_binary()
@config %{data_dir: @data_dir, locales: ["en"], default_locale: "en"}
@unit_tree "en"
|> Cldr.Config.get_locale(@config)
|> Map.get(:units)
|> Map.get(:short)
|> Enum.map(fn {k, v} -> {k, Map.keys(v)} end)
|> Map.new()
@doc """
Returns the known units.
Known units means units that can
be localised directly.
## Example
=> Cldr.Unit.known_units
[:acre, :acre_foot, :ampere, :arc_minute, :arc_second, :astronomical_unit, :bit,
:bushel, :byte, :calorie, :carat, :celsius, :centiliter, :centimeter, :century,
:cubic_centimeter, :cubic_foot, :cubic_inch, :cubic_kilometer, :cubic_meter,
:cubic_mile, :cubic_yard, :cup, :cup_metric, :day, :deciliter, :decimeter,
:degree, :fahrenheit, :fathom, :fluid_ounce, :foodcalorie, :foot, :furlong,
:g_force, :gallon, :gallon_imperial, :generic, :gigabit, :gigabyte, :gigahertz,
:gigawatt, :gram, :hectare, :hectoliter, :hectopascal, :hertz, :horsepower,
:hour, :inch, ...]
"""
@translatable_units @unit_tree
|> Map.delete(:compound)
|> Map.values()
|> List.flatten()
@spec known_units :: [atom, ...]
def known_units do
@translatable_units
end
@deprecated "Use Cldr.Unit.known_units/0"
def units, do: known_units()
@doc """
Returns a new `Unit.t` struct.
## Arguments
* `value` is any float, integer, `Ratio` or `Decimal`
* `unit` is any unit returned by `Cldr.Unit.known_units/0`
* `options` is Keyword list of options. The default
is `[]`
## Options
* `:use` is the intended use of the unit. This
is used during localization to convert the unit
to that appropriate for the unit category,
usage, target territory and unit value. The `:use`
must be known for the unit's category. See
`Cldr.Unit` for more information.
## Returns
* `unit` or
* `{:error, {exception, message}}`
## Examples
iex> Cldr.Unit.new(23, :gallon)
{:ok, Cldr.Unit.new!(:gallon, 23)}
iex> Cldr.Unit.new(:gallon, 23)
{:ok, Cldr.Unit.new!(:gallon, 23)}
iex> Cldr.Unit.new(14, :gadzoots)
{:error, {Cldr.UnknownUnitError,
"Unknown unit was detected at \\"gadzoots\\""}}
"""
@spec new(unit() | value(), value() | unit(), Keyword.t()) ::
{:ok, t()} | {:error, {module(), String.t()}}
def new(value, unit, options \\ [])
def new(value, unit, options) when is_number(value) do
create_unit(value, unit, options)
end
def new(unit, value, options) when is_number(value) do
new(value, unit, options)
end
def new(%Decimal{} = value, unit, options) do
create_unit(value, unit, options)
end
def new(unit, %Decimal{} = value, options) do
new(value, unit, options)
end
def new(%Ratio{} = value, unit, options) do
create_unit(value, unit, options)
end
def new(unit, %Ratio{} = value, options) do
new(value, unit, options)
end
@default_use :default
@default_format_options []
defp create_unit(value, unit, options) do
usage = Keyword.get(options, :usage, @default_use)
format_options = Keyword.get(options, :format_options, @default_format_options)
with {:ok, unit, base_conversion} <- validate_unit(unit),
{:ok, usage} <- validate_usage(unit, usage) do
unit = %Unit{
unit: unit,
value: value,
base_conversion: base_conversion,
usage: usage,
format_options: format_options
}
{:ok, unit}
end
end
defp validate_usage(unit, usage) do
with {:ok, category} <- unit_category(unit) do
validate_category_usage(category, usage)
end
end
defp validate_category_usage(:substance_amount, _) do
{:ok, nil}
end
@default_category_usage [@default_use]
defp validate_category_usage(category, usage) do
usage_list = Map.get(unit_category_usage(), category, @default_category_usage)
if usage in usage_list do
{:ok, usage}
else
{:error, unknown_usage_error(category, usage)}
end
end
@doc """
Returns a new `Unit.t` struct or raises on error.
## Arguments
* `value` is any float, integer or `Decimal`
* `unit` is any unit returned by `Cldr.Unit.known_units/0`
## Returns
* `unit` or
* raises an exception
## Examples
iex> Cldr.Unit.new! 23, :gallon
#Cldr.Unit<:gallon, 23>
Cldr.Unit.new! 14, :gadzoots
** (Cldr.UnknownUnitError) The unit :gadzoots is not known.
(ex_cldr_units) lib/cldr/unit.ex:57: Cldr.Unit.new!/2
"""
@spec new!(unit() | value(), value() | unit()) :: t() | no_return()
def new!(unit, value, options \\ []) do
case new(unit, value, options) do
{:ok, unit} -> unit
{:error, {exception, message}} -> raise exception, message
end
end
@doc """
Returns a boolean indicating if two units are
of the same unit category.
## Arguments
* `unit_1` and `unit_2` are any units returned by
`Cldr.Unit.new/2` or a valid unit name.
## Returns
* `true` or `false`
## Examples
iex> Cldr.Unit.compatible? :foot, :meter
true
iex> Cldr.Unit.compatible? Cldr.Unit.new!(:foot, 23), :meter
true
iex> Cldr.Unit.compatible? :foot, :liter
false
iex> Cldr.Unit.compatible? "light_year_per_second", "meter_per_gallon"
false
"""
@spec compatible?(unit(), unit()) :: boolean
def compatible?(unit_1, unit_2) do
with {:ok, _unit_1, conversion_1} <- validate_unit(unit_1),
{:ok, _unit_2, conversion_2} <- validate_unit(unit_2),
{:ok, base_unit_1} <- base_unit(conversion_1),
{:ok, base_unit_2} <- base_unit(conversion_2) do
Kernel.to_string(base_unit_1) == Kernel.to_string(base_unit_2)
else
_ -> false
end
end
@doc """
Formats a number into a string according to a unit definition
for the current process's locale and backend.
The curent process's locale is set with
`Cldr.put_locale/1`.
See `Cldr.Unit.to_string/3` for full details.
"""
@spec to_string(list_or_number :: value | t() | [t()]) ::
{:ok, String.t()} | {:error, {atom, binary}}
def to_string(unit) do
locale = Cldr.get_locale()
backend = locale.backend
to_string(unit, backend, locale: locale)
end
@doc """
Formats a number into a string according to a unit definition for a locale.
During processing any `:format_options` of a `Unit.t()` are merged with
`options` with `options` taking precedence.
## Arguments
* `list_or_number` is any number (integer, float or Decimal) or a
`Cldr.Unit.t()` struct or a list of `Cldr.Unit.t()` structs
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
* `options` is a keyword list of options.
## Options
* `:unit` is any unit returned by `Cldr.Unit.known_units/0`. Ignored if
the number to be formatted is a `Cldr.Unit.t()` struct
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/1`
or a `Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`
* `:style` is one of those returned by `Cldr.Unit.styles`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`
* `:list_options` is a keyword list of options for formatting a list
which is passed through to `Cldr.List.to_string/3`. This is only
applicable when formatting a list of units.
* Any other options are passed to `Cldr.Number.to_string/2`
which is used to format the `number`
## Returns
* `{:ok, formatted_string}` or
* `{:error, {exception, message}}`
## Examples
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
{:ok, "123 gallons"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr
{:ok, "1 gallon"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "af"
{:ok, "1 gelling"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "bs"
{:ok, "1 galon"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 1234), MyApp.Cldr, format: :long
{:ok, "1 thousand gallons"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:gallon, 1234), MyApp.Cldr, format: :short
{:ok, "1K gallons"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:megahertz, 1234), MyApp.Cldr
{:ok, "1,234 megahertz"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(:megahertz, 1234), MyApp.Cldr, style: :narrow
{:ok, "1,234MHz"}
iex> unit = Cldr.Unit.new!(123, :foot)
iex> Cldr.Unit.to_string unit, MyApp.Cldr
{:ok, "123 feet"}
iex> Cldr.Unit.to_string 123, MyApp.Cldr, unit: :foot
{:ok, "123 feet"}
iex> Cldr.Unit.to_string Decimal.new(123), MyApp.Cldr, unit: :foot
{:ok, "123 feet"}
iex> Cldr.Unit.to_string 123, MyApp.Cldr, unit: :megabyte, locale: "en", style: :unknown
{:error, {Cldr.UnknownFormatError, "The unit style :unknown is not known."}}
"""
@spec to_string(value | t() | list(t()), Cldr.backend() | Keyword.t(), Keyword.t()) ::
{:ok, String.t()} | {:error, {atom, binary}}
def to_string(list_or_unit, backend, options \\ [])
# Options but no backend
def to_string(list_or_unit, options, []) when is_list(options) do
locale = Cldr.get_locale()
to_string(list_or_unit, locale.backend, options)
end
# It's a list of units so we format each of them
# and combine the list
def to_string(unit_list, backend, options) when is_list(unit_list) do
with {locale, _style, options} <- normalize_options(backend, options),
{:ok, locale} <- backend.validate_locale(locale) do
options =
options
|> Keyword.put(:locale, locale)
list_options =
options
|> Keyword.get(:list_options, [])
|> Keyword.put(:locale, locale)
unit_list
|> Enum.map(&to_string!(&1, backend, options))
|> Cldr.List.to_string(backend, list_options)
end
end
# It's a number, not a unit struct
def to_string(number, backend, options) when is_number(number) do
with {:ok, unit} <- new(options[:unit], number) do
to_string(unit, backend, options)
end
end
def to_string(%Decimal{} = number, backend, options) do
with {:ok, unit} <- new(options[:unit], number) do
to_string(unit, backend, options)
end
end
# Now we have a unit, a backend and some options but ratio
# values need to be converted to floats
def to_string(%Unit{value: %Ratio{}} = unit, backend, options) when is_list(options) do
unit = ratio_to_float(unit)
to_string(unit, backend, options)
end
def to_string(%Unit{} = unit, backend, options) when is_list(options) do
with {:ok, list} <- to_iolist(unit, backend, options) do
list
|> :erlang.iolist_to_binary()
|> String.replace(~r/([\s])+/, "\\1")
|> wrap(:ok)
end
end
@doc """
Formats a number into a iolist according to a unit definition
for the current process's locale and backend.
The curent process's locale is set with
`Cldr.put_locale/1`.
See `Cldr.Unit.to_iolist/3` for full details.
"""
@spec to_iolist(list_or_number :: value | t() | [t()]) ::
{:ok, String.t()} | {:error, {atom, binary}}
def to_iolist(unit) do
locale = Cldr.get_locale()
backend = locale.backend
to_iolist(unit, backend, locale: locale)
end
@doc """
Formats a number into an `iolist` according to a unit definition
for a locale.
During processing any `:format_options` of a `Unit.t()` are merged with
`options` with `options` taking precedence.
## Arguments
* `list_or_number` is any number (integer, float or Decimal) or a
`Cldr.Unit.t()` struct or a list of `Cldr.Unit.t()` structs
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
* `options` is a keyword list of options.
## Options
* `:unit` is any unit returned by `Cldr.Unit.known_units/0`. Ignored if
the number to be formatted is a `Cldr.Unit.t()` struct
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/1`
or a `Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`
* `:style` is one of those returned by `Cldr.Unit.styles`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`
* `:list_options` is a keyword list of options for formatting a list
which is passed through to `Cldr.List.to_string/3`. This is only
applicable when formatting a list of units.
* Any other options are passed to `Cldr.Number.to_string/2`
which is used to format the `number`
## Returns
* `{:ok, io_list}` or
* `{:error, {exception, message}}`
## Examples
iex> Cldr.Unit.to_iolist Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
{:ok, ["123", " gallons"]}
iex> Cldr.Unit.to_iolist 123, MyApp.Cldr, unit: :megabyte, locale: "en", style: :unknown
{:error, {Cldr.UnknownFormatError, "The unit style :unknown is not known."}}
"""
@spec to_iolist(value() | t(), Cldr.backend() | Keyword.t(), Keyword.t()) ::
{:ok, list()} | {:error, {module, binary}}
def to_iolist(unit, backend, options \\ [])
# Options but no backend
def to_iolist(unit, options, []) when is_list(options) do
locale = Cldr.get_locale()
to_iolist(unit, locale.backend, options)
end
def to_iolist(%Unit{} = unit, backend, options) when is_list(options) do
with {locale, style, options} <- normalize_options(backend, options),
{:ok, locale} <- backend.validate_locale(locale),
{:ok, style} <- validate_style(style) do
number = value(unit)
options =
unit.format_options
|> Keyword.merge(options)
|> Keyword.put(:locale, locale)
{:ok, number_string} = Cldr.Number.to_string(number, backend, options)
number
|> extract_patterns(unit.base_conversion, locale, style, backend, options)
|> combine_patterns(number_string, locale, style, backend, options)
|> maybe_combine_per_unit(locale, style, backend, options)
|> wrap(:ok)
end
end
def to_iolist(number, backend, options) when is_number(number) do
with {:ok, unit} <- new(options[:unit], number) do
to_iolist(unit, backend, options)
end
end
defp wrap(term, tag) do
{tag, term}
end
@doc """
Formats a number into a string according to a unit definition
for the current process's locale and backend or raises
on error.
The curent process's locale is set with
`Cldr.put_locale/1`.
See `Cldr.Unit.to_string!/3` for full details.
"""
@spec to_string!(list_or_number :: value() | t() | [t()]) ::
String.t() | no_return()
def to_string!(unit) do
locale = Cldr.get_locale()
backend = locale.backend
to_string!(unit, backend, locale: locale)
end
@doc """
Formats a number into a string according to a unit definition
for the current process's locale and backend or raises
on error.
During processing any `:format_options` of a `Unit.t()` are merged with
`options` with `options` taking precedence.
## Arguments
* `number` is any number (integer, float or Decimal) or a
`Cldr.Unit.t()` struct
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
* `options` is a keyword list
## Options
* `:unit` is any unit returned by `Cldr.Unit.known_units/0`. Ignored if
the number to be formatted is a `Cldr.Unit.t()` struct
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`
* `:style` is one of those returned by `Cldr.Unit.available_styles`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`
* Any other options are passed to `Cldr.Number.to_string/2`
which is used to format the `number`
## Returns
* `formatted_string` or
* raises an exception
## Examples
iex> Cldr.Unit.to_string! Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
"123 gallons"
iex> Cldr.Unit.to_string! Cldr.Unit.new!(:gallon, 1), MyApp.Cldr
"1 gallon"
iex> Cldr.Unit.to_string! Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "af"
"1 gelling"
"""
@spec to_string!(value() | t() | list(t()), Cldr.backend() | Keyword.t(), Keyword.t()) ::
String.t() | no_return()
def to_string!(unit, backend, options \\ []) do
case to_string(unit, backend, options) do
{:ok, string} -> string
{:error, {exception, message}} -> raise exception, message
end
end
@doc """
Formats a number into an iolist according to a unit definition
for the current process's locale and backend or raises
on error.
The curent process's locale is set with
`Cldr.put_locale/1`.
See `Cldr.Unit.to_iolist!/3` for full details.
"""
@spec to_iolist!(list_or_number :: value | t() | [t()]) ::
list() | no_return()
def to_iolist!(unit) do
locale = Cldr.get_locale()
backend = locale.backend
to_iolist!(unit, backend, locale: locale)
end
@doc """
Formats a number into an iolist according to a unit definition
for the current process's locale and backend or raises
on error.
During processing any `:format_options` of a `Unit.t()` are merged with
`options` with `options` taking precedence.
## Arguments
* `number` is any number (integer, float or Decimal) or a
`Cldr.Unit.t()` struct
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
* `options` is a keyword list
## Options
* `:unit` is any unit returned by `Cldr.Unit.known_units/0`. Ignored if
the number to be formatted is a `Cldr.Unit.t()` struct
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`
* `:style` is one of those returned by `Cldr.Unit.available_styles`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`
* Any other options are passed to `Cldr.Number.to_string/2`
which is used to format the `number`
## Returns
* `io_list` or
* raises an exception
## Examples
iex> Cldr.Unit.to_iolist! Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
["123", " gallons"]
"""
@spec to_iolist!(value() | t(), Cldr.backend() | Keyword.t(), Keyword.t()) ::
list() | no_return()
def to_iolist!(unit, backend, options \\ []) do
case to_iolist(unit, backend, options) do
{:ok, string} -> string
{:error, {exception, message}} -> raise exception, message
end
end
@dialyzer {:nowarn_function, {:extract_patterns, 6}}
defp extract_patterns(number, {unit_list, per_list}, locale, style, backend, options) do
{
extract_patterns(number, unit_list, locale, style, backend, options),
extract_patterns(1, per_list, locale, style, backend, options)
}
end
# When extracting a list of patterns the objective is to use the singluar
# form of the pattern for all except the last element which uses the
# plural form indicated by the number
defp extract_patterns(number, [{unit, _conversion}], locale, style, backend, options) do
[to_pattern(number, unit, locale, style, backend, options)]
end
defp extract_patterns(number, [{unit, _conversion} | rest], locale, style, backend, options) do
[
to_pattern(1, unit, locale, style, backend, options)
| extract_patterns(number, rest, locale, style, backend, options)
]
end
# Combine the patterns merging prefix and units, applying "times" for
# compound units and the "per" pattern if required. This are some heuristics
# here than may not result in a grammatically correct result for some
# languages
@dialyzer {:nowarn_function, {:combine_patterns, 6}}
defp combine_patterns({patterns, per_patterns}, number_string, locale, style, backend, options) do
{
combine_patterns(patterns, number_string, locale, style, backend, options),
combine_patterns(per_patterns, "", locale, style, backend, options)
}
end
defp combine_patterns([pattern], number_string, _locale, _style, _backend, _options) do
Substitution.substitute(number_string, pattern)
end
defp combine_patterns([pattern | rest], number_string, locale, style, backend, _options) do
units = units_for(locale, style, backend)
times_pattern = get_in(units, [:times, :compound_unit_pattern])
[
Substitution.substitute(number_string, pattern)
| Enum.map(rest, fn p ->
Substitution.substitute("", p)
|> Enum.map(&String.trim/1)
end)
]
|> join_list(times_pattern)
end
defp join_list([head, tail], times_pattern) do
Substitution.substitute([head, tail], times_pattern)
end
defp join_list([head | rest], times_pattern) do
tail = join_list(rest, times_pattern)
join_list([head, tail], times_pattern)
end
@dialyzer {:nowarn_function, {:maybe_combine_per_unit, 5}}
defp maybe_combine_per_unit({unit_list, per_units}, locale, style, backend, _options) do
units = units_for(locale, style, backend)
per_pattern = get_in(units, [:per, :compound_unit_pattern])
Substitution.substitute([unit_list, per_units], per_pattern)
end
defp maybe_combine_per_unit(unit_list, _locale, _style, _backend, _options) do
unit_list
end
@spec to_pattern(value(), unit(), locale(), style(), Cldr.backend(), Keyword.t()) ::
list()
defp to_pattern(number, unit, locale, style, backend, _options)
when unit in @translatable_units do
{:ok, patterns} = pattern_for(locale, style, unit, backend)
cardinal_module = Module.concat(backend, Number.Cardinal)
cardinal_module.pluralize(number, locale, patterns)
end
for {prefix, power} <- Prefix.power_units() do
localize_key = String.to_atom("power#{power}")
match = quote do: <<unquote(prefix), "_", var!(unit)::binary>>
defp to_pattern(number, unquote(match), locale, style, backend, options) do
units = units_for(locale, style, backend)
pattern = get_in(units, [unquote(localize_key), :compound_unit_pattern1])
unit = maybe_translatable_unit(unit)
pattern
|> merge_power_prefix(to_pattern(number, unit, locale, style, backend, options))
end
end
# is there an SI prefix? If so, try reformatting the unit again
for {prefix, power} <- Prefix.si_power_prefixes() do
localize_key = "10p#{power}" |> String.replace("-", "_") |> String.to_atom()
match = quote do: <<unquote(prefix), var!(unit)::binary>>
defp to_pattern(number, unquote(match), locale, style, backend, options) do
units = units_for(locale, style, backend)
pattern = get_in(units, [unquote(localize_key), :unit_prefix_pattern])
unit = maybe_translatable_unit(unit)
pattern
|> merge_SI_prefix(to_pattern(number, unit, locale, style, backend, options))
end
end
# Merging power and SI prefixes into a pattern is a heuristic since the
# underlying data does not convey those rules.
@merge_SI_prefix ~r/([^\s]+)$/u
defp merge_SI_prefix([prefix, place], [place, string]) when is_integer(place) do
[place, String.replace(string, @merge_SI_prefix, "#{prefix}\\1")]
end
defp merge_SI_prefix([prefix, place], [string, place]) when is_integer(place) do
[String.replace(string, @merge_SI_prefix, "#{prefix}\\1"), place]
end
defp merge_SI_prefix([place, prefix], [place, string]) when is_integer(place) do
[place, String.replace(string, @merge_SI_prefix, "#{prefix}\\1")]
end
defp merge_SI_prefix([place, prefix], [string, place]) when is_integer(place) do
[String.replace(string, @merge_SI_prefix, "#{prefix}\\1"), place]
end
@merge_power_prefix ~r/^(\s)+/u
defp merge_power_prefix([prefix, place], [place, string]) when is_integer(place) do
[place, String.replace(string, @merge_power_prefix, "\\1#{prefix}")]
end
defp merge_power_prefix([prefix, place], [string, place]) when is_integer(place) do
[String.replace(string, @merge_power_prefix, "\\1#{prefix}"), place]
end
defp merge_power_prefix([place, prefix], [place, string]) when is_integer(place) do
[place, String.replace(string, @merge_power_prefix, "\\1#{prefix}")]
end
defp merge_power_prefix([place, prefix], [string, place]) when is_integer(place) do
[String.replace(string, @merge_power_prefix, "\\1#{prefix}"), place]
end
@doc """
Return the value of the Unit struct
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
## Returns
* an integer, float or Decimal representing the amount
of the unit
## Example
iex> Cldr.Unit.value Cldr.Unit.new!(:kilogram, 23)
23
"""
@spec value(unit :: t()) :: value()
def value(%Unit{value: value}) do
value
end
@doc """
Decomposes a unit into subunits.
Any list compatible units can be provided
however a list of units of decreasing scale
is recommended. For example `[:foot, :inch]`
or `[:kilometer, :meter, :centimeter, :millimeter]`
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
* `unit_list` is a list of valid units (one or
more from the list returned by `Cldr.Unit.known_units/0`. All
units must be from the same unit category.
* `format_options` is a Keyword list of options
that is added to the *last* unit in `unit_list`.
The `format_options` will be applied when calling
`Cldr.Unit.to_string/3` on the `unit`. The
default is `[]`.
## Returns
* a list of units after decomposition or an error
tuple
## Examples
iex> u = Cldr.Unit.new!(10.3, :foot)
iex> Cldr.Unit.decompose u, [:foot, :inch]
[Cldr.Unit.new!(:foot, 10), Cldr.Unit.new!(:inch, Ratio.new(18, 5))]
iex> u = Cldr.Unit.new!(:centimeter, 1111)
iex> Cldr.Unit.decompose u, [:kilometer, :meter, :centimeter, :millimeter]
[Cldr.Unit.new!(:meter, 11), Cldr.Unit.new!(:centimeter, 11)]
"""
@spec decompose(unit :: Unit.t(), unit_list :: [Unit.unit()], options :: Keyword.t()) ::
[Unit.t()]
def decompose(unit, unit_list, format_options \\ [])
def decompose(unit, [], _format_options) do
[unit]
end
# This is the last unit
def decompose(unit, [h | []], format_options) do
new_unit = Conversion.convert!(unit, h)
if zero?(new_unit) do
[]
else
[%{new_unit | format_options: format_options}]
end
end
def decompose(unit, [h | t], format_options) do
new_unit = Conversion.convert!(unit, h)
{integer_unit, remainder} = int_rem(new_unit)
if zero?(integer_unit) do
decompose(remainder, t, format_options)
else
[integer_unit | decompose(remainder, t, format_options)]
end
end
@doc """
Localizes a unit according to the current
processes locale and backend.
The curent process's locale is set with
`Cldr.put_locale/1`.
See `Cldr.Unit.localize/3` for futher
details.
"""
def localize(unit) do
locale = Cldr.get_locale()
backend = locale.backend
localize(unit, backend, locale)
end
@doc """
Localizes a unit according to a territory
A territory can be derived from a `locale_name`
or `Cldr.LangaugeTag.t()`.
Use this function if you have a unit which
should be presented in a user interface using
units relevant to the audience. For example, a
unit `#Cldr.Unit100, :meter>` might be better
presented to a US audiance as `#Cldr.Unit328, :foot>`.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module.
* `options` is a keyword list of options
## Options
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `Cldr.LanguageTag` struct. The default is `backend.get_locale/0`
* `:territory` is any valid territory code returned by
`Cldr.known_territories/0`. The default is the territory defined
as part of the `:locale`. The option `:territory` has a precedence
over the territory in a locale.
* `:usage` is the way in which the unit is intended
to be used. The available `usage` varyies according
to the unit category. See `Cldr.Unit.preferred_units/3`.
## Examples
iex> unit = Cldr.Unit.new!(1.83, :meter)
iex> Cldr.Unit.localize(unit, usage: :person_height, territory: :US)
[Cldr.Unit.new!(:foot, 6), Cldr.Unit.new!(:inch, Ratio.new(6485183463413016, 137269716642252725))]
"""
def localize(unit, backend, options \\ [])
def localize(%Unit{} = unit, options, []) when is_list(options) do
locale = Cldr.get_locale()
options = Keyword.merge([locale: locale], options)
localize(unit, locale.backend, options)
end
def localize(%Unit{} = unit, backend, options) when is_atom(backend) do
with {:ok, unit_list, format_options} <- Preference.preferred_units(unit, backend, options) do
decompose(unit, unit_list, format_options)
end
end
@doc """
Returns a new unit of the same unit
type but with a zero value.
## Argument
* `unit` is any unit returned by `Cldr.Unit.new/2`
## Example
iex> u = Cldr.Unit.new!(:foot, 23.3)
#Cldr.Unit<:foot, 23.3>
iex> Cldr.Unit.zero(u)
#Cldr.Unit<:foot, 0.0>
"""
def zero(%Unit{value: value} = unit) when is_integer(value) do
%Unit{unit | value: 0}
end
def zero(%Unit{value: value} = unit) when is_float(value) do
%Unit{unit | value: 0.0}
end
def zero(%Unit{} = unit) do
%Unit{unit | value: Decimal.new(0)}
end
@doc """
Returns a boolean indicating whether a given unit
has a zero value.
## Argument
* `unit` is any unit returned by `Cldr.Unit.new/2`
## Examples
iex> u = Cldr.Unit.new!(:foot, 23.3)
#Cldr.Unit<:foot, 23.3>
iex> Cldr.Unit.zero?(u)
false
iex> u = Cldr.Unit.new!(:foot, 0)
#Cldr.Unit<:foot, 0>
iex> Cldr.Unit.zero?(u)
true
"""
def zero?(%Unit{value: value}) when is_number(value) do
value == 0
end
@decimal_0 Decimal.new(0)
def zero?(%Unit{value: %Decimal{} = value}) do
Cldr.Math.decimal_compare(value, @decimal_0) == :eq
end
# Ratios that are zero are just integers
# so anything that is a %Ratio{} is not zero
def zero?(%Unit{value: %Ratio{}}) do
false
end
@doc """
Return a list of known measurement systems.
## Example
iex> Cldr.Unit.known_measurement_systems()
%{
metric: %{alias: nil, description: "Metric System"},
uksystem: %{
alias: :imperial,
description: "UK System of measurement: feet, pints, etc.; pints are 20oz"
},
ussystem: %{
alias: nil,
description: "US System of measurement: feet, pints, etc.; pints are 16oz"
}
}
"""
@measurement_systems Cldr.Config.measurement_systems()
def known_measurement_systems do
@measurement_systems
end
@category_usage Cldr.Config.units()
|> Map.get(:preferences)
|> Enum.map(fn {k, v} -> {k, Map.keys(v)} end)
|> Map.new()
@doc """
Returns a mapping between Unit categories
and the uses they define.
"""
def unit_category_usage do
@category_usage
end
@doc """
Returns a list of the known unit categories.
## Example
iex> Cldr.Unit.known_unit_categories
[:acceleration, :angle, :area, :compound, :concentr, :consumption, :coordinate, :digital,
:duration, :electric, :energy, :force, :frequency, :graphics, :length, :light, :mass,
:power, :pressure, :speed, :temperature, :torque, :volume]
"""
@unit_categories Map.keys(@unit_tree) -- [:"10p"]
def known_unit_categories do
@unit_categories
end
@doc """
Returns a mapping from unit categories to the
base unit.
"""
@base_units Cldr.Config.units() |> Map.get(:base_units) |> Map.new()
def base_units do
@base_units
end
@doc """
Returns the base unit for a given `Cldr.Unit.t()`
or `atom()`.
## Argument
* `unit` is either a `t:Cldr.Unit.t()` or an `atom`
## Returns
* `{:ok, base_unit}` or
* `{:error, {exception, reason}}`
## Example
iex> Cldr.Unit.base_unit :square_kilometer
{:ok, :square_meter}
iex> Cldr.Unit.base_unit :square_table
{:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"table\\""}}
"""
def base_unit(unit_name) when is_atom(unit_name) or is_binary(unit_name) do
with {:ok, _unit, conversion} <- validate_unit(unit_name) do
base_unit(conversion)
end
end
# def base_unit(%{base_unit: [base_name]}) when is_atom(base_name) do
# {:ok, base_name}
# end
def base_unit(%Unit{base_conversion: conversion}) do
base_unit(conversion)
end
def base_unit(conversion) when is_list(conversion) or is_tuple(conversion) do
Parser.canonical_base_unit(conversion)
end
def unknown_base_unit_error(unit_name) do
{Cldr.Unit.UnknownBaseUnitError, "Base unit for #{inspect(unit_name)} is not known"}
end
@deprecated "Use `Cldr.Unit.known_unit_categories/0"
defdelegate unit_categories(), to: __MODULE__, as: :known_unit_categories
@doc """
Returns the units category for a given unit
## Options
* `unit` is any unit returned by
`Cldr.Unit.new/2`
## Returns
* `{:ok, category}` or
* `{:error, {exception, message}}`
## Examples
iex> Cldr.Unit.unit_category :pint_metric
{:ok, :volume}
iex> Cldr.Unit.unit_category :stone
{:ok, :mass}
"""
@spec unit_category(Unit.t() | String.t() | atom()) ::
{:ok, atom()} | {:error, {module(), String.t()}}
def unit_category(unit) do
with {:ok, _unit, conversion} <- validate_unit(unit),
{:ok, base_unit} <- base_unit(conversion) do
{:ok, Map.get(base_unit_category_map(), Kernel.to_string(base_unit))}
end
end
@deprecated "Please use `Cldr.Unit.unit_category/1"
def unit_type(unit) do
unit_category(unit)
end
@base_unit_category_map Cldr.Config.units()
|> Map.get(:base_units)
|> Enum.map(fn {k, v} -> {to_string(v), k} end)
|> Map.new()
@doc """
Returns a mapping of base units to their respective
unit categories.
Base units are a common unit for a given unit
category which are used in two scenarios:
1. When converting between units. If two units
have the same base unit they can be converted
to each other. See `Cldr.Unit.Conversion`.
2. When identifying the preferred units for a given
locale or territory, the base unit is used to
aid identification of preferences for given use
cases. See `Cldr.Unit.Preference`.
## Example
=> Cldr.Unit.base_unit_category_map
%{
"kilogram_square_meter_per_cubic_second_ampere" => :voltage,
"kilogram_meter_per_meter_square_second" => :torque,
"square_meter" => :area,
"kilogram" => :mass,
"kilogram_square_meter_per_square_second" => :energy,
"revolution" => :angle,
"candela_per_square_meter" => :luminance,
...
}
"""
@spec base_unit_category_map :: map()
def base_unit_category_map do
@base_unit_category_map
end
@doc """
Returns the known styles for a unit.
## Example
iex> Cldr.Unit.styles
[:long, :short, :narrow]
"""
def styles do
@styles
end
@doc """
Returns the default formatting style.
## Example
iex> Cldr.Unit.default_style
:long
"""
def default_style do
@default_style
end
# Returns a map of unit preferences
#
# Units of measure vary country by country. While
# most countries standardize on the metric system,
# others use the US or UK systems of measure.
#
# When presening a unit to an end user it is appropriate
# to do so using units familiar and relevant to that
# end user.
#
# The data returned by this function supports the
# opportunity to convert a give unit to meet this
# requirement.
#
# Unit preferences can vary by usage, not just territory,
# Therefore the data is structured according to unit
# category andunit usage.
@doc false
@unit_preferences Cldr.Config.units() |> Map.get(:preferences)
@spec unit_preferences() :: map()
@rounding 10
def unit_preferences do
for {category, usages} <- @unit_preferences, into: Map.new() do
usages =
for {usage, preferences} <- usages, into: Map.new() do
preferences =
Cldr.Enum.reduce_peeking(preferences, [], fn
%{regions: regions} = pref, [%{regions: regions} | _rest], acc ->
%{units: units, geq: geq} = pref
value =
Unit.new!(hd(units), geq)
|> Conversion.convert_to_base_unit!()
|> Math.round(@rounding)
|> Map.get(:value)
{:cont, acc ++ [%{pref | geq: Ratio.to_float(value)}]}
pref, _rest, acc ->
pref = %{pref | geq: 0}
{:cont, acc ++ [pref]}
end)
{usage, preferences}
end
{category, usages}
end
end
@doc false
def rounding do
@rounding
end
@doc false
def units_for(locale, style \\ default_style(), backend \\ Cldr.default_backend()) do
module = Module.concat(backend, :"Elixir.Unit")
module.units_for(locale, style)
end
@measurement_systems Cldr.Config.territories()
|> Enum.map(fn {k, v} -> {k, v.measurement_system} end)
|> Map.new()
@doc """
Returns a map of measurement systems by territory
"""
@spec measurement_systems() :: map()
def measurement_systems do
@measurement_systems
end
@doc """
Returns the default measurement system for a territory
in a given category.
## Arguments
* `territory` is any valid territory returned by
`Cldr.validate_territory/1`
* `category` is any measurement system category.
The known categories are `:default`, `:temperature`
and `:paper_size`. The default category is `:default`.
## Examples
iex> Cldr.Unit.measurement_system_for :US
:ussystem
iex> Cldr.Unit.measurement_system_for :GB
:uksystem
iex> Cldr.Unit.measurement_system_for :AU
:metric
iex> Cldr.Unit.measurement_system_for :US, :temperature
:ussystem
iex> Cldr.Unit.measurement_system_for :GB, :temperature
:uksystem
"""
@spec measurement_system_for(atom(), atom()) ::
:metric | :ussystem | :uk_system | {:error, {module(), String.t()}}
def measurement_system_for(territory, category \\ :default) do
with {:ok, territory} <- Cldr.validate_territory(territory) do
measurement_systems()
|> get_in([territory, category])
end
end
@doc false
def pattern_for(%LanguageTag{cldr_locale_name: locale_name}, style, unit, backend) do
with {:ok, style} <- validate_style(style),
{:ok, unit, _conversion} <- validate_unit(unit) do
units = units_for(locale_name, style, backend)
pattern = Map.get(units, unit)
{:ok, pattern}
end
end
def pattern_for(locale_name, style, unit, backend) do
with {:ok, locale} <- backend.validate_locale(locale_name) do
pattern_for(locale, style, unit, backend)
end
end
def per_pattern_for(%LanguageTag{cldr_locale_name: locale_name}, style, unit, backend) do
with {:ok, style} <- validate_style(style),
{:ok, unit, _conversion} <- validate_unit(unit) do
units = units_for(locale_name, style, backend)
pattern = get_in(units, [unit, :per_unit_pattern])
default_pattern = get_in(units, [:per, :compound_unit_pattern])
{:ok, pattern || default_pattern}
end
end
def per_pattern_for(locale_name, style, unit, backend) do
with {:ok, locale} <- backend.validate_locale(locale_name) do
per_pattern_for(locale, style, unit, backend)
end
end
defp normalize_options(backend, options) do
locale = Keyword.get(options, :locale, backend.get_locale())
style = Keyword.get(options, :style, @default_style)
options =
options
|> Keyword.delete(:locale)
|> Keyword.put(:style, style)
{locale, style, options}
end
@doc """
Validates a unit name and normalizes it,
A unit name can be expressed as:
* an `atom()` in which case the unit must be
localizable in CLDR directly
* or a `String.t()` in which case it is parsed
into a list of composable subunits that
can be converted but are not guaranteed to
be output as a localized string.
## Arguments
* `unit_name` is an `atom()` or `String.t()`
## Returns
* `{:ok, canonical_unit_name, conversion}` where
`canonical_unit_name` is the normalized unit name
and `conversion` is an opaque structure used
to convert this this unit into its base unit or
* `{:error, {exception, reason}}`
## Notes
A returned `unit_name` that is an atom is directly
localisable (CLDR has translation data for the unit).
A `unit_name` that is a `String.t()` is composed of
one or more unit names that need to be resolved in
order for the `unit_name` to be localised.
The difference is an implementation detail and should
not be of concern to the user of this library.
## Examples
iex> Cldr.Unit.validate_unit :meter
{
:ok,
:meter,
[meter: %Cldr.Unit.Conversion{base_unit: [:meter], factor: 1, offset: 0}]
}
iex> Cldr.Unit.validate_unit "meter"
{:ok, :meter,
[meter: %Cldr.Unit.Conversion{base_unit: [:meter], factor: 1, offset: 0}]}
iex> Cldr.Unit.validate_unit "miles_per_liter"
{:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"s\\""}}
iex> Cldr.Unit.validate_unit "mile_per_liter"
{:ok, "mile_per_liter",
{[
mile:
%Cldr.Unit.Conversion{
base_unit: [:meter],
factor: Ratio.new(905980129838867985, 562949953421312),
offset: 0
}
],
[
liter:
%Cldr.Unit.Conversion{
base_unit: [:cubic_meter],
factor: Ratio.new(1152921504606847, 1152921504606846976),
offset: 0
}
]}}
"""
def validate_unit(unit_name) when unit_name in @translatable_units do
{:ok, unit_name, [{unit_name, Conversions.conversion_for!(unit_name)}]}
end
@aliases Alias.aliases() |> Map.keys()
def validate_unit(unit_name) when unit_name in @aliases do
unit_name
|> Alias.alias()
|> validate_unit
end
# FIXME refactor this hacky conditional
def validate_unit(unit_name) when is_binary(unit_name) do
unit_name =
unit_name
|> normalize_unit_name
|> maybe_translatable_unit
if is_atom(unit_name) do
validate_unit(unit_name)
else
with {:ok, parsed} <- Parser.parse_unit(unit_name) do
name = Parser.canonical_unit_name(parsed)
canonical_name = maybe_translatable_unit(name)
{:ok, canonical_name, parsed}
end
end
end
def validate_unit(unit_name) when is_atom(unit_name) do
unit_name
|> Atom.to_string()
|> validate_unit
end
def validate_unit(%Unit{unit: unit_name, base_conversion: base_conversion}) do
{:ok, unit_name, base_conversion}
end
def validate_unit(unknown_unit) do
{:error, unit_error(unknown_unit)}
end
@doc false
def normalize_unit_name(name) when is_binary(name) do
String.replace(name, [" ", "-"], "_")
end
def maybe_translatable_unit(name) do
atom_name = String.to_existing_atom(name)
if atom_name in known_units() do
atom_name
else
name
end
rescue
ArgumentError ->
name
end
@doc """
Validates a unit style and normalizes it to a
standard downcased atom form
"""
def validate_style(style) when style in @styles do
{:ok, style}
end
def validate_style(style) when is_binary(style) do
style
|> String.downcase()
|> String.to_existing_atom()
|> validate_style()
catch
ArgumentError ->
{:error, style_error(style)}
end
def validate_style(style) do
{:error, style_error(style)}
end
@doc """
Convert a ratio Unit to a float unit
"""
def ratio_to_float(%Unit{value: %Ratio{} = value} = unit) do
value = Ratio.to_float(value)
%{unit | value: value}
end
def ratio_to_float(%Unit{} = unit) do
unit
end
@doc false
def unit_error(nil) do
{
Cldr.UnknownUnitError,
"A unit must be provided, for example 'Cldr.Unit.string(123, unit: :meter)'."
}
end
def unit_error(unit) do
{Cldr.UnknownUnitError, "The unit #{inspect(unit)} is not known."}
end
@doc false
def unit_category_error(category) do
{Cldr.Unit.UnknownUnitCategoryError, "The unit category #{inspect(category)} is not known."}
end
@doc false
def style_error(style) do
{Cldr.UnknownFormatError, "The unit style #{inspect(style)} is not known."}
end
@doc false
def incompatible_units_error(unit_1, unit_2) do
{
Unit.IncompatibleUnitsError,
"Operations can only be performed between units of the same category. " <>
"Received #{inspect(unit_1)} and #{inspect(unit_2)}"
}
end
@doc false
def unknown_usage_error(category, usage) do
{
Cldr.Unit.UnknownUsageError,
"The unit category #{inspect(category)} does not define a usage #{inspect(usage)}"
}
end
def unit_not_translatable_error(unit) do
{
Cldr.Unit.UnitNotTranslatableError,
"The unit #{inspect(unit)} is not translatable"
}
end
defp int_rem(unit) do
integer = Unit.round(unit, 0, :down) |> Math.trunc()
remainder = Math.sub(unit, integer)
{integer, remainder}
end
end