lib/cldr/number/formatter/currency_formatter.ex

defmodule Cldr.Number.Formatter.Currency do
  @moduledoc """
  Number formatter for the `:currency` `:long` format.

  This formatter implements formatting a currency in a long form. This
  is not the same as decimal formatting with a currency placeholder.

  To explain the difference, look at the following examples:

      iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :currency, currency: "USD"
      {:ok, "$123.00"}

      iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :long, currency: "USD"
      {:ok, "123 US dollars"}

  In the first example the format is defined by a decimal mask. In this example
  the format mask comes from:

      iex> {:ok, formats} = Cldr.Number.Format.all_formats_for("en", TestBackend.Cldr)
      ...> formats.latn.currency
      "¤#,##0.00"

  In the second example we are using a format that combines the number with
  a language translation of the currency name.  In this example the format
  comes from:

      iex> {:ok, formats} = Cldr.Number.Format.all_formats_for("en", TestBackend.Cldr)
      ...> formats.latn.currency_long
      %{one: [0, " ", 1], other: [0, " ", 1]}

  Where "{0}" is replaced with the number formatted using the `:standard`
  decimal format and "{1} is replaced with locale-specific name of the
  currency adjusted for the locales plural rules."

  **This module is not part of the public API and is subject
  to change at any time.**

  """

  alias Cldr.Number.{Format, System}
  alias Cldr.{Substitution, Currency}
  alias Cldr.Number.Format.Options
  alias Cldr.Number.Formatter

  import DigitalToken, only: [is_digital_token: 1]

  @doc false
  def to_string(number, _format, _backend, _options) when is_binary(number) do
    {:error,
     {
       ArgumentError,
       "Not a number: #{inspect(number)}. Currency long formats only support number or Decimal arguments"
     }}
  end

  # The format :currency_long_with_symbol is a composition of :currency_long
  # and the default :currency format. It is a derived format, not one
  # defined by CLDR.

  def to_string(number, :currency_long_with_symbol, backend, options) do
    decimal_options = decimal_options(options, backend, number)
    decimal_format = decimal_options.format

    number
    |> Cldr.Number.to_string!(backend, long_options(options))
    |> Formatter.Decimal.to_string(decimal_format, backend, decimal_options)
  end

  def to_string(number, :currency_long, backend, options) do
    locale = options.locale
    number_system = System.system_name_from!(options.number_system, locale, backend)
    cardinal = Module.concat(backend, Number.Cardinal)

    if !(formats = Format.formats_for!(locale, number_system, backend).currency_long) do
      raise ArgumentError,
        message:
          "No :currency_long format known for " <>
            "locale #{inspect(locale)} and number system #{inspect(number_system)}."
    end

    options =
      options
      |> Map.put(:format, :standard)
      |> set_fractional_digits(options.currency, options.fractional_digits)
      |> Options.resolve_standard_format(backend)

    currency_string = currency_string(number, options.currency, cardinal, locale, backend)
    number_string = Cldr.Number.to_string!(number, backend, options)
    format = cardinal.pluralize(number, locale, formats)

    Substitution.substitute([number_string, currency_string], format)
    |> :erlang.iolist_to_binary()
  end

  defp currency_string(number, %Cldr.Currency{} = currency, cardinal, locale, _backend) do
    cardinal.pluralize(number, locale, currency.count)
  end

  defp currency_string(_number, currency, _cardinal, _locale, _backend)
       when is_digital_token(currency) do
    {:ok, currency_string} = DigitalToken.long_name(currency)
    currency_string
  end

  defp set_fractional_digits(options, %Cldr.Currency{}, nil) do
    Map.put(options, :fractional_digits, 0)
  end

  defp set_fractional_digits(options, _currency, _digits) do
    options
  end

  defp long_options(options) do
    options
    |> Map.put(:format, :decimal_long)
    |> Map.put(:currency, nil)
  end

  defp decimal_options(options, backend, number) do
    currency_format = Currency.currency_format_from_locale(options.locale)
    options = Map.put(options, :format, currency_format)

    options
    |> Options.resolve_standard_format(backend)
    |> Options.maybe_expand_currency_symbol(number)
  end
end