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, ...] @known_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.known_styles/0`. 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.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, 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 @known_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: name} = unit, backend, options) do if atom_name = known_unit(name) do to_iolist(%{unit | unit: atom_name}, backend, options) else 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 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 @known_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 "([^\\s]+)$" defp merge_prefix([prefix, place], [place, string]) when is_integer(place) do string = maybe_downcase(prefix, string) [place, String.replace(string, ~r/#{@merge_SI_prefix}/u, "#{prefix}\\1")] end defp merge_prefix([prefix, place], [string, place]) when is_integer(place) do string = maybe_downcase(prefix, string) [String.replace(string, ~r/#{@merge_SI_prefix}/u, "#{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, ~r/#{@merge_SI_prefix}/u, "#{prefix}\\1")] end defp merge_prefix([place, prefix], [string, place]) when is_integer(place) do string = maybe_downcase(prefix, string) [String.replace(string, ~r/#{@merge_SI_prefix}/u, "#{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 "([^\\s]+)" defp merge_power_prefix([prefix, place], [place, string]) when is_integer(place) do [place, String.replace(string, ~r/#{@merge_power_prefix}/u, "#{prefix}\\1")] end defp merge_power_prefix([prefix, place], [string, place]) when is_integer(place) do [String.replace(string, ~r/#{@merge_power_prefix}/u, "#{prefix}\\1"), place] end defp merge_power_prefix([place, prefix], [place, string]) when is_integer(place) do [place, String.replace(string, ~r/#{@merge_power_prefix}/u, "\\1#{prefix}")] end defp merge_power_prefix([place, prefix], [string, place]) when is_integer(place) do [String.replace(string, ~r/#{@merge_power_prefix}/u, "\\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() # String decomposition for {power, exp} <- @power do power_unit = String.to_atom("power#{exp}") power = to_string(power) 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] @doc false def known_unit(name) do atom_name = String.to_existing_atom(name) if atom_name in @known_units do atom_name else nil end rescue ArgumentError -> nil end end