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 convertible.

  * `Cldr.Unit.convert/2` to convert one unit to another unit as long as they
    are convertible.

  * `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.

  """
  import Kernel, except: [to_string: 1]

  alias Cldr.Unit
  alias Cldr.{Locale, LanguageTag}
  alias Cldr.Unit.{Math, Alias, Parser, Conversion, Conversions, Preference, BaseUnit, Format}

  @enforce_keys [:unit, :value, :base_conversion, :usage, :format_options]

  defstruct unit: nil,
            value: 0,
            base_conversion: [],
            usage: :default,
            format_options: [],
            backend: nil

  @root_locale_name Cldr.Config.root_locale_name()

  # See https://unicode.org/reports/tr35/tr35-general.html#Case
  @grammatical_case [
    :abessive,
    :ablative,
    :accusative,
    :adessive,
    :allative,
    :causal,
    :comitative,
    :dative,
    :delative,
    :elative,
    :ergative,
    :genitive,
    :illative,
    :inessive,
    :instrumental,
    :locative,
    :localtivecopulative,
    :nominative,
    :oblique,
    :partitive,
    :prepositional,
    :sociative,
    :sublative,
    :superessive,
    :terminative,
    :translative,
    :vocative
  ]

  @grammatical_gender [
    :animate,
    :inanimate,
    :personal,
    :common,
    :feminine,
    :masculine,
    :neuter
  ]

  @styles [
    :long,
    :short,
    :narrow
  ]

  @measurement_systems Cldr.Config.measurement_systems()
  @system_names Map.keys(@measurement_systems) |> Enum.sort()

  @measurement_system_keys [
    :default,
    :paper_size,
    :temperature
  ]

  # Converts a list of atoms into a typespec
  type = &Enum.reduce(&1, fn x, acc -> {:|, [], [x, acc]} end)

  @type translatable_unit :: atom() | list(atom())
  @type unit :: translatable_unit | String.t()
  @type category :: atom()
  @type usage :: atom()
  @type grammatical_gender :: unquote(type.(@grammatical_gender))
  @type grammatical_case :: unquote(type.(@grammatical_case))
  @type measurement_system :: unquote(type.(@system_names))
  @type measurement_system_key :: unquote(type.(@measurement_system_keys))
  @type style :: unquote(type.(@styles))
  @type value :: Cldr.Math.number_or_decimal()
  @type locale :: Locale.locale_name() | LanguageTag.t()
  @type base_conversion :: {translatable_unit, Conversion.t()}
  @type conversion :: [base_conversion()] | {[base_conversion()], [base_conversion()]}

  @type t :: %__MODULE__{
          unit: unit(),
          value: value(),
          base_conversion: conversion(),
          usage: usage(),
          format_options: Keyword.t()
        }

  @default_style :long

  @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.Locale.Loader.get_locale(@config)
             |> Map.fetch!(:units)
             |> Map.fetch!(:long)
             |> Enum.map(fn {k, v} -> {k, Map.keys(v) |> Enum.sort()} end)
             |> Map.new()

  @units Cldr.Config.units()

  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

  @doc """
  Returns the units that are defined for
  a given category (such as :volume, :length)

  See also `Cldr.Unit.known_unit_categories/0`.

  ## Example

      => Cldr.Unit.known_units_by_category
      %{
        acceleration: [:g_force, :meter_per_square_second],
        angle: [:arc_minute, :arc_second, :degree, :radian, :revolution],
        area: [:acre, :dunam, :hectare, :square_centimeter, :square_foot,
         :square_inch, :square_kilometer, :square_meter, :square_mile, :square_yard],
        concentr: [:karat, :milligram_per_deciliter, :millimole_per_liter, :mole,
         :percent, :permille, :permillion, :permyriad], ...
       }

  """
  @units_by_category @unit_tree
                     |> Map.delete(:compound)
                     |> Map.delete(:coordinate)

  @doc since: "3.4.0"
  @spec known_units_by_category :: %{category() => [translatable_unit(), ...]}

  def known_units_by_category do
    @units_by_category
  end

  @doc """
  Returns the known units that are directly
  translatable.

  These units have localised content in CLDR
  and are used as a key to retrieving that
  content.

  ## 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 @units_by_category
                      |> Map.values()
                      |> List.flatten()
                      |> List.delete(:generic)
                      |> Kernel.++(Cldr.Unit.Additional.additional_units())
                      # Beaufort not fully supported yet
                      |> Kernel.--([:beaufort])

  @spec known_units :: [translatable_unit(), ...]
  def known_units do
    @translatable_units
  end

  @deprecated "Use Cldr.Unit.known_units/0"
  defdelegate unit(), to: __MODULE__, as: :known_units

  @doc """
  Returns a list of the known unit categories.

  ## Example

      iex> Cldr.Unit.known_unit_categories
      [:acceleration, :angle, :area, :concentr, :consumption, :digital,
       :duration, :electric, :energy, :force, :frequency, :graphics, :length, :light, :mass,
       :power, :pressure, :speed, :temperature, :torque, :volume]

  """
  @unit_categories Map.keys(@units_by_category) |> Enum.sort()

  @spec known_unit_categories :: list(category())
  def known_unit_categories do
    @unit_categories
  end

  @doc """
  Returns the list of units defined for a given
  category.

  ## Arguments

  * `category` is any unit category returned by
    `Cldr.Unit.known_unit_categories/0`.

  See also `Cldr.Unit.known_units_by_category/0`.

  ## Example

      => Cldr.Unit.known_units_for_category :volume
      {
        :ok,
        [
          :cubic_centimeter,
          :centiliter,
          :hectoliter,
          :cubic_kilometer,
          :acre_foot,
          ...
        ]
      }

  """
  @doc since: "3.4.0"
  @spec known_units_for_category(category()) ::
          {:ok, [translatable_unit(), ...]} | {:error, {module(), String.t()}}

  def known_units_for_category(category) when category in @unit_categories do
    units = Map.fetch!(known_units_by_category(), category)
    {:ok, units}
  end

  def known_units_for_category(category) do
    {:error, unit_category_error(category)}
  end

  @doc """
  Returns a list of the known grammatical
  cases.

  A grammatical case can be provided as an option to
  `Cldr.Unit.to_string/2` in order to localise a unit
  appropriate to the context in which it is used.

  ## Example

      iex> Cldr.Unit.known_grammatical_cases()
      [
        :abessive,
        :ablative,
        :accusative,
        :adessive,
        :allative,
        :causal,
        :comitative,
        :dative,
        :delative,
        :elative,
        :ergative,
        :genitive,
        :illative,
        :inessive,
        :instrumental,
        :locative,
        :localtivecopulative,
        :nominative,
        :oblique,
        :partitive,
        :prepositional,
        :sociative,
        :sublative,
        :superessive,
        :terminative,
        :translative,
        :vocative
      ]

  """
  @doc since: "3.5.0"
  def known_grammatical_cases do
    @grammatical_case
  end

  @doc """
  Returns a list of the known grammatical genders.

  A gender can be provided as an option to
  `Cldr.Unit.to_string/2` in order to localise a unit
  appropriate to the context in which it is used.

  ## Example

      iex> Cldr.Unit.known_grammatical_genders
      [
        :animate,
        :inanimate,
        :personal,
        :common,
        :feminine,
        :masculine,
        :neuter
      ]

  """
  @doc since: "3.5.0"
  def known_grammatical_genders do
    @grammatical_gender
  end

  @doc """
  Returns a new `Unit.t` struct.

  ## Arguments

  * `value` is any float, integer or `Decimal`

  * `unit` is any unit name returned by `Cldr.Unit.known_units/0`

  * `options` is Keyword list of options. The default
    is `[]`

  ## Options

  * `:usage` 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 usage
    must be defined for the unit's category. See
    `Cldr.Unit.unit_category_usage/0` for the known
    usage types for each category.

  ## 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\\""}}

      Cldr.Unit.new(:gallon, 23, usage: :fluid)
      #=> {:ok, #Cldr.Unit<:gallon, 23, usage: :fluid, format_options: []>}

  """
  @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_integer(value) do
    create_unit(value, unit, options)
  end

  def new(unit, value, options) when is_integer(value) do
    new(value, unit, options)
  end

  def new(value, unit, options) when is_float(value) do
    create_unit(Decimal.from_float(value), unit, options)
  end

  def new(unit, value, options) when is_float(value) do
    new(Decimal.from_float(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(a, b, options) do
    with {:error, _} <- maybe_new(a, b, options) do
      maybe_new(b, a, options)
    end
  end

  def maybe_new(a, b, options) do
    new(a, Decimal.new(b), options)
  rescue
    Decimal.Error ->
      {:error,
       {Cldr.InvalidUnit,
        "Could not resolve a new unit from Cldr.Unit.new(#{inspect(b)}, #{inspect(a)})"}}
  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.new!(: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 new `Unit.t` struct from a map.

  A map representation of a unit may be generated
  from json or other external format data.

  The map provided must have the keys `unit` and
  `value` in either `String.t` or `atom` (both keys
  must be of the same type).

  `value` must be either an integer, a float or a map
  representation of a rational number. A rational number
  map has the keys `numerator` and `denominator` in either
  `String.t` or `atom` format (both keys must be of
  the same type).

  ## Arguments

  * `map` is a map with the keys `unit` and `value`

  ## Returns

  * `{:ok, unit}` or

  * `{:error, {exception, reason}}`

  ## Examples

      Cldr.Unit.from_map %{value: 1, unit: "kilogram"}
      => {:ok, #Cldr.Unit<:kilogram, 1>}

      Cldr.Unit.from_map %{value: %{numerator: 3, denominator: 4}, unit: "kilogram"}
      => {:ok, #Cldr.Unit<:kilogram, 3 <|> 4>}

      Cldr.Unit.from_map %{"value" => 1, "unit" => "kilogram"}
      => {:ok, #Cldr.Unit<:kilogram, 1>}

  """
  def from_map(%{"unit" => unit, "value" => value}) when is_number(value) do
    new(unit, value)
  end

  def from_map(%{"unit" => unit, "value" => value}) when is_binary(value) do
    new(unit, value)
  end

  def from_map(%{
        "unit" => unit,
        "value" => %{"numerator" => numerator, "denominator" => denominator}
      }) do
    new(unit, Decimal.div(numerator, denominator))
  end

  def from_map(%{unit: unit, value: value}) when is_number(value) do
    new(unit, value)
  end

  def from_map(%{unit: unit, value: %{numerator: numerator, denominator: denominator}}) do
    new(unit, Decimal.div(numerator, denominator))
  end

  def from_map(other) do
    {:error, unit_error(other)}
  end

  @doc """
  Parse a string to create a new unit.

  This function attempts to parse a string
  into a `number` and `unit type`. If successful
  it attempts to create a new unit using
  `Cldr.Unit.new/3`.

  The parsed `unit type` is aliased against all the
  known unit names for a give locale (or the current
  locale if no locale is specified). The known
  aliases for unit types can be returned with
  `MyApp.Cldr.Unit.unit_strings_for/1` where `MyApp.Cldr`
  is the name of a backend module.

  ## Arguments

  * `unit string` is any string to be parsed and if
    possible used to create a new `t:Cldr.Unit`

  * `options` is a keyword list of options

  ## Options

  * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
    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`.

  * `:only` is a unit category or unit, or a list of unit categories and units.
    The parsed unit must match one of the categories or units in order to
    be valid. This is helpful when disambiguating parsed units. For example,
    parsing "2w" could be either "2 watts" or "2 weeks". Specifying `only: :duration`
    would return "2 weeks". Specifying `only: :power` would return
    "2 watts"

  * `:except` is the oppostte of `:only`. The parsed unit must *not*
    match the specified unit or category, or unit categories and units.

  ## Returns

  * `{:ok, unit}` or

  * `{:error, {exception, reason}}`

  ## Notes

  * When both `:only` and `:except` options are passed, both
    conditions must be true in order to return a parsed result.

  * Only units returned by `Cldr.Unit.known_units/0` can be
    used in the `:only` and `:except` filters.

  ## Examples

      iex> Cldr.Unit.parse "1kg"
      Cldr.Unit.new(1, :kilogram)

      iex> Cldr.Unit.parse "1w"
      Cldr.Unit.new(1, :watt)

      iex> Cldr.Unit.parse "1w", only: :duration
      Cldr.Unit.new(1, :week)

      iex> Cldr.Unit.parse "1m", only: [:year, :month, :day]
      Cldr.Unit.new(1, :month)

      iex> Cldr.Unit.parse "1 tages", locale: "de"
      Cldr.Unit.new(1, :day)

      iex> Cldr.Unit.parse "1 tag", locale: "de"
      Cldr.Unit.new(1, :day)

      iex> Cldr.Unit.parse("42 millispangels")
      {:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"spangels\\""}}

  """
  @spec parse(binary) :: {:ok, t()} | {:error, {module(), binary()}}

  @doc since: "3.6.0"
  def parse(unit_string, options \\ []) do
    {locale, backend} = Cldr.locale_and_backend_from(options)

    with {:ok, locale} <- Cldr.validate_locale(locale, backend),
         {:ok, strings} <- Module.concat([backend, :Unit]).unit_strings_for(locale) do
      case Cldr.Number.Parser.scan(unit_string, options) do
        [number, unit] when is_number(number) and is_binary(unit) ->
          units = resolve_unit_alias(unit, strings)
          new_unit(number, unit, units, options)

        [unit, number] when is_number(number) and is_binary(unit) ->
          units = resolve_unit_alias(unit, strings)
          new_unit(number, unit, units, options)

        _other ->
          {:error, not_parseable_error(unit_string)}
      end
    end
  end

  @doc """
  Parse a string to find a matching unit-atom.

  This function attempts to parse a string and
  extract the `unit type`.

  The parsed `unit type` is aliased against all the
  known unit names for a give locale (or the current
  locale if no locale is specified). The known
  aliases for unit types can be returned with
  `MyApp.Cldr.Unit.unit_strings_for/1` where `MyApp.Cldr`
  is the name of a backend module.

  ## Arguments

  * `unit_name_string` is any string to be parsed and converted into a `unit type`

  * `options` is a keyword list of options

  ## Options

  * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
    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`.

  * `:only` is a unit category or unit, or a list of unit categories and units.
    The parsed unit must match one of the categories or units in order to
    be valid. This is helpful when disambiguating parsed units. For example,
    parsing "w" could be either `:watt` or `:weeks`. Specifying `only: :duration`
    would return `:weeks`. Specifying `only: :power` would return `:watt`

  * `:except` is the oppostte of `:only`. The parsed unit must *not*
    match the specified unit or category, or unit categories and units.

  ## Returns

  * `{:ok, unit_name}` or

  * `{:error, {exception, reason}}`

  ## Notes

  * When both `:only` and `:except` options are passed, both
    conditions must be true in order to return a parsed result.

  * Only units returned by `Cldr.Unit.known_units/0` can be
    used in the `:only` and `:except` filters.

  ## Examples

      iex> Cldr.Unit.parse_unit_name "kg"
      {:ok, :kilogram}

      iex> Cldr.Unit.parse_unit_name "w"
      {:ok, :watt}

      iex> Cldr.Unit.parse_unit_name "w", only: :duration
      {:ok, :week}

      iex> Cldr.Unit.parse_unit_name "m", only: [:year, :month, :day]
      {:ok, :month}

      iex> Cldr.Unit.parse_unit_name "tages", locale: "de"
      {:ok, :day}

      iex> Cldr.Unit.parse_unit_name "tag", locale: "de"
      {:ok, :day}

      iex> Cldr.Unit.parse_unit_name("millispangels")
      {:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"spangels\\""}}

  """
  @doc since: "3.14.0"

  @spec parse_unit_name(binary, Keyword.t()) :: {:ok, atom} | {:error, {module(), binary()}}
  def parse_unit_name(unit_name_string, options \\ []) do
    {locale, backend} = Cldr.locale_and_backend_from(options)

    with {:ok, locale} <- Cldr.validate_locale(locale, backend),
         {:ok, strings} <- Module.concat([backend, :Unit]).unit_strings_for(locale),
         units = resolve_unit_alias(unit_name_string, strings),
         {:ok, {only, except, _}} <- get_filter_options(options),
         {:ok, unit} = unit_matching_filter(unit_name_string, units, only, except),
         {:ok, unit, _base_conversion} <- validate_unit(unit) do
      {:ok, unit}
    end
  end

  defp new_unit(number, unit, units, options) do
    with {:ok, {only, except, options}} <- get_filter_options(options),
         {:ok, unit} <- unit_matching_filter(unit, units, only, except) do
      new(number, unit, options)
    end
  end

  defp get_filter_options(options) do
    {only, options} = Keyword.pop(options, :only, []) |> maybe_list_wrap()
    {except, options} = Keyword.pop(options, :except, []) |> maybe_list_wrap()

    with {:ok, only} <- validate_categories_or_units(only),
         {:ok, except} <- validate_categories_or_units(except) do
      {:ok, {only, except, options}}
    end
  end

  # If there are no options to filter then
  # use the original implementation which is to pick
  # the shortest match (lexically shortest)

  defp unit_matching_filter(_unit, unit, {[], []} = _only, {[], []} = _except)
       when is_binary(unit) do
    {:ok, unit}
  end

  defp unit_matching_filter(_unit, units, {[], []} = _only, {[], []} = _except) do
    units
    |> Enum.map(&Kernel.to_string/1)
    |> Enum.sort(&(String.length(&1) <= String.length(&2) && &1 < &2))
    |> hd
    |> wrap(:ok)
  end

  # If there is an :only and/or :except option then
  # filter for a match. If there is no match its an
  # error. And error could be because the result is
  # ambiguous (multiple results) or because no category
  # could be derived for a unit

  defp unit_matching_filter(unit, units, only, except) do
    case filter_units(units, only, except) do
      [unit] -> {:ok, unit}
      [] -> {:error, category_unit_match_error(units, only, except)}
      units -> {:error, ambiguous_unit_error(unit, units)}
    end
  end

  defp filter_units(units, only, except) do
    Enum.filter(units, fn unit ->
      case unit_category(unit) do
        {:ok, category} ->
          category_match?(category, only, except) && unit_match?(unit, only, except)

        _other ->
          false
      end
    end)
  end

  defp category_match?(_category, {[], _}, {[], _}), do: true
  defp category_match?(category, {only, _}, {[], _}), do: category in only
  defp category_match?(category, {[], _}, {except, _}), do: category not in except

  defp category_match?(category, {only, _}, {except, _}),
    do: category in only and category not in except

  defp unit_match?(_unit, {_, []}, {_, []}), do: true
  defp unit_match?(unit, {_, only}, {_, []}), do: unit in only
  defp unit_match?(unit, {_, []}, {_, except}), do: unit not in except
  defp unit_match?(unit, {_, only}, {_, except}), do: unit in only and unit not in except

  defp wrap(term, atom), do: {atom, term}

  defp maybe_list_wrap({list, options}) when is_list(list), do: {list, options}
  defp maybe_list_wrap({other, options}), do: {[other], options}

  @doc """
  Parse a string to create a new unit or
  raises an exception.

  This function attempts to parse a string
  into a `number` and `unit type`. If successful
  it attempts to create a new unit using
  `Cldr.Unit.new/3`.

  The parsed `unit type` is un-aliased against all the
  known unit names for a give locale (or the current
  locale if no locale is specified). The known
  aliases for unit types can be returned with
  `MyApp.Cldr.Unit.unit_strings_for/1` where `MyApp.Cldr`
  is the name of a backend module.

  ## Arguments

  * `unit string` is any string to be parsed and if
    possible used to create a new `t:Cldr.Unit`

  * `options` is a keyword list of options

  ## Options

  * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
    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`.

  * `:only` is a unit category or unit, or a list of unit categories and units.
    The parsed unit must match one of the categories or units in order to
    be valid. This is helpful when disambiguating parsed units. For example,
    parsing "2w" could be either "2 watts" or "2 weeks". Specifying `only: :duration`
    would return "2 weeks". Specifying `only: :power` would return
    "2 watts"

  * `:except` is the oppostte of `:only`. The parsed unit must *not*
    match the specified unit or category, or unit categories and units.

  ## Notes

  * When both `:only` and `:except` options are passed, both
    conditions must be true in order to return a parsed result.

  * Only units returned by `Cldr.Unit.known_units/0` can be
    used in the `:only` and `:except` filters.

  ## Returns

  * `unit` or

  * raises an exception

  ## Examples

      iex> Cldr.Unit.parse! "1kg"
      Cldr.Unit.new!(1, :kilogram)

      iex> Cldr.Unit.parse! "1 tages", locale: "de"
      Cldr.Unit.new!(1, :day)

      iex> Cldr.Unit.parse!("42 candela per lux")
      Cldr.Unit.new!(42, "candela per lux")

      iex> Cldr.Unit.parse!("42 millispangels")
      ** (Cldr.UnknownUnitError) Unknown unit was detected at "spangels"

  """
  @spec parse!(binary) :: t() | no_return()

  @doc since: "3.6.0"
  def parse!(unit_string, options \\ []) do
    case parse(unit_string, options) do
      {:ok, unit} -> unit
      {:error, {exception, message}} -> raise exception, message
    end
  end

  @doc """
  Parse a string to find a matching unit-atom.

  This function attempts to parse a string and
  extract the `unit type`.

  The parsed `unit type` is aliased against all the
  known unit names for a give locale (or the current
  locale if no locale is specified). The known
  aliases for unit types can be returned with
  `MyApp.Cldr.Unit.unit_strings_for/1` where `MyApp.Cldr`
  is the name of a backend module.

  ## Arguments

  * `unit_name_string` is any string to be parsed and converted into a `unit type`

  * `options` is a keyword list of options

  ## Options

  * `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
    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`.

  * `:only` is a unit category or unit, or a list of unit categories and units.
    The parsed unit must match one of the categories or units in order to
    be valid. This is helpful when disambiguating parsed units. For example,
    parsing "w" could be either `watts` or `:week`. Specifying `only: :duration`
    would return `:week`. Specifying `only: :power` would return `:watts`

  * `:except` is the oppostte of `:only`. The parsed unit must *not*
    match the specified unit or category, or unit categories and units.

  ## Returns

  * `unit_name` or

  * raises an exception

  ## Notes

  * When both `:only` and `:except` options are passed, both
    conditions must be true in order to return a parsed result.

  * Only units returned by `Cldr.Unit.known_units/0` can be
    used in the `:only` and `:except` filters.

  ## Examples

      iex> Cldr.Unit.parse_unit_name! "kg"
      :kilogram

      iex> Cldr.Unit.parse_unit_name! "w"
      :watt

      iex> Cldr.Unit.parse_unit_name! "w", only: :duration
      :week

      iex> Cldr.Unit.parse_unit_name! "m", only: [:year, :month, :day]
      :month

      iex> Cldr.Unit.parse_unit_name! "tages", locale: "de"
      :day

      iex> Cldr.Unit.parse_unit_name! "tag", locale: "de"
      :day

      iex> Cldr.Unit.parse_unit_name!("millispangels")
      ** (Cldr.UnknownUnitError) Unknown unit was detected at "spangels"

  """
  @doc since: "3.14.0"

  @spec parse_unit_name!(binary) :: atom() | no_return()
  def parse_unit_name!(unit_name_string, options \\ []) do
    case parse_unit_name(unit_name_string, options) do
      {:ok, unit} -> unit
      {:error, {exception, message}} -> raise exception, message
    end
  end

  defp resolve_unit_alias(unit, strings) do
    unit = String.trim(unit)
    Map.get(strings, unit, unit)
  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, base_conversion) do
      unit = %Unit{
        unit: unit,
        value: value,
        base_conversion: base_conversion,
        usage: usage,
        format_options: format_options
      }

      {:ok, unit}
    end
  end

  @doc false
  def validate_usage(_unit, @default_use = usage, _base_conversion) do
    {:ok, usage}
  end

  def validate_usage(unit, usage, base_conversion) do
    with {:ok, category} <- unit_category(unit, base_conversion) do
      validate_category_usage(category, usage)
    end
  end

  @default_category_usage [@default_use]
  defp validate_category_usage(category, usage) when is_atom(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

  defp validate_category_usage(category, "") do
    validate_category_usage(category, @default_use)
  end

  defp validate_category_usage(category, usage) when is_binary(usage) do
    atom_usage = String.to_existing_atom(usage)
    validate_category_usage(category, atom_usage)
  rescue
    ArgumentError ->
      {:error, unknown_usage_error(category, usage)}
  end

  defp validate_categories_or_units(units_or_categories) do
    {categories, units} = Enum.split_with(units_or_categories, &(&1 in known_unit_categories()))
    invalid_units = Enum.filter(units, &(&1 not in known_units()))

    if invalid_units == [] do
      {:ok, {categories, units}}
    else
      {:error, unit_error(invalid_units)}
    end
  end

  @doc """
  Formats a number into a string according to a unit definition
  for the current process's locale and backend.

  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()]) ::
          {:ok, String.t()} | {:error, {atom, binary}}

  def to_string(unit) do
    Format.to_string(unit)
  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
    `t:Cldr.Unit` struct or a list of `t:Cldr.Unit` 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 `t:Cldr.Unit` 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`

  * `: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.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(
          Unit.value() | Unit.t() | list(Unit.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
    Format.to_string(list_or_unit, options, [])
  end

  def to_string(list_or_unit, backend, options) do
    Format.to_string(list_or_unit, backend, options)
  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 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
    Format.to_string!(unit)
  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 `t:Cldr.Unit` are merged with
  `options` with `options` taking precedence.

  ## Arguments

  * `number` is any number (integer, float or Decimal) or a
    `t:Cldr.Unit` 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` 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.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!(
          Unit.value() | Unit.t() | list(Unit.t()),
          Cldr.backend() | Keyword.t(),
          Keyword.t()
        ) ::
          String.t() | no_return()

  def to_string!(unit, backend, options \\ []) do
    Format.to_string!(unit, backend, options)
  end

  ## TODO remove in ex_cldr 4.0

  @doc false
  @spec to_iolist(list_or_number :: Unit.value() | Unit.t() | [Unit.t()]) ::
          {:ok, String.t()} | {:error, {atom, binary}}

  def to_iolist(unit) do
    Format.to_iolist(unit)
  end

  @doc false
  @spec to_iolist(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.t(), ...], Keyword.t()) ::
          {:ok, list()} | {:error, {atom, binary}}

  def to_iolist(unit, backend, options \\ []) do
    Format.to_iolist(unit, backend, options)
  end

  @doc false
  @spec to_iolist!(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.t(), ...]) ::
          list() | no_return()

  def to_iolist!(unit) do
    Format.to_iolist!(unit)
  end

  @doc false
  @spec to_iolist!(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.t(), ...], Keyword.t()) ::
          list() | no_return()

  def to_iolist!(unit, backend, options \\ []) do
    Format.to_iolist!(unit, backend, options)
  end

  @doc """
  Inverts a unit

  Only "per" units can be inverted.

  """
  @spec invert(t()) :: {:ok, t()} | {:error, {module(), String.t()}}

  def invert(%Unit{value: value, base_conversion: conversion} = unit)
      when is_tuple(conversion) and is_number(value) do
    new_unit = inverted_unit(unit)

    {:ok, %{new_unit | value: invert_value(value)}}
  end

  def invert(%Unit{value: %Decimal{} = value, base_conversion: conversion} = unit)
      when is_tuple(conversion) do
    new_unit = inverted_unit(unit)

    {:ok, %{new_unit | value: invert_value(value)}}
  end

  @per Cldr.Unit.Parser.per()
  @one Decimal.new(1)

  def invert(%Unit{unit: name, value: value}) when is_atom(name) do
    case Atom.to_string(name) |> String.split(@per) do
      [_unit_name] ->
        {:error, not_invertable_error(name)}

      [left, right] ->
        new_name = right <> @per <> left
        new_value = invert_value(value)
        Cldr.Unit.new(new_name, new_value)
    end
  end

  def invert(%Unit{} = unit) do
    {:error, not_invertable_error(unit)}
  end

  defp invert_value(value) when is_number(value) do
    1 / value
  end

  defp invert_value(%Decimal{} = value) do
    Decimal.div(@one, value)
  end

  defp inverted_unit(%Unit{base_conversion: {numerator, denominator}} = unit) do
    new_conversion = {denominator, numerator}
    new_unit = %{unit | base_conversion: new_conversion}

    new_name =
      new_conversion
      |> Cldr.Unit.Parser.canonical_unit_name()
      |> maybe_translatable_unit()

    %{new_unit | unit: new_name}
  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?(t() | unit(), t() | unit()) :: boolean

  def compatible?(unit_1, unit_2) do
    case Conversion.conversion_for(unit_1, unit_2) do
      {:ok, _conversion, _maybe_inverted} -> true
      {:error, _error} -> false
    end
  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, "3.6")]

      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 current process's locale is set with
  `Cldr.put_locale/1`.

  See `Cldr.Unit.localize/3` for further
  details.

  """
  @spec localize(t()) :: [t(), ...]
  def localize(%Unit{} = unit) do
    locale = Cldr.get_locale()
    backend = locale.backend
    localize(unit, backend, locale: locale)
  end

  @doc """
  Localizes a unit according to a territory

  A territory can be derived from a `t:Cldr.Locale.locale_name`
  or `t:Cldr.LangaugeTag`.

  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 audience as `#Cldr.Unit<328, :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, usage: :person_height),
        Cldr.Unit.new!(:inch, "0.04724409448818897637795275598", usage: :person_height)
      ]

  """
  @spec localize(t(), Cldr.backend(), Keyword.t()) :: [t(), ...]
  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
      unit = %{unit | usage: options[:usage] || unit.usage}
      decompose(unit, unit_list, format_options)
    end
  end

  @doc """
  Returns the localized display name
  for a unit.

  The returned text is generally suitable
  for including in UI elements such as
  selection boxes.

  ## Arguments

  * `unit` is any `t:Cldr.Unit` or any
    unit name returned by `Cldr.Unit.known_units/0`.

  * `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 `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`.

  * `:style` is one of those returned by `Cldr.Unit.available_styles`.
    The current styles are `:long`, `:short` and `:narrow`.
    The default is `style: :long`.

  ## Examples

      iex> Cldr.Unit.display_name :liter
      "liters"

      iex> Cldr.Unit.display_name :liter, locale: "fr"
      "litres"

      iex> Cldr.Unit.display_name :liter, locale: "fr", style: :short
      "l"

  """
  @spec display_name(Cldr.Unit.value() | Cldr.Unit.t(), Keyword.t()) ::
          String.t() | {:error, {module, binary}}

  def display_name(unit, options \\ [])

  def display_name(%Unit{unit: unit}, options) do
    display_name(unit, options)
  end

  def display_name(unit, options) when unit in @translatable_units do
    style = Keyword.get(options, :style, @default_style)
    {locale, backend} = Cldr.locale_and_backend_from(options)

    with {:ok, locale} <- Cldr.validate_locale(locale, backend),
         {:ok, style} <- validate_style(style) do
      locale
      |> units_for(style, backend)
      |> Map.fetch!(unit)
      |> Map.fetch!(:display_name)
    end
  end

  def display_name(unit, _options) do
    if match?({:ok, _unit, _conversion}, validate_unit(unit)) do
      {:error, unit_not_translatable_error(unit)}
    else
      {:error, unit_error(unit)}
    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.new!(:foot, "23.3")
      iex> Cldr.Unit.zero(u)
      Cldr.Unit.new!(:foot, 0)

  """
  def zero(%Unit{value: _value} = unit) do
    %Unit{unit | value: 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.new!(:foot, 23.3)
      iex> Cldr.Unit.zero?(u)
      false

      iex> u = Cldr.Unit.new!(:foot, 0)
      Cldr.Unit.new!(: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.Decimal.compare(value, @decimal_0) == :eq
  end

  @system_units @units
                |> Map.get(:conversions)
                |> Enum.flat_map(fn {unit, conversion} ->
                  Enum.map(conversion.systems, fn system -> {system, unit} end)
                end)
                |> Enum.group_by(fn {system, _unit} -> system end, fn {_system, unit} ->
                  unit
                end)

  @doc """
  Returns the list of units defined in a given
  measurement system.

  ## Arguments

  * `system` is any measurement system returned by
    `Cldr.Unit.known_measurement_systems/0`

  ## Returns

  * A list of translatable units as atoms or

  * `{:error, {exception, message}}`

  ## Example

      => Cldr.Unit.measurement_system_units :uksystem
      [
        :ton,
        :inch,
        :yard,
        ...
      ]

  """
  @doc since: "3.4.0"
  @spec measurement_system_units(measurement_system()) ::
          [translatable_unit(), ...] | {:error, {module(), String.t()}}

  def measurement_system_units(system) when system in @system_names do
    Map.get(@system_units, system)
  end

  def measurement_system_units(system) do
    {:error, unknown_measurement_system_error(system)}
  end

  @doc """
  Returns a boolean indicating if a given
  unit belongs to one or more measurement
  systems.

  When a list or more than one measurement
  system is provided, the test is one of
  inclusion. That is, if the unit belongs to
  any of the provided measurement systems
  the return is `true`.

  ## Arguments

  * `system` is any measurement system or list
    of measurement systems returned by
    `Cldr.Unit.known_measurement_systems/0`

  ## Returns

  * `true` or `false` or

  * `{:error, {exception, message}}`

  ## Examples

      iex> Cldr.Unit.measurement_system? :foot, :uksystem
      true

      iex> Cldr.Unit.measurement_system? :foot, [:uksystem, :ussystem]
      true

      iex> Cldr.Unit.measurement_system? :foot, [:metric]
      false

      iex> Cldr.Unit.measurement_system? :invalid, [:metric]
      {:error, {Cldr.UnknownUnitError, "The unit :invalid is not known."}}

  """
  @doc since: "3.4.0"
  @spec measurement_system?(t() | unit, measurement_system | list(measurement_system)) ::
          boolean() | {:error, {module(), String.t()}}

  def measurement_system?(unit, system) when is_atom(system) do
    measurement_system?(unit, [system])
  end

  def measurement_system?(%Unit{unit: unit}, systems) when is_list(systems) do
    measurement_system?(unit, systems)
  end

  def measurement_system?(unit, systems) when unit in @translatable_units and is_list(systems) do
    Enum.any?(measurement_systems_for_unit(unit), &(&1 in systems))
  end

  def measurement_system?(_unit, systems) when not is_list(systems) do
    {:error, unknown_measurement_system_error(systems)}
  end

  def measurement_system?(unit, _systems) do
    {:error, unit_error(unit)}
  end

  @systems_for_unit @units
                    |> Map.get(:conversions)
                    |> Enum.map(fn {unit, conversion} -> {unit, conversion.systems} end)
                    |> Kernel.++(Cldr.Unit.Additional.systems_for_units())
                    |> Map.new()

  @doc """
  Returns the measurement systems for a given
  unit.

  ## Arguments

  * `unit` is any `t:Cldr.Unit` or any unit
    returned by `Cldr.Unit.known_units/0` or a
    string unit name.

  ## Returns

  * A list of measurement systems to which
    the `unit` belongs.

  * `{:error, {exception, message}}`

  ## Examples

      iex> Cldr.Unit.measurement_systems_for_unit :foot
      [:ussystem, :uksystem]

      iex> Cldr.Unit.measurement_systems_for_unit :meter
      [:metric, :si]

      iex> Cldr.Unit.measurement_systems_for_unit :invalid
      {:error, {Cldr.UnknownUnitError, "The unit :invalid is not known."}}

  """
  @doc since: "3.4.0"
  @spec measurement_systems_for_unit(t() | unit()) ::
          [measurement_system(), ...] | {:error, {module(), String.t()}}

  def measurement_systems_for_unit(%Unit{unit: unit}) do
    measurement_systems_for_unit(unit)
  end

  def measurement_systems_for_unit(unit) when unit in @translatable_units do
    case Map.fetch(@systems_for_unit, unit) do
      {:ok, systems} -> systems
      :error -> measurement_systems_for_unit(Kernel.to_string(unit))
    end
  end

  # Strip SI and power factors amd try the root
  # unit
  for prefix <- Cldr.Unit.Prefix.prefixes() do
    def measurement_systems_for_unit(unquote(prefix) <> unit) do
      with {:ok, unit, _conversion} <- Cldr.Unit.validate_unit(unit) do
        measurement_systems_for_unit(unit)
      end
    end
  end

  def measurement_systems_for_unit("per_" <> unit) do
    measurement_systems_for_unit(unit)
  end

  # Decompose the unit recursively until there is a match on
  # a base unit, otherwise return an error
  def measurement_systems_for_unit(unit) when is_binary(unit) do
    [first | rest] = String.split(unit, "_", parts: 2)

    with {:ok, part_unit, _conversion} <- Cldr.Unit.validate_unit(first) do
      measurement_systems_for_unit(part_unit)
    else
      _other ->
        if rest == [], do: {:error, unit_error(unit)}, else: measurement_systems_for_unit(hd(rest))
    end
  end

  def measurement_systems_for_unit(unit) do
    {:error, unit_error(unit)}
  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"},
        si: %{alias: nil, description: "SI System"}
      }

  """
  @spec known_measurement_systems ::
          %{measurement_system() => %{alias: atom(), description: String.t()}}

  def known_measurement_systems do
    @measurement_systems
  end

  @doc """
  Return a list of known measurement system names.

  ## Example

      iex> Cldr.Unit.known_measurement_system_names()
      [:metric, :si, :uksystem, :ussystem]

  """
  @doc since: "3.5.0"
  @spec known_measurement_system_names :: [measurement_system(), ...]

  def known_measurement_system_names do
    @system_names
  end

  @doc """
  Determines the preferred measurement system
  from a locale.

  See also `Cldr.Unit.known_measurement_systems/0`.

  ## Arguments

  * `locale` is any valid locale name returned by
    `Cldr.known_locale_names/0` or a `t:Cldr.LanguageTag`
    struct.  The default is `Cldr.get_locale/0`.

  * `key` is any measurement system key.
    The known keys are `:default`, `:temperature`
    and `:paper_size`. The default key is `:default`.

  ## Examples

      iex> Cldr.Unit.measurement_system_from_locale "en"
      :ussystem

      iex> Cldr.Unit.measurement_system_from_locale "en-GB"
      :uksystem

      iex> Cldr.Unit.measurement_system_from_locale "en-AU"
      :metric

      iex> Cldr.Unit.measurement_system_from_locale "en-AU-u-ms-ussystem"
      :ussystem

      iex> Cldr.Unit.measurement_system_from_locale "en-GB", :temperature
      :uksystem

      iex> Cldr.Unit.measurement_system_from_locale "en-AU", :paper_size
      :a4

      iex> Cldr.Unit.measurement_system_from_locale "en-GB", :invalid
      {:error,
       {Cldr.Unit.InvalidSystemKeyError,
        "The key :invalid is not known. Valid keys are :default, :paper_size and :temperature"}}

  """
  @doc since: "3.4.0"
  @spec measurement_system_from_locale(
          Cldr.LanguageTag.t() | Cldr.Locale.locale_name(),
          measurement_system_key() | Cldr.backend()
        ) ::
          measurement_system() | {:error, {module(), String.t()}}

  def measurement_system_from_locale(locale \\ Cldr.get_locale(), key \\ :default)

  def measurement_system_from_locale(locale, backend_or_key) when is_binary(locale) do
    case Cldr.validate_backend(backend_or_key) do
      {:ok, backend} ->
        with {:ok, locale} <- Cldr.validate_locale(locale, backend) do
          measurement_system_from_locale(locale)
        end

      {:error, _} ->
        with {:ok, locale} <- Cldr.validate_locale(locale) do
          measurement_system_from_locale(locale, backend_or_key)
        end
    end
  end

  def measurement_system_from_locale(%LanguageTag{locale: %{ms: :imperial}}, _key) do
    :uksystem
  end

  def measurement_system_from_locale(%LanguageTag{locale: %{ms: system}}, _key)
      when not is_nil(system) do
    system
  end

  def measurement_system_from_locale(%Cldr.LanguageTag{} = locale, key) do
    territory = Cldr.Locale.territory_from_locale(locale)
    measurement_system_for_territory(territory, key)
  end

  @doc since: "3.4.0"
  @spec measurement_system_from_locale(
          Locale.locale_reference(),
          Cldr.backend(),
          measurement_system_key()
        ) ::
          measurement_system() | {:error, {module(), String.t()}}

  def measurement_system_from_locale(locale, backend, key) do
    with {:ok, locale} <- Cldr.validate_locale(locale, backend) do
      measurement_system_from_locale(locale, key)
    end
  end

  @category_usage @units
                  |> Map.get(:preferences)
                  |> Enum.map(fn {k, v} -> {k, Map.keys(v) |> Enum.sort()} end)
                  |> Map.new()

  @doc """
  Returns a mapping between Unit categories
  and the uses they define.

  ## Example

      iex> Cldr.Unit.unit_category_usage()
      %{
        area: [:default, :floor, :geograph, :land],
        concentration: [:blood_glucose, :default],
        consumption: [:default, :vehicle_fuel],
        duration: [:default, :media],
        energy: [:default, :food],
        length: [:default, :focal_length, :person, :person_height, :rainfall, :road,
         :snowfall, :vehicle, :visiblty],
        mass: [:default, :person],
        mass_density: [:default],
        power: [:default, :engine],
        pressure: [:baromtrc, :default],
        speed: [:default, :rainfall, :snowfall, :wind],
        temperature: [:default, :weather],
        volume: [:default, :fluid, :oil, :vehicle],
        year_duration: [:default, :person_age]
      }

  """
  def unit_category_usage do
    @category_usage
  end

  @doc """
  Returns a mapping from unit categories to the
  base unit.

  """
  @base_units @units
              |> Map.get(:base_units)
              |> Kernel.++(Cldr.Unit.Additional.base_units())
              |> Enum.uniq()
              |> Map.new()

  def base_units do
    @base_units
  end

  @doc """
  Returns the base unit for a given unit.

  ## Argument

  * `unit` is either a `t:Cldr.Unit`, an `atom` or
    a `t:String`

  ## 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{base_conversion: conversion}) do
    BaseUnit.canonical_base_unit(conversion)
  end

  def base_unit(unit_name) when is_atom(unit_name) or is_binary(unit_name) do
    with {:ok, _unit, conversion} <- Cldr.Unit.validate_unit(unit_name) do
      BaseUnit.canonical_base_unit(conversion)
    end
  end

  @deprecated "Use `Cldr.Unit.known_unit_categories/0"
  defdelegate unit_categories(), to: __MODULE__, as: :known_unit_categories

  @unit_category_inverse_map Cldr.Map.invert(@units_by_category) |> Cldr.Map.stringify_keys()

  @doc false
  def unit_category_inverse_map do
    @unit_category_inverse_map
  end

  @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}

      iex> Cldr.Unit.unit_category :watt
      {:ok, :power}

      iex> Cldr.Unit.unit_category "kilowatt hour"
      {:ok, :energy}

      iex> Cldr.Unit.unit_category "watt hour per light year"
      {:error,
       {Cldr.Unit.UnknownCategoryError,
        "The category for \\"watt hour per light year\\" is not known."}}

  """

  @spec unit_category(Unit.t() | String.t() | atom()) ::
          {:ok, category()} | {:error, {module(), String.t()}}

  def unit_category(unit) when unit in @translatable_units do
    case Map.fetch(@unit_category_inverse_map, Kernel.to_string(unit)) do
      {:ok, category} -> {:ok, category}
      :error -> {:error, unknown_category_error(unit)}
    end
  end

  def unit_category(unit) do
    with {:ok, resolved_unit, conversion} <- validate_unit(unit) do
      if resolved_unit in @translatable_units do
        unit_category(resolved_unit)
      else
        unit_category(unit, conversion)
      end
    end
  end

  @doc false
  def unit_category(unit, conversion) do
    with {:ok, base_unit} <- BaseUnit.canonical_base_unit(conversion),
         {:ok, category} <- Map.fetch(@unit_category_inverse_map, Kernel.to_string(base_unit)) do
      {:ok, category}
    else
      :error -> {:error, unknown_category_error(unit)}
      other -> other
    end
  end

  @deprecated "Please use `Cldr.Unit.unit_category/1"
  def unit_type(unit) do
    unit_category(unit)
  end

  @doc """
  Return the grammatical gender for a
  unit.

  ## Arguments

  ## Options

  ## Returns

  ## Examples

  """
  @unknown_gender :undefined
  @doc since: "3.5.0"
  @spec grammatical_gender(t(), Keyword.t()) :: grammatical_gender()

  def grammatical_gender(unit, options \\ [])

  def grammatical_gender(%__MODULE__{} = unit, options) when is_list(options) do
    {locale, backend} = Cldr.locale_and_backend_from(options)
    module = Module.concat(backend, :Unit)
    units = units_for(locale, :long)

    features =
      module.grammatical_features(@root_locale_name)
      |> Map.merge(module.grammatical_features(locale))
      |> Map.fetch!(:gender)

    Cldr.Unit.Format.traverse(unit, fn
      {:unit, unit} -> Map.fetch!(units, unit) |> Map.get(:gender, @unknown_gender)
      {:times, left_right} -> elem(left_right, features.times)
      {:per, left_right} -> elem(left_right, features.per)
      {:prefix, left_right} -> elem(left_right, features.prefix + 1)
      {:power, left_right} -> elem(left_right, features.power + 1)
    end)
  end

  def grammatical_gender(unit, options) when is_binary(unit) do
    grammatical_gender(new!(1, unit), options)
  end

  @base_unit_category_map Cldr.Config.units()
                          |> Map.get(:base_units)
                          # |> Kernel.++(Cldr.Unit.Additional.base_units())
                          |> Enum.map(fn {k, v} -> {Kernel.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.known_styles
      [:long, :short, :narrow]

  """
  @spec known_styles :: [style(), ...]
  def known_styles do
    @styles
  end

  @deprecated "Use Cldr.Unit.known_styles/0"
  defdelegate styles, to: __MODULE__, as: :known_styles

  @doc """
  Returns the default formatting style.

  ## Example

      iex> Cldr.Unit.default_style
      :long

  """
  @spec default_style :: style()
  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 given unit to meet this
  # requirement.
  #
  # Unit preferences can vary by usage, not just territory,
  # Therefore the data is structured according to unit
  # category and unit usage.
  #
  # This function is called at compile time to generate
  # preference functions in `Cldr.Unit.Preference`.

  @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: 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 \\ default_backend()) do
    module = Module.concat(backend, Elixir.Unit)
    module.units_for(locale, style)
  end

  @systems_by_territory Cldr.Config.territories()
                        |> Enum.map(fn {k, v} -> {k, v.measurement_system} end)
                        |> Map.new()

  @doc """
  Returns a map of measurement systems by territory.

  ## Example

      => Cldr.Unit.measurement_systems_by_territory
      %{
        KE: %{default: :metric, paper_size: :a4, temperature: :metric},
        GU: %{default: :metric, paper_size: :a4, temperature: :metric},
        ...
      }

  """
  @spec measurement_systems_by_territory() :: %{Locale.territory_code() => map()}
  def measurement_systems_by_territory do
    @systems_by_territory
  end

  @deprecated "Use Cldr.Unit.measurement_systems_by_territory/0"
  defdelegate measurement_systems(), to: __MODULE__, as: :measurement_systems_by_territory

  @doc """
  Returns the default measurement system for a territory
  and a given system key.

  ## Arguments

  * `territory` is any valid territory returned by
    `Cldr.validate_territory/1`

  * `key` is any measurement system key.
    The known keys are `:default`, `:temperature`
    and `:paper_size`. The default key is `:default`.

  ## Examples

      iex> Cldr.Unit.measurement_system_for_territory :US
      :ussystem

      iex> Cldr.Unit.measurement_system_for_territory :GB
      :uksystem

      iex> Cldr.Unit.measurement_system_for_territory :AU
      :metric

      iex> Cldr.Unit.measurement_system_for_territory :US, :temperature
      :ussystem

      iex> Cldr.Unit.measurement_system_for_territory :GB, :temperature
      :uksystem

      iex> Cldr.Unit.measurement_system_for_territory :GB, :volume
      {:error,
       {Cldr.Unit.InvalidSystemKeyError,
        "The key :volume is not known. Valid keys are :default, :paper_size and :temperature"}}

  """
  @spec measurement_system_for_territory(atom(), atom()) ::
          :metric | :ussystem | :uksystem | :a4 | :us_letter | {:error, {module(), String.t()}}

  def measurement_system_for_territory(territory, category \\ :default)

  def measurement_system_for_territory(territory, :default = key) do
    do_measurement_system_for_territory(territory, key, :metric)
  end

  def measurement_system_for_territory(territory, :paper_size = key) do
    do_measurement_system_for_territory(territory, key, :a4)
  end

  def measurement_system_for_territory(territory, :temperature = key) do
    do_measurement_system_for_territory(territory, key, :metric)
  end

  def measurement_system_for_territory(_territory, key) do
    {:error, invalid_system_key_error(key)}
  end

  defp do_measurement_system_for_territory(territory, key, default) do
    with {:ok, territory} <- Cldr.validate_territory(territory) do
      get_in(measurement_systems_by_territory(), [territory, key]) || default
    end
  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 `t:String` 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 `t:String`, supplied
    as is or as part of an `t:Cldr.Unit` struct.

  ## 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 `t:String` 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{
            factor: Decimal.new("1609.3440"),
            offset: 0,
            base_unit: [:meter]
          }
        ],
        [
          liter: %Cldr.Unit.Conversion{
            factor: Decimal.new("0.001"),
            offset: 0,
            base_unit: [:cubic_meter]
          }
        ]}}

  """
  def validate_unit(unit_name) when unit_name in @translatable_units do
    {:ok, unit_name, Conversions.conversion_for!(unit_name)}
  end

  @aliases Alias.aliases() |> Map.keys() |> Enum.sort()
  def validate_unit(unit_name) when unit_name in @aliases do
    unit_name
    |> Alias.alias()
    |> validate_unit()
  end

  def validate_unit(unit_name) when is_atom(unit_name) do
    unit_name
    |> Atom.to_string()
    |> validate_unit()
  end

  def validate_unit(unit_name) when is_binary(unit_name) do
    unit_name
    |> normalize_unit_name()
    |> maybe_translatable_unit()
    |> return_parsed_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

  defp return_parsed_unit(unit_name) when is_atom(unit_name) do
    validate_unit(unit_name)
  end

  defp return_parsed_unit(unit_name) do
    with {:ok, parsed} <- Parser.parse_unit(unit_name) do
      name =
        parsed
        |> Parser.canonical_unit_name()
        |> maybe_translatable_unit()

      {:ok, name, parsed}
    end
  end

  @doc false
  def normalize_unit_name(name) when is_binary(name) do
    String.replace(name, [" ", "-"], "_")
  end

  @doc false
  def maybe_translatable_unit(name) do
    atom_name = String.to_existing_atom(name)
    if atom_name in known_units(), do: atom_name, else: name
  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 """
  Validates a grammatical case and normalizes it to a
  standard downcased atom form

  """
  @doc since: "3.5.0"
  def validate_grammatical_case(grammatical_case) when grammatical_case in @grammatical_case do
    {:ok, grammatical_case}
  end

  def validate_grammatical_case(grammatical_case) when is_binary(grammatical_case) do
    grammatical_case
    |> String.downcase()
    |> String.to_existing_atom()
    |> validate_grammatical_case()
  catch
    ArgumentError ->
      {:error, grammatical_case_error(grammatical_case)}
  end

  def validate_grammatical_case(grammatical_case) do
    {:error, grammatical_case_error(grammatical_case)}
  end

  @doc false
  def validate_grammatical_gender(nil, default_gender, locale) do
    validate_grammatical_gender(default_gender, locale)
  end

  def validate_grammatical_gender(grammatical_gender, _default_gender, locale) do
    validate_grammatical_gender(grammatical_gender, locale)
  end

  @doc """
  Validates a grammatical gender and normalizes it to a
  standard downcased atom form

  """
  @doc since: "3.5.0"
  def validate_grammatical_gender(grammatical_gender, locale \\ Cldr.default_locale())

  def validate_grammatical_gender(grammatical_gender, %LanguageTag{} = locale)
      when is_atom(grammatical_gender) do
    with {:ok, genders} = Module.concat(locale.backend, :Unit).grammatical_gender(locale) do
      if grammatical_gender in genders do
        {:ok, grammatical_gender}
      else
        {:error, grammatical_gender_error(grammatical_gender, genders, locale)}
      end
    end
  end

  def validate_grammatical_gender(grammatical_gender, %LanguageTag{} = locale)
      when is_binary(grammatical_gender) do
    grammatical_gender
    |> String.downcase()
    |> String.to_existing_atom()
    |> validate_grammatical_gender(locale)
  catch
    ArgumentError ->
      {:error, grammatical_gender_error(grammatical_gender, locale)}
  end

  @doc """
  Convert a ratio, Decimal or integer `t:Unit` to a float `t:Unit`
  """
  @doc since: "3.5.0"
  def to_float_unit(%Unit{value: value} = unit) when is_integer(value) do
    value = 1.0 * value
    %{unit | value: value}
  end

  def to_float_unit(%Unit{value: %Decimal{} = value} = unit) do
    value = Decimal.to_float(value)
    %{unit | value: value}
  end

  def to_float_unit(%Unit{} = other) do
    other
  end

  @deprecated "Use Cldr.Unit.to_float_unit/1"
  defdelegate ratio_to_float(unit), to: __MODULE__, as: :to_float_unit

  @doc """
  Convert a ratio, float or integer `t:Unit` to a Decimal `t:Unit`
  """
  @doc since: "3.5.0"
  def to_decimal_unit(%Unit{value: value} = unit) when is_float(value) do
    value = Decimal.from_float(value)
    %{unit | value: value}
  end

  def to_decimal_unit(%Unit{value: value} = unit) when is_integer(value) do
    value = Decimal.new(value)
    %{unit | value: value}
  end

  def to_decimal_unit(%Unit{} = other) do
    other
  end

  @deprecated "Use Cldr.Unit.to_decimal_unit/1"
  defdelegate ratio_to_decimal(unit), to: __MODULE__, as: :to_decimal_unit

  @doc false
  def unknown_base_unit_error(unit_name) do
    {Cldr.Unit.UnknownBaseUnitError, "Base unit for #{inspect(unit_name)} is not known"}
  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

  def unit_error(units) when is_list(units) do
    units = Enum.sort(units)
    {Cldr.UnknownUnitError, "The units #{inspect(units)} are not known."}
  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 unit_categories_error([category]) do
    unit_category_error(category)
  end

  @doc false
  def unit_categories_error(categories) do
    {Cldr.Unit.UnknownUnitCategoryError,
     "The unit categories #{inspect(categories)} are not known."}
  end

  @doc false
  def unknown_category_error(unit) do
    {Cldr.Unit.UnknownCategoryError, "The category for #{inspect(unit)} 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 grammatical_case_error(grammatical_case) do
    {
      Cldr.UnknownGrammaticalCaseError,
      "The grammatical case #{inspect(grammatical_case)} " <>
        "is not known. The valid cases are #{inspect(@grammatical_case)}"
    }
  end

  @doc false
  def grammatical_gender_error(grammatical_gender, known_genders, locale) do
    {
      Cldr.UnknownGrammaticalGenderError,
      "The locale #{inspect(locale.cldr_locale_name)} does not define " <>
        "a grammatical gender #{inspect(grammatical_gender)}. " <>
        "The valid genders are #{inspect(known_genders)}"
    }
  end

  def grammatical_gender_error(grammatical_gender, _locale) do
    {
      Cldr.UnknownGrammaticalGenderError,
      "The grammatical gender #{inspect(grammatical_gender)} is invalid"
    }
  end

  @doc false
  def incompatible_units_error(%Unit{unit: unit_1}, unit_2) do
    incompatible_units_error(unit_1, unit_2)
  end

  def incompatible_units_error(unit_1, %Unit{unit: unit_2}) do
    incompatible_units_error(unit_1, unit_2)
  end

  def incompatible_units_error(unit_1, unit_2) do
    {
      Unit.IncompatibleUnitsError,
      "Operations can only be performed between units with the same base unit. " <>
        "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

  @doc false
  def unit_not_translatable_error(unit) do
    {
      Cldr.Unit.UnitNotTranslatableError,
      "The unit #{inspect(unit)} is not translatable"
    }
  end

  @doc false
  def unknown_measurement_system_error(system) do
    {
      Cldr.Unit.UnknownMeasurementSystemError,
      "The measurement system #{inspect(system)} is not known"
    }
  end

  @doc false
  def invalid_system_key_error(key) do
    {
      Cldr.Unit.InvalidSystemKeyError,
      "The key #{inspect(key)} is not known. " <>
        "Valid keys are :default, :paper_size and :temperature"
    }
  end

  @doc false
  def no_pattern_error(unit, locale, style) do
    {
      Cldr.Unit.NoPatternError,
      "No localisation pattern was found for unit #{inspect(unit)} in " <>
        "locale #{inspect(locale.requested_locale_name)} for " <>
        "style #{inspect(style)}"
    }
  end

  @doc false
  def not_invertable_error(unit) do
    {
      Cldr.Unit.NotInvertableError,
      "The unit #{inspect(unit)} cannot be inverted. Only compound 'per' units " <>
        "can be inverted"
    }
  end

  @doc false
  def not_parseable_error(string) do
    {
      Cldr.Unit.NotParseableError,
      "The string #{inspect(string)} could not be parsed as a unit and a value"
    }
  end

  @doc false
  def ambiguous_unit_error(unit, units) do
    units = Enum.sort(units)

    {
      Cldr.Unit.AmbiguousUnitError,
      "The string #{inspect(String.trim(unit))} ambiguously resolves to #{inspect(units)}"
    }
  end

  @doc false
  defp category_unit_match_error(unit, only, {[], []}) do
    {
      Cldr.Unit.CategoryMatchError,
      "None of the units #{inspect(Enum.sort(unit))} belong to a unit or category matching " <>
        "only: #{inspect(flatten(only))}"
    }
  end

  defp category_unit_match_error(unit, {[], []}, except) do
    {
      Cldr.Unit.CategoryMatchError,
      "None of the units #{inspect(Enum.sort(unit))} belong to a unit or category matching " <>
        "except: #{inspect(flatten(except))}"
    }
  end

  defp category_unit_match_error(unit, only, except) do
    {
      Cldr.Unit.CategoryMatchError,
      "None of the units #{inspect(Enum.sort(unit))} belong to a unit or category matching " <>
        "only: #{inspect(flatten(only))} except: #{inspect(flatten(except))}"
    }
  end

  defp flatten(tuple) when is_tuple(tuple) do
    tuple
    |> Tuple.to_list()
    |> List.flatten()
  end

  defp int_rem(unit) do
    integer_unit = Unit.round(unit, 0, :down) |> Math.trunc()
    remainder = Math.sub(unit, integer_unit)
    {integer_unit, remainder}
  end

  @doc false
  # TODO remove for Cldr 3.0
  if Code.ensure_loaded?(Cldr) && function_exported?(Cldr, :default_backend!, 0) do
    def default_backend do
      Cldr.default_backend!()
    end
  else
    def default_backend do
      Cldr.default_backend()
    end
  end

  defimpl Cldr.DisplayName do
    def display_name(unit, options) do
      Cldr.Unit.display_name(unit, options)
    end
  end

  @doc false
  def exclude_protocol_implementation(module) do
    exclusions =
      :ex_unit
      |> Application.get_env(:exclude_protocol_implementations, [])
      |> List.wrap()

    module in exclusions
  end

  def to_float(%Decimal{} = value) do
    Decimal.to_float(value)
    |> Cldr.Math.round(@rounding)
  end

  def to_float(other) do
    other
  end
end