lib/number/currency.ex

defmodule Number.Currency do
  @moduledoc """
  Provides functions for converting numbers into formatted currency strings.
  """

  import Number.Delimit, only: [number_to_delimited: 2]

  @doc """
  Converts a number to a formatted currency string.

  ## Parameters

  * `number` - A float or integer to convert.

  * `options` - A keyword list of options. See the documentation of all
    available options below for more information.

  ## Options

  * `:unit` - The currency symbol to use. Default: "$"

  * `:precision` - The number of decimal places to include. Default: 2

  * `:delimiter` - The character to use to delimit the number by thousands.
    Default: ","

  * `:separator` - The character to use to separate the number from the decimal
    places. Default: "."

  * `:format` - The format of the number. This can be used to put the currency
    symbol in a different place.  See the examples for usage. There are two
    supported format string placeholders:
      * `%u` - Represents the currency symbol, or unit.
      * `%n` - Represents the number.

  * `:negative_format` - The format of the number when it is negative. Uses the
    same formatting placeholders as the `:format` option.

  Default config for these options can be specified in the `Number`
  application configuration.

      config :number,
        currency: [
          unit: "£",
          precision: 2,
          delimiter: ",",
          separator: ".",
          format: "%u%n",           # "£30.00"
          negative_format: "(%u%n)" # "(£30.00)"
        ]

  ## Examples

      iex> Number.Currency.number_to_currency(nil)
      nil

      iex> Number.Currency.number_to_currency(1000)
      "$1,000.00"

      iex> Number.Currency.number_to_currency(1000, unit: "£")
      "£1,000.00"

      iex> Number.Currency.number_to_currency(-1000)
      "-$1,000.00"

      iex> Number.Currency.number_to_currency(-234234.23)
      "-$234,234.23"

      iex> Number.Currency.number_to_currency(1234567890.50)
      "$1,234,567,890.50"

      iex> Number.Currency.number_to_currency(1234567890.506)
      "$1,234,567,890.51"

      iex> Number.Currency.number_to_currency(1234567890.506, precision: 3)
      "$1,234,567,890.506"

      iex> Number.Currency.number_to_currency(-1234567890.50, negative_format: "(%u%n)")
      "($1,234,567,890.50)"

      iex> Number.Currency.number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "")
      "R$1234567890,50"

      iex> Number.Currency.number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
      "1234567890,50 R$"

      iex> Number.Currency.number_to_currency(Decimal.from_float(50.0))
      "$50.00"

      iex> Number.Currency.number_to_currency(Decimal.from_float(-100.01))
      "-$100.01"

      iex> Number.Currency.number_to_currency(Decimal.from_float(-100.01), unit: "$", separator: ",", delimiter: ".", negative_format: "- %u %n")
      "- $ 100,01"

  """
  @spec number_to_currency(Number.t(), Keyword.t()) :: String.t()
  def number_to_currency(number, options \\ [])
  def number_to_currency(nil, _options), do: nil

  def number_to_currency(number, options) do
    options = Keyword.merge(config(), options)
    {number, format} = get_format(number, options)
    number = number_to_delimited(number, options)

    format
    |> String.replace(~r/%u/, options[:unit])
    |> String.replace(~r/%n/, number)
  end

  defp get_format(number, options) do
    number = if is_float(number), do: Decimal.from_float(number), else: Decimal.new(number)

    case Number.Decimal.compare(number, Decimal.new(0)) do
      :lt -> {Decimal.abs(number), options[:negative_format] || "-#{options[:format]}"}
      _ -> {number, options[:format]}
    end
  end

  defp config do
    defaults = [
      delimiter: ",",
      separator: ".",
      precision: 2,
      unit: "$",
      format: "%u%n"
    ]

    Keyword.merge(defaults, Application.get_env(:number, :currency, []))
  end
end