lib/cldr/unit/backend.ex

defmodule Cldr.Unit.Backend do
  def define_unit_module(config) do
    module = inspect(__MODULE__)
    backend = config.backend
    additional_units = Module.concat(backend, Unit.Additional)
    config = Macro.escape(config)

    quote location: :keep,
          bind_quoted: [
            module: module,
            backend: backend,
            config: config,
            additional_units: additional_units
          ] do
      # Create an empty additional units module if it wasn't previously
      # defined
      unless Code.ensure_loaded?(additional_units) do
        defmodule additional_units do
          @moduledoc false
          def known_locales do
            []
          end

          def units_for(_locale, _style) do
            %{}
          end

          def additional_units do
            []
          end
        end
      end

      defmodule Unit do
        @moduledoc false
        if Cldr.Config.include_module_docs?(config.generate_docs) do
          @moduledoc """
          Supports the CLDR Units definitions which provide for the localization of many
          unit types.

          """
        end

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

        alias Cldr.Math

        defdelegate new(unit, value), to: Cldr.Unit
        defdelegate new!(unit, value), to: Cldr.Unit
        defdelegate compatible?(unit_1, unit_2), to: Cldr.Unit
        defdelegate value(unit), to: Cldr.Unit
        defdelegate zero(unit), to: Cldr.Unit
        defdelegate zero?(unit), to: Cldr.Unit
        defdelegate decompose(unit, list), to: Cldr.Unit
        defdelegate localize(unit, usage, options), to: Cldr.Unit

        defdelegate measurement_system_from_locale(locale), to: Cldr.Unit
        defdelegate measurement_system_from_locale(locale, category), to: Cldr.Unit
        defdelegate measurement_system_from_locale(locale, backend, category), to: Cldr.Unit

        defdelegate measurement_systems_for_unit(unit), to: Cldr.Unit

        defdelegate measurement_system_for_territory(territory), to: Cldr.Unit
        defdelegate measurement_system_for_territory(territory, key), to: Cldr.Unit

        defdelegate measurement_system?(unit, systems), to: Cldr.Unit

        @deprecated "Use #{inspect(__MODULE__)}.measurement_system_for_territory/1"
        defdelegate measurement_system_for(territory),
          to: Cldr.Unit,
          as: :measurement_system_for_territory

        @deprecated "Use #{inspect(__MODULE__)}.measurement_system_for_territory/2"
        defdelegate measurement_system_for(territory, key),
          to: Cldr.Unit,
          as: :measurement_system_for_territory

        defdelegate known_units, to: Cldr.Unit
        defdelegate known_unit_categories, to: Cldr.Unit
        defdelegate known_styles, to: Cldr.Unit
        defdelegate styles, to: Cldr.Unit, as: :known_styles
        defdelegate default_style, to: Cldr.Unit

        defdelegate validate_unit(unit), to: Cldr.Unit
        defdelegate validate_style(unit), to: Cldr.Unit
        defdelegate unit_category(unit), to: Cldr.Unit

        defdelegate add(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate sub(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate mult(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate div(unit_1, unit_2), to: Cldr.Unit.Math

        defdelegate add!(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate sub!(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate mult!(unit_1, unit_2), to: Cldr.Unit.Math
        defdelegate div!(unit_1, unit_2), to: Cldr.Unit.Math

        defdelegate round(unit, places, mode), to: Cldr.Unit.Math
        defdelegate round(unit, places), to: Cldr.Unit.Math
        defdelegate round(unit), to: Cldr.Unit.Math

        defdelegate convert(unit_1, to_unit), to: Cldr.Unit.Conversion
        defdelegate convert!(unit_1, to_unit), to: Cldr.Unit.Conversion

        @doc """
        Formats a number into a string according to a unit definition for a locale.

        ## 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

        * `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`.
          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. The default gender is `#{inspect __MODULE__}.default_gender/1`
          for the given locale.

        * `: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> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 123)
            {:ok, "123 gallons"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1)
            {:ok, "1 gallon"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1), locale: "af"
            {:ok, "1 gelling"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1), locale: "af-NA"
            {:ok, "1 gelling"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1), locale: "bs"
            {:ok, "1 galon"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1234), format: :long
            {:ok, "1 thousand gallons"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:gallon, 1234), format: :short
            {:ok, "1K gallons"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:megahertz, 1234)
            {:ok, "1,234 megahertz"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:megahertz, 1234), style: :narrow
            {:ok, "1,234MHz"}

            iex> #{inspect(__MODULE__)}.to_string Cldr.Unit.new!(:megabyte, 1234), locale: "en", style: :unknown
            {:error, {Cldr.UnknownFormatError, "The unit style :unknown is not known."}}

        """
        @spec to_string(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.t(), ...], Keyword.t()) ::
                {:ok, String.t()} | {:error, {atom, binary}}

        def to_string(number, options \\ []) do
          Cldr.Unit.Format.to_string(number, unquote(backend), options)
        end

        @doc """
        Formats a list using `to_string/3` but raises if there is
        an error.

        ## 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

        * `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`.
          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. The default gender is `#{inspect __MODULE__}.default_gender/1`
          for the given locale.

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

        * `formatted_string` or

        * raises an exception

        ## Examples

            iex> #{inspect(__MODULE__)}.to_string! 123, unit: :gallon
            "123 gallons"

            iex> #{inspect(__MODULE__)}.to_string! 1, unit: :gallon
            "1 gallon"

            iex> #{inspect(__MODULE__)}.to_string! 1, unit: :gallon, locale: "af"
            "1 gelling"

        """
        @spec to_string!(Cldr.Unit.value() | Cldr.Unit.t() | [Cldr.Unit.t(), ...], Keyword.t()) ::
                String.t() | no_return()

        def to_string!(number, options \\ []) do
          Cldr.Unit.Format.to_string!(number, unquote(backend), options)
        end

        @doc """
        Formats a number into an iolist according to a unit definition
        for a locale.

        ## 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

        * `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`.
          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. The default gender is `#{inspect __MODULE__}.default_gender/1`
          for the given locale.

        * `:list_options` is a keyword list of options for formatting a list
          which is passed through to `Cldr.List.to_string/3`. This is only
          applicable when formatting a list of units.

        * Any other options are passed to `Cldr.Number.to_string/2`
          which is used to format the `number`

        ## Returns

        * `{:ok, io_list}` or

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

        ## Examples

            iex> #{inspect(__MODULE__)}.to_iolist Cldr.Unit.new!(:gallon, 123)
            {:ok, ["123", " gallons"]}

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

        def to_iolist(number, options \\ []) do
          Cldr.Unit.Format.to_iolist(number, unquote(backend), options)
        end

        @doc """
        Formats a unit using `to_iolist/3` but raises if there is
        an error.

        ## 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

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

        * `: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. The default gender is `#{inspect __MODULE__}.default_gender/1`
          for the given locale.

        * `:list_options` is a keyword list of options for formatting a list
          which is passed through to `Cldr.List.to_string/3`. This is only
          applicable when formatting a list of units.

        * Any other options are passed to `Cldr.Number.to_string/2`
          which is used to format the `number`

        ## Returns

        * `io_list` or

        * raises an exception

        ## Examples

            iex> #{inspect(__MODULE__)}.to_iolist! 123, unit: :gallon
            ["123", " gallons"]

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

        def to_iolist!(number, options \\ []) do
          Cldr.Unit.Format.to_iolist!(number, unquote(backend), options)
        end

        @doc """
        Returns a list of the preferred units for a given
        unit, locale, use case and scope.

        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
          type of length. The available usage for a given unit category can
          be seen with `Cldr.Unit.unit_category_usage/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, formatting_options}` or

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

        ## Notes

        `formatting_options` is a keyword list of options
        that can be passed to `Cldr.Unit.to_string/3`. Its
        primary intended usage is for localizing a unit that
        decomposes into more than one unit (for example when
        2 meters might become 6 feet 6 inches.) In such
        cases, the last unit in the list (in this case the
        inches) is formatted with the `formatting_options`.

        ## Examples

            iex> meter = Cldr.Unit.new!(:meter, 1)
            iex> #{inspect(__MODULE__)}.preferred_units meter, locale: "en-US", usage: :person_height
            {:ok, [:foot, :inch], []}
            iex> #{inspect(__MODULE__)}.preferred_units meter, locale: "en-US", usage: :person
            {:ok, [:inch], []}
            iex> #{inspect(__MODULE__)}.preferred_units meter, locale: "en-AU", usage: :person
            {:ok, [:centimeter], []}
            iex> #{inspect(__MODULE__)}.preferred_units meter, locale: "en-US", usage: :road
            {:ok, [:foot], [round_nearest: 1]}
            iex> #{inspect(__MODULE__)}.preferred_units meter, locale: "en-AU", usage: :road
            {:ok, [:meter], [round_nearest: 1]}

        """
        def preferred_units(unit, options \\ []) do
          Cldr.Unit.Preference.preferred_units(unit, unquote(backend), options)
        end

        @doc """
        Returns a list of the preferred units for a given
        unit, locale, use case and scope.

        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
          type of length. The available usage for a given unit category can
          be seen with `Cldr.Unit.unit_category_usage/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

        * `unit_list` or

        * raises an exception

        ## Examples

            iex> meter = Cldr.Unit.new!(:meter, 2)
            iex> #{inspect(__MODULE__)}.preferred_units! meter, locale: "en-US", usage: :person_height
            [:foot, :inch]
            iex> #{inspect(__MODULE__)}.preferred_units! meter, locale: "en-AU", usage: :person
            [:centimeter]
            iex> #{inspect(__MODULE__)}.preferred_units! meter, locale: "en-US", usage: :road
            [:foot]
            iex> #{inspect(__MODULE__)}.preferred_units! meter, locale: "en-AU", usage: :road
            [:meter]

        """
        def preferred_units!(unit, options \\ []) do
          Cldr.Unit.Preference.preferred_units!(unit, unquote(backend), options)
        end

        @grammatical_features Cldr.Config.grammatical_features()
        @grammatical_gender Cldr.Config.grammatical_gender()
        @default_gender :masculine

        # Generate the functions that encapsulate the unit data from CDLR
        @doc false
        def units_for(locale \\ unquote(backend).get_locale(), style \\ Cldr.Unit.default_style())

        for locale_name <- Cldr.Config.known_locale_names(config) do
          locale_data =
            locale_name
            |> Cldr.Locale.Loader.get_locale(config)
            |> Map.get(:units)

            units_for_style = fn additional_units, style ->
              Map.get(locale_data, style)
              |> Enum.map(&elem(&1, 1))
              |> Cldr.Map.merge_map_list()
              |> Map.merge(additional_units)
              |> Map.new()
            end

          for style <- @styles do
            additional_units = additional_units.units_for(locale_name, style)
            units = units_for_style.(additional_units, style)

            def units_for(unquote(locale_name), unquote(style)) do
              unquote(Macro.escape(units))
            end
          end

          language_tag = Cldr.Config.language_tag(locale_name)
          language = Map.fetch!(language_tag, :language)

          grammatical_features = Map.get(@grammatical_features, language, %{})
          grammatical_gender = Map.get(@grammatical_gender, language, [@default_gender])
          default_gender = Enum.find(grammatical_gender, &(&1 == :neuter)) || @default_gender

          def grammatical_features(unquote(locale_name)) do
            unquote(Macro.escape(grammatical_features))
          end

          def grammatical_gender(unquote(locale_name)) do
            {:ok, unquote(Macro.escape(grammatical_gender))}
          end

          def default_gender(unquote(locale_name)) do
            {:ok, unquote(default_gender)}
          end

          unit_strings =
            for style <- @styles do
              additional_units =
                additional_units.units_for(locale_name, style)

              units =
                units_for_style.(additional_units, style)
                |> Cldr.Map.prune(fn
                   {k, _v} when k in [:per_unit_pattern, :per, :times] ->
                     true
                   {k, _v} ->
                     if String.starts_with?(Atom.to_string(k), "10"), do: true, else: false
                   _other -> false
                end)
                |> Enum.map(fn {k, v} -> {k, Cldr.Map.extract_strings(v)} end)
                |> Map.new()
            end
            |> Cldr.Map.merge_map_list(&Cldr.Map.combine_list_resolver/3)
            |> Enum.map(fn {k, v} -> {k, Enum.map(v, &String.trim/1)} end)
            |> Enum.map(fn {k, v} -> {k, Enum.map(v, &String.downcase/1)} end)
            |> Enum.map(fn {k, v} -> {k, Enum.uniq(v)} end)
            |> Map.new
            |> Cldr.Map.invert(duplicates: :shortest)

            def unit_strings_for(unquote(locale_name)) do
              {:ok, unquote(Macro.escape(unit_strings))}
            end
        end

        def unit_strings_for(locale) when is_binary(locale) do
          {:error, Cldr.Locale.locale_error(locale)}
        end

        def unit_strings_for(%LanguageTag{cldr_locale_name: cldr_locale_name}) do
          unit_strings_for(cldr_locale_name)
        end

        def units_for(%LanguageTag{cldr_locale_name: cldr_locale_name}, style) do
          units_for(cldr_locale_name, style)
        end

        def grammatical_features(%LanguageTag{language: language}) do
          grammatical_features(language)
        end

        def grammatical_features(language) do
          {:error, Cldr.Locale.locale_error(language)}
        end

        def grammatical_gender(%LanguageTag{language: language}) do
          grammatical_gender(language)
        end

        def grammatical_gender(language) do
          {:error, Cldr.Locale.locale_error(language)}
        end

        def default_gender(%LanguageTag{language: language}) do
          default_gender(language)
        end

        def default_gender(language) do
          {:error, Cldr.Locale.locale_error(language)}
        end
      end
    end
  end
end