defmodule Cldr.Unit.Preference do alias Cldr.Unit alias Cldr.Unit.Conversion alias Cldr.Unit.Conversion.Options @doc """ Returns a list of the preferred units for a given unit, locale, territory and use case. The units used to represent length, volume and so on depend on a given territory, measurement system and usage. For example, in the US, people height is most commonly referred to in `inches`, or informally as `feet and inches`. In most of the rest of the world it is `centimeters`. ## Arguments * `unit` is any unit returned by `Cldr.Unit.new/2`. * `backend` is any Cldr backend module. That is, any module that includes `use Cldr`. The default is `Cldr.default_backend/0` * `options` is a keyword list of options or a `Cldr.Unit.Conversion.Options` struct. The default is `[]`. ## Options * `:usage` is the unit usage. for example `;person` for a unit of type `:length`. The available usage for a given unit category can be seen with `Cldr.Config.preferred_units/0`. The default is `nil`. * `:scope` is either `:small` or `nil`. In some usage, the units used are different when the unit size is small. It is up to the developer to determine when `scope: :small` is appropriate. * `:alt` is either `:informal` or `nil`. Like `:scope`, the units in use depend on whether they are being used in a formal or informal context. * `:locale` is any locale returned by `Cldr.validate_locale/2` ## Returns * `{:ok, unit_list}` or * `{:error, {exception, reason}}` ## Examples iex> meter = Cldr.Unit.new!(:meter, 1) iex> Cldr.Unit.Preference.preferred_units meter, MyApp.Cldr, locale: "en-US", usage: :person {:ok, [:inch], []} iex> Cldr.Unit.Preference.preferred_units meter, MyApp.Cldr, locale: "en-AU", usage: :person {:ok, [:centimeter], []} iex> Cldr.Unit.Preference.preferred_units meter, MyApp.Cldr, locale: "en-US", usage: :road {:ok, [:foot], [round_nearest: 10]} iex> Cldr.Unit.Preference.preferred_units meter, MyApp.Cldr, locale: "en-AU", usage: :road {:ok, [:meter], [round_nearest: 10]} ## Notes One common pattern is to convert a given unit into the unit appropriate for a given local and usage. This can be accomplished with a combination of `Cldr.Unit.Preference.preferred_units/3` and `Cldr.Unit.decompose/2`. For example: iex> meter = Cldr.Unit.new!(:meter, 1) iex> preferred_units = Cldr.Unit.Preference.preferred_units(meter, ...> MyApp.Cldr, locale: "en-US", usage: :person) iex> with {:ok, preferred_units, _} <- preferred_units do ...> Cldr.Unit.decompose(meter, preferred_units) ...> end [Cldr.Unit.new!(:inch, Ratio.new(216172782113783808, 5490788665690109))] """ def preferred_units(unit, backend, options \\ []) def preferred_units(%Unit{} = unit, options, []) when is_list(options) do with {:ok, options} <- validate_preference_options(options) do preferred_units(unit, options.backend, options) end end def preferred_units(%Unit{} = unit, backend, options) when is_list(options) do options = Keyword.put_new(options, :usage, unit.usage) with {:ok, options} <- validate_preference_options(backend, options) do preferred_units(unit, backend, options) end end def preferred_units(%Unit{} = unit, _backend, %Options{} = options) do %{usage: usage, territory: territory} = options {:ok, territory_chain} = Cldr.territory_chain(territory) {:ok, category} = Unit.unit_category(unit) {:ok, base_unit} = Conversion.convert_to_base_unit(unit) with {:ok, usage} <- validate_usage(category, usage) do usage = usage_chain(usage) geq = Unit.value(base_unit) |> to_float() preferred_units(category, usage, territory_chain, geq) end end # Rounding matches the number we used # to generate the function clauses @rounding Cldr.Unit.rounding() defp to_float(%Ratio{} = value) do Ratio.to_float(value) |> Cldr.Math.round(@rounding) end defp to_float(%Decimal{} = value) do Decimal.to_float(value) |> Cldr.Math.round(@rounding) end defp to_float(other) do other end defp usage_chain(usage) when is_atom(usage) do usage_chain() |> Map.fetch!(usage) end # TR35 says that for a given usage, if it # can't be found, split it at the last # "_", take the head of the split and try # again. Repeat until the usage is found or # there is nothing more to try. # # The following precomputes these "usage chains" # by taking each known usage and breaking it # down as required by TR36. @usage_chain Cldr.Unit.unit_category_usage() |> Enum.map(fn {_category, usage} -> Enum.map(usage, fn use -> chain = use |> Atom.to_string() |> String.split("_") |> Cldr.Enum.combine_list() |> Enum.map(&String.to_atom/1) |> List.insert_at(0, :default) |> Enum.reverse() {use, chain} end) end) |> List.flatten() |> Map.new() defp usage_chain do @usage_chain end @doc """ Returns a list of the preferred units for a given unit, locale, territory and use case. The units used to represent length, volume and so on depend on a given territory, measurement system and usage. For example, in the US, people height is most commonly referred to in `inches`, or `feet and inches`. In most of the rest of the world it is `centimeters`. ## Arguments * `unit` is any unit returned by `Cldr.Unit.new/2`. * `backend` is any Cldr backend module. That is, any module that includes `use Cldr`. The default is `Cldr.default_backend/0` * `options` is a keyword list of options or a `Cldr.Unit.Conversion.Options` struct. The default is `[]`. ## 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`. ## Returns * `unit_list` or * raises an exception ## Note This function, unlike `Cldr.Unit.preferred_units/3` does not return any available formatting hints. ## Examples iex> meter = Cldr.Unit.new!(:meter, 1) iex> Cldr.Unit.Preference.preferred_units! meter, MyApp.Cldr, locale: "en-US", usage: :person_height [:foot, :inch] iex> Cldr.Unit.Preference.preferred_units! meter, MyApp.Cldr, locale: "en-US", usage: :person [:inch] iex> Cldr.Unit.Preference.preferred_units! meter, MyApp.Cldr, locale: "en-AU", usage: :person [:centimeter] iex> Cldr.Unit.Preference.preferred_units! meter, MyApp.Cldr, locale: "en-US", usage: :road [:foot] iex> Cldr.Unit.Preference.preferred_units! meter, MyApp.Cldr, locale: "en-AU", usage: :road [:meter] """ def preferred_units!(unit, backend, options \\ []) do case preferred_units(unit, backend, options) do {:ok, preferred_units, _} -> preferred_units {:error, {exception, reason}} -> raise exception, reason end end defp validate_preference_options(backend, options) when is_list(options) do options |> Keyword.put(:backend, backend) |> validate_preference_options end defp validate_preference_options(options) when is_list(options) do backend = Keyword.get_lazy(options, :backend, &Cldr.Unit.default_backend/0) locale = Keyword.get_lazy(options, :locale, &backend.get_locale/0) usage = Keyword.get(options, :usage, :default) with {:ok, locale} <- backend.validate_locale(locale), territory = Keyword.get_lazy(options, :territory, fn -> Cldr.Locale.territory_from_locale(locale) end), {:ok, territory} <- Cldr.validate_territory(territory) do options = [locale: locale, territory: territory, usage: usage, backend: backend] {:ok, struct(Options, options)} end end def validate_usage(category, usage) do if get_in(Unit.unit_preferences(), [category, usage]) do {:ok, usage} else {:error, unknown_usage_error(category, usage)} end end for {category, usages} <- Unit.unit_preferences() do for {usage, preferences} <- usages do for preference <- preferences do %{geq: geq, regions: regions, units: units, skeleton: skeleton} = preference if geq == 0 do def preferred_units(unquote(category), unquote(usage), region, _value) when is_atom(region) and region in unquote(regions) do # debug(unquote(category), unquote(usage), region, value, 0) {:ok, unquote(units), unquote(skeleton)} end else def preferred_units(unquote(category), unquote(usage), region, value) when is_atom(region) and region in unquote(regions) and value >= unquote(geq) do # debug(unquote(category), unquote(usage), region, value, unquote(geq)) {:ok, unquote(units), unquote(skeleton)} end end end end end # First we process the potential usage we were offered def preferred_units(category, [usage], region, value) do # debug(category, usage, region, value) preferred_units(category, usage, region, value) end def preferred_units(category, [usage | other_usage], region, value) do # debug(category, usage, region, value) case preferred_units(category, usage, region, value) do {:ok, units, skeleton} -> {:ok, units, skeleton} _other -> preferred_units(category, other_usage, region, value) end end # Second we walk the territory chain with the usage # we were provided def preferred_units(category, usage, [region], value) do # debug(category, usage, region, value) preferred_units(category, usage, region, value) end def preferred_units(category, usage, [region | other_regions], value) do # debug(category, usage, region, value) case preferred_units(category, usage, region, value) do {:ok, units, skeleton} -> {:ok, units, skeleton} _other -> preferred_units(category, usage, other_regions, value) end end # If we get here then try with the default case # Which should basically always work # def preferred_units(category, usage, region, value) when usage != :default do # preferred_units(category, :default, region, value) # end # And it we get here is't game over def preferred_units(category, usage, region, value) do {:error, unknown_preferences_error(category, usage, region, value)} end def debug(category, usage, region, value) do IO.inspect( """ Category: #{inspect(category)} with usage #{inspect(usage)} for region #{inspect(region)} and value #{inspect(value)} """ |> String.replace("\n", " ") ) end def debug(category, usage, region, value, geq) do IO.inspect( """ Preference: #{inspect(category)} with usage #{inspect(usage)} for region #{inspect(region)} and value #{inspect(value)} with >= #{inspect(geq)} """ |> String.replace("\n", " ") ) end def unknown_preferences_error(category, usage, regions, value) do { Cldr.Unit.UnknownUnitPreferenceError, "No preferences found for #{inspect(category)} " <> "with usage #{inspect(usage)} " <> "for region #{inspect(regions)} and " <> "value #{inspect(value)}" } end def unknown_usage_error(category, usage) do { Cldr.Unit.UnknownUsageError, "The unit category #{inspect(category)} does not define a usage #{inspect(usage)}" } end end