lib/better_number/currency.ex

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

  alias BetterNumber.Conversion
  import BetterNumber.Delimit, only: [number_to_delimited: 2]

  @doc ~S[
  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` - Function which converts number and unit to the final string.
    Default: `fn unit, number -> unit <> number end`

  * `:negative_format` - The format of the number when it is negative. Uses the
    same formatting placeholders as the `:format` option. Default value is derived
    from `:format` option just with "-" prefix.

  * `:trim_zero_fraction` - Whether to trim the zeroes in fraction part of number.
    Default: "false"

  ## Examples

      iex> number_to_currency(nil)
      nil

      iex> number_to_currency(1000)
      "$1,000.00"

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

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

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

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

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

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

      iex> number_to_currency(-1234567890.50, negative_format: &"(#{&1}#{&2})")
      "($1,234,567,890.50)"

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

      iex> number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: &"#{&2} #{&1}")
      "1234567890,50 R$"

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

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

      iex> number_to_currency(Decimal.from_float(-100.01), unit: "$", separator: ",", delimiter: ".", negative_format: &"- #{&1} #{&2}")
      "- $ 100,01"
  ]
  @spec number_to_currency(BetterNumber.t(), Keyword.t() | map()) :: String.t()
  def number_to_currency(number, options \\ [])
  def number_to_currency(nil, _options), do: nil

  def number_to_currency(number, options) do
    %{unit: unit} = options = config(options)
    {number, format} = get_format(number, options)
    number = number_to_delimited(number, options)

    format.(unit, number)
  end

  @zero_decimal Decimal.new(0)

  defp get_format(number, %{} = options) do
    number = Conversion.to_decimal(number)

    number
    |> Decimal.compare(@zero_decimal)
    |> case do
      :lt -> {Decimal.abs(number), options.negative_format}
      _ -> {number, options.format}
    end
  end

  defp config(
         %{
           unit: _,
           precision: _,
           delimiter: _,
           separator: _,
           format: _,
           trim_zero_fraction: _,
           negative_format: _
         } = options
       ) do
    options
  end

  defp config(options) do
    %{
      unit: "$",
      precision: 2,
      delimiter: ",",
      separator: ".",
      format: fn unit, number -> unit <> number end,
      trim_zero_fraction: false
    }
    |> Map.merge(Map.new(options))
    |> case do
      %{negative_format: _} = options ->
        options

      %{format: format} = options ->
        Map.put(options, :negative_format, fn unit, number -> "-" <> format.(unit, number) end)
    end
  end
end