defmodule Cldr.Unit.Format do
@moduledoc """
Functions for formatting a unit or unit range into
an iolist or a string.
"""
alias Cldr.Number
alias Cldr.Unit
defmacrop is_grammar(unit) do
quote do
is_tuple(unquote(unit))
end
end
@typep grammar ::
{Unit.translatable_unit(),
{Unit.grammatical_case(), Cldr.Number.PluralRule.plural_type()}}
@typep grammar_list :: [grammar, ...]
@translatable_units Cldr.Unit.known_units()
@si_keys Cldr.Unit.Prefix.si_keys()
@binary_keys Cldr.Unit.Prefix.binary_keys()
@power_keys Cldr.Unit.Prefix.power_keys()
@currencies Cldr.known_currencies()
@currency_base Cldr.Unit.Parser.currency_base()
@default_case :nominative
@default_style :long
@default_plural :other
@root_locale_name Cldr.Config.root_locale_name()
@doc """
Formats a number into a string according to a unit definition
using the current process's locale and backend.
See `Cldr.Unit.to_string/3` for full details.
"""
@spec to_string(list_or_number :: Unit.value() | Unit.t() | [Unit.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 unit or unit range or a number into a string according to a unit
definition for a locale.
During processing any `:format_options` of a `t:Cldr.Unit.t/0` are merged
into the `options` argument.
## Arguments
* `list_or_unit` is any number (integer, float or Decimal) or a
`t:Cldr.Unit.t/0` struct or a list of `t:Cldr.Unit.t/0` structs or a
`t:Cldr.Unit.Range.t/0` 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 of options.
## Options
* `:unit` is any unit returned by `Cldr.Unit.known_units/0`. Ignored if
the number to be formatted is a `t:Cldr.Unit.t/0` 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.known_styles/0`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`.
* `:grammatical_case` indicates that a localisation for the given
locale and given grammatical case should be used. See `Cldr.Unit.known_grammatical_cases/0`
for the list of known grammatical cases. Note that not all locales
define all cases. However all locales do define the `:nominative`
case, which is also the default.
* `:gender` indicates that a localisation for the given
locale and given grammatical gender should be used.
See `Cldr.Unit.known_grammatical_genders/0`
for the list of known grammatical genders. Note that not all locales
define all genders.
* `: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.Format.to_string Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
{:ok, "123 gallons"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr
{:ok, "1 gallon"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "af"
{:ok, "1 gelling"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "bs"
{:ok, "1 galon"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:gallon, 1234), MyApp.Cldr, format: :long
{:ok, "1 thousand gallons"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:gallon, 1234), MyApp.Cldr, format: :short
{:ok, "1K gallons"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:megahertz, 1234), MyApp.Cldr
{:ok, "1,234 megahertz"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(:megahertz, 1234), MyApp.Cldr, style: :narrow
{:ok, "1,234MHz"}
iex> {:ok, range} = Cldr.Unit.Range.new(Cldr.Unit.new!(:gram, 1), Cldr.Unit.new!(:gram, 5))
iex> Cldr.Unit.to_string(range, locale: :ja)
{:ok, "1~5 グラム"}
iex> Cldr.Unit.Format.to_string Cldr.Unit.new!(123, :foot), MyApp.Cldr
{:ok, "123 feet"}
iex> Cldr.Unit.Format.to_string 123, MyApp.Cldr, unit: :foot
{:ok, "123 feet"}
iex> Cldr.Unit.Format.to_string Decimal.new(123), MyApp.Cldr, unit: :foot
{:ok, "123 feet"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(2, "curr-usd-per-gallon"), MyApp.Cldr
{:ok, "$2.00 per gallon"}
iex> Cldr.Unit.to_string Cldr.Unit.new!(2, "gallon-per-curr-usd"), MyApp.Cldr
{:ok, "2 gallons per US dollar"}
iex> Cldr.Unit.Format.to_string 123, MyApp.Cldr, unit: :megabyte, locale: "en", style: :unknown
{:error, {Cldr.UnknownFormatError, "The unit style :unknown is not known."}}
iex> Cldr.Unit.Format.to_string 123, MyApp.Cldr, unit: :megabyte, locale: "en",
...> grammatical_gender: :feminine
{:error, {Cldr.UnknownGrammaticalGenderError,
"The locale :en does not define a grammatical gender :feminine. The valid genders are [:masculine]"
}}
"""
@spec to_string(
Unit.value() | Unit.t() | Unit.Range.t() | list(Unit.t()),
Cldr.backend() | Keyword.t(),
Keyword.t() | map()
) ::
{: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, backend} = Cldr.locale_and_backend_from(options)
to_string(list_or_unit, backend, options)
end
# Backend but no options
def to_string(list_or_unit, backend, options) when is_atom(backend) and is_list(options) do
with {:ok, options} <- normalize_options(backend, options) do
to_string(list_or_unit, backend, options)
end
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 {:ok, options} <- normalize_options(backend, options) do
list_options =
options
|> Map.get(:list_options, [])
|> Keyword.put(:locale, options[:locale])
unit_list
|> Enum.map(&to_string!(&1, backend, options))
|> Cldr.List.to_string(backend, list_options)
end
end
def to_string(%Unit{} = unit, backend, options) when is_map(options) do
with {:ok, list} <- to_iolist(unit, backend, options) do
list
|> :erlang.iolist_to_binary()
|> wrap(:ok)
end
end
def to_string(%Unit.Range{} = range, backend, options) when is_map(options) do
with {:ok, list} <- to_iolist(range, backend, options) do
list
|> :erlang.iolist_to_binary()
|> wrap(:ok)
end
end
# It's a number, not a unit or range struct
def to_string(number, backend, options) when is_number(number) do
with {:ok, unit} <- Cldr.Unit.new(options[:unit], number) do
to_string(unit, backend, options)
end
end
def to_string(%Decimal{} = number, backend, options) do
with {:ok, unit} <- Cldr.Unit.new(options[:unit], number) do
to_string(unit, backend, options)
end
end
@doc """
Formats a unit or unit range or a number into a string according to a unit
definition for the current locale.
During processing any `:format_options` of a `t:Cldr.Unit.t/0` are merged
into the `options` argument.
The current 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 :: Unit.value() | Unit.t() | [Unit.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 unit or unit range or a number into a string according to a unit
definition for a locale. Raises on error.
During processing any `:format_options` of a `t:Cldr.Unit.t/0` are merged
into the `options` argument.
During processing any `:format_options` of a `t:Cldr.Unit.t/0` are merged with
`options` with `options` taking precedence.
## Arguments
* `number_or_unit` is any number (integer, float or Decimal) or a
`t:Cldr.Unit.t/0` struct or a list of `t:Cldr.Unit.t/0` structs or a
`t:Cldr.Unit.Range.t/0` 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 `t:Cldr.Unit.t/0` 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.Format.to_string! Cldr.Unit.new!(:gallon, 123), MyApp.Cldr
"123 gallons"
iex> Cldr.Unit.Format.to_string! Cldr.Unit.new!(:gallon, 1), MyApp.Cldr
"1 gallon"
iex> Cldr.Unit.Format.to_string! Cldr.Unit.new!(:gallon, 1), MyApp.Cldr, locale: "af"
"1 gelling"
iex> {:ok, range} = Cldr.Unit.Range.new(Cldr.Unit.new!(:gram, 1), Cldr.Unit.new!(:gram, 5))
iex> Cldr.Unit.to_string!(range, locale: :ja)
"1~5 グラム"
"""
@spec to_string!(
Unit.value() | Unit.t() | Unit.Range.t() | list(Unit.t()),
Cldr.backend() | Keyword.t(),
Keyword.t() | map()
) ::
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
defp normalize_options(_backend, options) when is_map(options) do
{:ok, options}
end
defp normalize_options(backend, options) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend)
unit_backend = Module.concat(backend, :Unit)
style = Keyword.get(options, :style, @default_style)
grammatical_case = Keyword.get(options, :grammatical_case, @default_case)
grammatical_gender = Keyword.get(options, :grammatical_gender)
with {:ok, locale} <- Cldr.validate_locale(locale, backend),
{:ok, grammatical_case} <- Cldr.Unit.validate_grammatical_case(grammatical_case),
{:ok, default_gender} <- unit_backend.default_gender(locale),
{:ok, gender} <-
Cldr.Unit.validate_grammatical_gender(grammatical_gender, default_gender, locale),
{:ok, style} <- Cldr.Unit.validate_style(style) do
options
|> Map.new()
|> Map.put(:locale, locale)
|> Map.put(:style, style)
|> Map.put(:grammatical_case, grammatical_case)
|> Map.put(:grammatical_gender, gender)
|> Map.put(:backend, backend)
|> wrap(:ok)
end
end
@doc """
Formats a number into an iolist according to a unit definition
for the current process's locale and backend.
See `Cldr.Unit.Format.to_iolist/3` for full details.
"""
@spec to_iolist(list_or_number :: Unit.value() | Unit.t() | [Unit.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.
## Arguments
* `list_or_unit` is any number (integer, float or Decimal) or a
`t:Cldr.Unit.t/0` struct or a list of `t:Cldr.Unit.t/0` structs or a
`t:Cldr.Unit.Range.t/0` struct.
* `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 `t:Cldr.Unit.t/0` 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`
* `:grammatical_case` indicates that a localisation for the given
locale and given grammatical case should be used. See `Cldr.Unit.known_grammatical_cases/0`
for the list of known grammatical cases. Note that not all locales
define all cases. However all locales do define the `:nominative`
case, which is also the default.
* `:gender` indicates that a localisation for the given
locale and given grammatical gender should be used. See `Cldr.Unit.known_grammatical_genders/0`
for the list of known grammatical genders. Note that not all locales
define all genders.
* `: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.Format.to_iolist Cldr.Unit.new!(:gallon, 123)
{:ok, ["123", " gallons"]}
"""
@spec to_iolist(
Cldr.Unit.value() | Cldr.Unit.t() | Cldr.Unit.Range.t() | [Cldr.Unit.t(), ...],
Keyword.t() | map()
) ::
{:ok, list()} | {:error, {atom, binary}}
def to_iolist(unit, backend, options \\ [])
# Options but no backend
def to_iolist(list_or_unit, options, []) when is_list(options) do
{_locale, backend} = Cldr.locale_and_backend_from(options)
to_iolist(list_or_unit, backend, options)
end
# Direct formatting of the unit since it is translatable directly
def to_iolist(%Cldr.Unit{unit: name} = unit, backend, options) when name in @translatable_units do
with {:ok, options} <- normalize_options(backend, options) do
options = extract_options!(unit, options)
unit_grammar = {name, {options.grammatical_case, options.plural}}
unit_pattern = get_unit_pattern!(unit, unit_grammar, options)
unit
|> format_number!(options)
|> Cldr.Substitution.substitute(unit_pattern)
|> wrap(:ok)
end
end
# The unit is a standalone currency
def to_iolist(%Cldr.Unit{unit: <<@currency_base, _curr::binary-3>>} = unit, backend, options) do
with {:ok, options} <- normalize_options(backend, options) do
[{currency, _}] = unit.base_conversion
options =
options
|> Map.put(:currency, currency)
|> Map.put(:backend, backend)
Cldr.Number.to_string(unit.value, Map.to_list(options))
end
end
# Its a compound unit
def to_iolist(%Cldr.Unit{} = unit, backend, options) do
with {:ok, options} <- normalize_options(backend, options) do
options = extract_options!(unit, options)
grammar = grammar(unit, locale: options.locale, backend: options.backend)
formatted_number = format_number!(unit, options)
to_iolist(unit, grammar, formatted_number, options)
|> wrap(:ok)
end
end
# It's a number, which we convert to a unit and then process
def to_iolist(number, backend, options) when is_number(number) do
{unit, options} = Keyword.pop(options, :unit)
with {:ok, unit} <- Cldr.Unit.new(number, unit) do
to_iolist(unit, backend, options)
end
end
# It's a decimal, which we convert to a unit and then process
def to_iolist(%Decimal{} = number, backend, options) do
{unit, options} = Keyword.pop(options, :unit)
with {:ok, unit} <- Cldr.Unit.new(number, unit) do
to_iolist(unit, backend, options)
end
end
# It's a Cldr.Unit.Range when the values are the same so
# format as a single unit.
def to_iolist(%{first: %{value: v}, last: %{value: v} = last}, backend, options) do
to_iolist(last, backend, options)
end
# It's a Cldr.Unit.Range for a basic unit
def to_iolist(%{first: %{value: v1}, last: %{unit: name, value: v2} = last}, backend, options)
when name in @translatable_units do
with {:ok, options} <- normalize_options(backend, options) do
options = extract_options!(last, options)
unit_grammar = {name, {options.grammatical_case, options.plural}}
unit_pattern = get_unit_pattern!(last, unit_grammar, options)
range = Range.new(v1, v2)
number_options = Map.to_list(options)
{:ok, formatted_range} = Number.to_range_string(range, options.backend, number_options)
formatted_range
|> Cldr.Substitution.substitute(unit_pattern)
|> wrap(:ok)
end
end
# It's a Cldr.Unit.Range for a compound unit
def to_iolist(%{first: first, last: last}, backend, options) do
with {:ok, options} <- normalize_options(backend, options) do
options = extract_options!(last, options)
grammar = grammar(last, locale: options.locale, backend: options.backend)
range = Range.new(first.value, last.value)
number_options = Map.to_list(options)
{:ok, formatted_range} = Number.to_range_string(range, options.backend, number_options)
to_iolist(last, grammar, formatted_range, options)
|> wrap(:ok)
end
end
@doc """
Formats a number into an iolist according to a unit definition
for the current process's locale and backend.
See `Cldr.Unit.Format.to_iolist!/3` for full details.
"""
@spec to_iolist!(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.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 unit using `to_iolist/3` but raises if there is
an error.
## Arguments
* `number` is any number (integer, float or Decimal) or a
`t:Cldr.Unit.t/0` struct or a list of `t:Cldr.Unit.t/0` structs or a
`t:Cldr.Unit.Range.t/0` struct.
* `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 `t:Cldr.Unit.t/0` 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.known_styles/0`.
The current styles are `:long`, `:short` and `:narrow`.
The default is `style: :long`.
* `:grammatical_case` indicates that a localisation for the given
locale and given grammatical case should be used. See `Cldr.Unit.known_grammatical_cases/0`
for the list of known grammatical cases. Note that not all locales
define all cases. However all locales do define the `:nominative`
case, which is also the default.
* `:gender` indicates that a localisation for the given
locale and given grammatical gender should be used. See `Cldr.Unit.known_grammatical_genders/0`
for the list of known grammatical genders. Note that not all locales
define all genders.
* `: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
* `io_list` or
* raises an exception
## Examples
iex> Cldr.Unit.Format.to_iolist! 123, unit: :gallon
["123", " gallons"]
"""
@spec to_iolist!(
Cldr.Unit.value() | Cldr.Unit.t() | Cldr.Unit.Range.t() | [Cldr.Unit.t(), ...],
Keyword.t() | map()
) ::
list() | no_return()
def to_iolist!(number, backend, options \\ [])
def to_iolist!(list_or_unit, options, []) when is_list(options) do
{_locale, backend} = Cldr.locale_and_backend_from(options)
to_iolist!(list_or_unit, backend, options)
end
def to_iolist!(number, backend, options) do
case to_iolist(number, backend, options) do
{:ok, io_list} -> io_list
{:error, {exception, reason}} -> raise exception, reason
end
end
##
##
## Implementation details
##
##
# For the numerator of a unit
defp to_iolist(unit, grammar, formatted_number, options) when is_list(grammar) do
unit
|> do_iolist(grammar, options)
|> substitute_number(formatted_number)
end
# For compound "per" units
defp to_iolist(unit, {numerator, denominator}, formatted_number, options) do
per_pattern = get_in(options.formats, [:per, :compound_unit_pattern])
numerator_pattern = to_iolist(unit, numerator, formatted_number, options)
denominator_pattern =
unit
|> Map.put(:_denominator, true)
|> do_iolist(denominator, Map.put(options, :plural, options.per_plural))
|> extract_unit()
Cldr.Substitution.substitute([numerator_pattern, denominator_pattern], per_pattern)
end
# Recurive processing of a unit grammar
defp do_iolist(_unit, [], _options) do
[]
end
# Currency units
defp do_iolist(%{_denominator: true} = unit, [{currency, _} | rest], options)
when currency in @currencies do
{:ok, currency} =
Cldr.Currency.currency_for_code(currency, options.backend, locale: options.locale)
formatted = Map.get(currency.count, options.plural, :other)
[formatted | do_iolist(unit, rest, options)]
end
defp do_iolist(unit, [{currency, _} | rest], options) when currency in @currencies do
formatted = format_number!(unit, Map.put(options, :currency, currency))
[formatted | do_iolist(unit, rest, options)]
end
# Numeric prefixes
defp do_iolist(unit, [{integer, _} | rest], options) when is_integer(integer) do
options = Map.put(options, :plural, plural(integer, options))
formatted = Cldr.Number.to_string!(integer, options.backend, Map.to_list(options))
rest = do_iolist(unit, rest, options)
merge_numeric_prefix([formatted, 0], rest)
end
# SI Prefixes
defp do_iolist(unit, [{si_prefix, _} | rest], options) when si_prefix in @si_keys do
si_pattern = get_prefix_pattern!(si_prefix, options)
rest = do_iolist(unit, rest, options)
merge_prefix(si_pattern, rest)
end
# Binary Prefixes
defp do_iolist(unit, [{binary_prefix, _} | rest], options) when binary_prefix in @binary_keys do
binary_pattern = get_prefix_pattern!(binary_prefix, options)
rest = do_iolist(unit, rest, options)
merge_prefix(binary_pattern, rest)
end
# Power prefixes
defp do_iolist(unit, [{power_prefix, _} | rest], options) when power_prefix in @power_keys do
power_pattern = get_power_pattern!(power_prefix, options)
rest = do_iolist(unit, rest, options)
merge_power_prefix(power_pattern, rest)
end
defp do_iolist(unit, [first], options) when is_grammar(first) do
get_unit_pattern!(unit, first, options)
end
defp do_iolist(_unit, [pattern_list], _options) do
pattern_list
end
# List head is a grammar unit
defp do_iolist(unit, [first | rest], %{formats: formats} = options) when is_grammar(first) do
times_pattern = get_in(formats, [:times, :compound_unit_pattern])
unit_pattern_1 = get_unit_pattern!(unit, first, options)
unit_pattern_2 =
do_iolist(unit, rest, options)
|> extract_unit()
Cldr.Substitution.substitute([unit_pattern_1, unit_pattern_2], times_pattern)
end
# List head is a format pattern
@dialyzer {:nowarn_function, do_iolist: 3}
defp do_iolist(unit, [unit_pattern_1 | rest], options) do
times_pattern = get_in(options.formats, [:times, :compound_unit_pattern])
unit_pattern_2 =
do_iolist(unit, rest, options)
|> extract_unit()
Cldr.Substitution.substitute([unit_pattern_1, unit_pattern_2], times_pattern)
end
defp do_iolist(unit, grammar, _options) do
raise "Unmatched grammar: #{inspect(grammar)} for unit #{inspect(unit)}"
end
# Get the appropriate unit pattern. An important part of
# this is the following from TR35:
# Note that for certain plural cases, the unit pattern may not
# provide for inclusion of a numeric value—that is, it may not
# include “{0}”. This is especially true for the explicit cases
# “0” and “1” (which may have patterns like “zero seconds”). In
# certain languages such as Arabic and Hebrew, this may also be
# true with certain units for the plural cases “zero”, “one”, or
# “two” (in these languages, such plural cases are only used for
# the corresponding exact numeric values, so there is no concern
# about loss of precision without the numeric value).
# Therefore the overall proess is as follows:
#
# If there is a tenplate for an explicit value, try that template.
# as of CLDR39 there are no locales that have any explicit cases
# but a custom unit may have such data.
# If there is no such value then proceed with the
# provided plural category
# If however the retrieved pattern has no substitutions
# then that pattern is only used if there is an exacf match
# with the value. This means that if the pattern has no
# substitutions for the plural category `:one` then it
# is applied only if the the unit value is "1". Otherwise
# use the unit category `:other`.
defp get_unit_pattern!(%Unit{} = unit, grammar, options) do
%{grammatical_case: grammatical_case, grammatical_gender: gender, plural: plural} = options
integer = integer_unit_value(unit)
integer_pattern = get_unit_pattern(grammar, Map.put(options, :plural, integer))
cond do
integer = integer_unit?(grammar) ->
integer
currency = currency_unit?(grammar) ->
currency
# If the pattern for an integer is found, use it
integer_pattern ->
integer_pattern
# |> IO.inspect(label: "Integer pattern")
# If the plural range and the integer are aligned, use the plural
# rule no matter whether it has substitutions
integer_and_plural_match?(integer, plural) ->
get_unit_pattern(grammar, options) ||
get_unit_pattern(grammar, Map.put(options, :plural, @default_plural))
# For these plurals get the template and use it only
# if it has substitutions. If it doesn't then use the default
# pattern
plural in [:zero, :one, :two] ->
pattern = get_unit_pattern(grammar, options)
if has_substitutions?(pattern) do
pattern
else
get_unit_pattern(grammar, Map.put(options, :plural, :force_default))
end
# For all other cases return the pattern for the given plural
# category or the default.
true ->
get_unit_pattern(grammar, options) ||
get_unit_pattern(grammar, Map.put(options, :plural, @default_plural))
end || raise(Cldr.Unit.NoPatternError, {unit, grammatical_case, gender, plural})
end
defp get_unit_pattern(grammar, %{plural: plural} = options) when is_integer(plural) do
%{formats: formats, grammatical_case: grammatical_case} = options
{name, {unit_case, _unit_plural}} = grammar
unit_case = if unit_case == :compound, do: grammatical_case, else: unit_case
get_in(formats, [name, unit_case, plural]) ||
get_in(formats, [name, @default_case, plural])
end
defp get_unit_pattern(grammar, %{plural: :force_default} = options) do
%{formats: formats, grammatical_case: grammatical_case} = options
{name, {unit_case, _unit_plural}} = grammar
unit_case = if unit_case == :compound, do: grammatical_case, else: unit_case
get_in(formats, [name, unit_case, @default_plural]) ||
get_in(formats, [name, @default_case, @default_plural])
end
defp get_unit_pattern(grammar, options) do
%{formats: formats, grammatical_case: grammatical_case, plural: plural} = options
{name, {unit_case, unit_plural}} = grammar
unit_case = if unit_case == :compound, do: grammatical_case, else: unit_case
unit_plural = if unit_plural == :compound, do: plural, else: unit_plural
get_in(formats, [name, unit_case, unit_plural]) ||
get_in(formats, [name, @default_case, unit_plural]) ||
get_in(formats, [name, unit_case, @default_plural]) ||
get_in(formats, [name, @default_case, @default_plural])
end
defp get_prefix_pattern!(prefix, options) do
%{grammatical_case: grammatical_case, grammatical_gender: gender, plural: plural} = options
get_in(options.formats, [prefix, :unit_prefix_pattern]) ||
raise(Cldr.Unit.NoPatternError, {prefix, grammatical_case, gender, plural})
end
defp get_power_pattern!(power_prefix, options) do
%{grammatical_case: grammatical_case, grammatical_gender: gender, plural: plural} = options
power_formats = get_in(options.formats, [power_prefix, :compound_unit_pattern])
get_in(power_formats, [gender, plural, grammatical_case]) ||
get_in(power_formats, [gender, plural]) ||
get_in(power_formats, [plural, grammatical_case]) ||
get_in(power_formats, [plural]) ||
get_in(power_formats, [@default_case]) ||
raise(Cldr.Unit.NoPatternError, {power_prefix, grammatical_case, gender, plural})
end
defp currency_unit?({currency, _}) when currency in @currencies do
currency
end
defp currency_unit?(_other) do
nil
end
defp integer_unit?({integer, _}) when is_integer(integer) do
integer
end
defp integer_unit?(_other) do
nil
end
defp integer_and_plural_match?(0, :zero), do: true
defp integer_and_plural_match?(1, :one), do: true
defp integer_and_plural_match?(2, :two), do: true
defp integer_and_plural_match?(_, _), do: false
defp has_substitutions?(pattern) when is_list(pattern) and length(pattern) > 1, do: true
defp has_substitutions?(pattern) when is_list(pattern), do: false
defp extract_unit([place, string]) when is_integer(place) do
String.trim(string)
end
defp extract_unit([string, place]) when is_integer(place) do
String.trim(string)
end
defp extract_unit([unit | rest]) do
[extract_unit(unit) | rest]
end
defp extract_unit(other) do
other
end
defp format_number!(unit, options) do
number_format_options = Keyword.merge(unit.format_options, Map.to_list(options))
Cldr.Number.to_string!(unit.value, options.backend, number_format_options)
end
defp substitute_number([place, unit], formatted_number) when is_integer(place) do
Cldr.Substitution.substitute(formatted_number, [place, unit])
end
defp substitute_number([unit, place], formatted_number) when is_integer(place) do
Cldr.Substitution.substitute(formatted_number, [place, unit])
end
defp substitute_number([currency_string], _formatted_nunber) when is_binary(currency_string) do
[currency_string]
end
defp substitute_number([currency_string | rest], _formatted) when is_binary(currency_string) do
case rest do
[placeholder, string] when is_integer(placeholder) ->
[currency_string, string]
[[placeholder, string] | rest] when is_integer(placeholder) ->
[currency_string | [string | rest]]
end
end
defp substitute_number([head | rest], formatted_number) when is_list(rest) do
[Cldr.Substitution.substitute(formatted_number, head) | rest]
end
# Merging power and SI prefixes into a pattern is a heuristic since the
# underlying data does not convey those rules.
##
## Merge SI prefixes
##
@merge_SI_prefix ~r/([^\s]+)$/u
defp merge_prefix([prefix, place], [place, string]) when is_integer(place) do
string = maybe_downcase(prefix, string)
[place, String.replace(string, @merge_SI_prefix, "#{prefix}\\1")]
end
defp merge_prefix([prefix, place], [string, place]) when is_integer(place) do
string = maybe_downcase(prefix, string)
[String.replace(string, @merge_SI_prefix, "#{prefix}\\1"), place]
end
defp merge_prefix([place, prefix], [place, string]) when is_integer(place) do
string = maybe_downcase(prefix, string)
[place, String.replace(string, @merge_SI_prefix, "#{prefix}\\1")]
end
defp merge_prefix([place, prefix], [string, place]) when is_integer(place) do
string = maybe_downcase(prefix, string)
[String.replace(string, @merge_SI_prefix, "#{prefix}\\1"), place]
end
defp merge_prefix(prefix_pattern, [unit_pattern | rest]) do
[merge_prefix(prefix_pattern, unit_pattern) | rest]
end
##
## Merge power prefixes (square, cube)
##
@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, "#{prefix}\\1")]
end
defp merge_power_prefix([prefix, place], [string, place]) when is_integer(place) do
[String.replace(string, @merge_power_prefix, "#{prefix}\\1"), 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
defp merge_power_prefix([place, prefix], list) when is_integer(place) and is_list(list) do
[list, prefix]
end
defp merge_power_prefix([prefix, place], [string | rest]) when is_integer(place) do
string = maybe_downcase(prefix, string)
[prefix, [string | rest]]
end
defp merge_power_prefix([prefix, place], string) when is_integer(place) and is_binary(string) do
string = maybe_downcase(prefix, string)
[prefix, string]
end
##
## Merge numeric prefixes
##
defp merge_numeric_prefix([prefix, place], [place, string]) when is_integer(place) do
[place, prefix <> string]
end
defp merge_numeric_prefix([prefix, place], [string, place]) when is_integer(place) do
[prefix <> string, place]
end
defp merge_numeric_prefix([place, prefix], [place, string]) when is_integer(place) do
[place, prefix <> string]
end
defp merge_numeric_prefix([place, prefix], [string, place]) when is_integer(place) do
[prefix <> string, place]
end
defp merge_numeric_prefix([place, prefix], list) when is_integer(place) and is_list(list) do
[list, prefix]
end
defp merge_numeric_prefix([prefix, place], [string | rest]) when is_integer(place) do
[prefix, [string | rest]]
end
defp merge_numeric_prefix([prefix, place], string) when is_integer(place) and is_binary(string) do
[prefix, string]
end
# If the prefix has no trailing whitespace then
# downcase the string since it will be
# joined adjacent to the prefix
defp maybe_downcase(prefix, string) do
if String.match?(prefix, ~r/\s+$/u) do
string
else
String.downcase(string)
end
end
@per_plural_default :one
defp extract_options!(unit, %{backend: backend, locale: locale, style: style} = options) do
unit_backend = Module.concat(options.backend, :Unit)
formats = Cldr.Unit.units_for(locale, style, backend)
number_format_options = Map.merge(Map.new(unit.format_options), options)
plural = Cldr.Number.PluralRule.plural_type(unit.value, backend, locale: locale)
per_plural =
locale
|> unit_backend.grammatical_features()
|> get_in([:plural, :per, 1])
|> Kernel.||(@per_plural_default)
options
|> Map.put(:plural, plural)
|> Map.put(:per_plural, per_plural)
|> Map.put(:formats, formats)
|> Map.put(:number_format_options, number_format_options)
end
@doc false
def wrap(term, tag) do
{tag, term}
end
@doc """
Traverses the components of a unit
and resolves a list of base units with
their gramatical case and plural selector
definitions for a given locale.
This function relies upon the internal
representation of units and grammatical features
and is primarily for the support of
formatting a function through `Cldr.Unit.to_string/2`.
## Arguments
* `unit` is a `t:Cldr.Unit.t/0` or a binary
unit string
## Options
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/1`
or a `t:Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.
## Returns
## Examples
"""
@doc since: "3.5.0"
@spec grammar(Unit.t(), Keyword.t()) :: grammar_list() | {grammar_list(), grammar_list()}
def grammar(unit, options \\ [])
def grammar(%Unit{} = unit, options) do
{locale, backend} = Cldr.locale_and_backend_from(options)
module = Module.concat(backend, :Unit)
features =
module.grammatical_features(@root_locale_name)
|> Map.merge(module.grammatical_features(locale))
grammatical_case = Map.get(features, :case)
plural = Map.get(features, :plural)
traverse(unit, &grammar(&1, grammatical_case, plural, options))
end
def grammar(unit, options) when is_binary(unit) do
grammar(Unit.new!(1, unit), options)
end
defp grammar({:unit, unit}, _grammatical_case, _plural, _options) do
{unit, {:compound, :compound}}
end
defp grammar({:per, {left, right}}, _grammatical_case, _plural, _options)
when is_list(left) and is_list(right) do
{left, right}
end
defp grammar({:per, {left, {right, _}}}, grammatical_case, plural, _options) when is_list(left) do
{left, [{right, {grammatical_case.per[1], plural.per[1]}}]}
end
defp grammar({:per, {{left, _}, right}}, grammatical_case, plural, _options)
when is_list(right) do
{[{left, {grammatical_case.per[0], plural.per[0]}}], right}
end
defp grammar({:per, {{left, _}, {right, _}}}, grammatical_case, plural, _options) do
{[{left, {grammatical_case.per[0], plural.per[0]}}],
[{right, {grammatical_case.per[1], plural.per[1]}}]}
end
defp grammar({:times, {left, right}}, _grammatical_case, _plural, _options)
when is_list(left) and is_list(right) do
left ++ right
end
defp grammar({:times, {{left, _}, right}}, grammatical_case, plural, _options)
when is_list(right) do
[{left, {grammatical_case.times[0], plural.times[0]}} | right]
end
defp grammar({:times, {left, {right, _}}}, grammatical_case, plural, _options)
when is_list(left) do
left ++ [{right, {grammatical_case.times[1], plural.times[1]}}]
end
defp grammar({:times, {{left, _}, {right, _}}}, grammatical_case, plural, _options) do
[
{left, {grammatical_case.times[0], plural.times[0]}},
{right, {grammatical_case.times[1], plural.times[1]}}
]
end
defp grammar({:power, {{left, _}, right}}, grammatical_case, plural, _options)
when is_list(right) do
[{left, {grammatical_case.power[0], plural.power[0]}} | right]
end
defp grammar({:power, {{left, _}, {right, _}}}, grammatical_case, plural, _options) do
[
{left, {grammatical_case.power[0], plural.power[0]}},
{right, {grammatical_case.power[1], plural.power[1]}}
]
end
defp grammar({:prefix, {{left, _}, {right, _}}}, grammatical_case, plural, _options) do
[
{left, {grammatical_case.prefix[0], plural.prefix[0]}},
{right, {grammatical_case.prefix[1], plural.prefix[1]}}
]
end
@doc """
Traverses a unit's decomposition and invokes
a function on each node of the composition
tree.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
* `fun` is any single-arity function. It will be invoked
for each node of the composition tree. The argument is a tuple
of the following form:
* `{:unit, argument}`
* `{:times, {argument_1, argument_2}}`
* `{:prefix, {prefix_unit, argument}}`
* `{:power, {power_unit, argument}}`
* `{:per, {argument_1, argument_2}}`
Where the arguments are the results returned
from the `fun/1`.
## Returns
The result returned from `fun/1`
"""
def traverse(%Unit{base_conversion: {left, right}}, fun) when is_function(fun) do
fun.({:per, {do_traverse(left, fun), do_traverse(right, fun)}})
end
def traverse(%Unit{base_conversion: conversion}, fun) when is_function(fun) do
do_traverse(conversion, fun)
end
defp do_traverse([{unit, _}], fun) do
do_traverse(unit, fun)
end
defp do_traverse([head | rest], fun) do
fun.({:times, {do_traverse(head, fun), do_traverse(rest, fun)}})
end
defp do_traverse({unit, _}, fun) do
do_traverse(unit, fun)
end
@si_prefix Cldr.Unit.Prefix.si_power_prefixes()
@binary_prefix Cldr.Unit.Prefix.binary_prefixes()
@power Cldr.Unit.Prefix.power_units() |> Map.new()
# String decomposition
for {power, exp} <- @power do
power_unit = String.to_atom("power#{exp}")
defp do_traverse(unquote(power) <> "_" <> unit, fun) do
fun.({:power, {fun.({:unit, unquote(power_unit)}), do_traverse(unit, fun)}})
end
end
for {prefix, exp} <- @si_prefix do
prefix_unit = String.to_atom("10p#{exp}" |> String.replace("-", "_"))
defp do_traverse(unquote(prefix) <> unit, fun) do
fun.(
{:prefix,
{fun.({:unit, unquote(prefix_unit)}), fun.({:unit, String.to_existing_atom(unit)})}}
)
end
end
for {prefix, exp} <- @binary_prefix do
prefix_unit = String.to_atom("1024p#{exp}" |> String.replace("-", "_"))
defp do_traverse(unquote(prefix) <> unit, fun) do
fun.(
{:prefix,
{fun.({:unit, unquote(prefix_unit)}), fun.({:unit, String.to_existing_atom(unit)})}}
)
end
end
defp do_traverse(unit, fun) when is_binary(unit) do
case Integer.parse(unit) do
{integer, unit} when is_integer(integer) ->
unit = String.trim_leading(unit, "_")
[{integer, {:nominative, :one}} | maybe_wrap(do_traverse(unit, fun))]
_other ->
fun.({:unit, String.to_existing_atom(unit)})
end
end
defp do_traverse(unit, fun) when is_atom(unit) do
fun.({:unit, unit})
end
defp integer_unit_value(%Unit{value: value}) when is_integer(value) do
value
end
defp integer_unit_value(%Unit{value: value}) when is_float(value) do
int_value = trunc(value)
if int_value == value, do: int_value, else: nil
end
defp integer_unit_value(%Unit{value: %Decimal{}} = value) do
value
|> Unit.to_float_unit()
|> integer_unit_value()
end
defp plural(integer, options) do
Cldr.Number.PluralRule.plural_type(integer, options.backend, locale: options.locale)
end
defp maybe_wrap(list) when is_list(list), do: list
defp maybe_wrap(elem), do: [elem]
end