defmodule Money do import Kernel, except: [abs: 1, round: 1] @moduledoc """ Defines a `Money` struct along with convenience methods for working with currencies. ## Examples iex> money = Money.new(500, :USD) %Money{amount: 500, currency: :USD} iex> money = Money.add(money, 550) %Money{amount: 1050, currency: :USD} iex> Money.to_string(money) "$10.50" ## Configuration You can set defaults in your Mix configuration to make working with `Money` a little easier. config :money, default_currency: :EUR, # this allows you to do Money.new(100) separator: ".", # change the default thousands separator for Money.to_string delimiter: ",", # change the default decimal delimiter for Money.to_string symbol: false, # don’t display the currency symbol in Money.to_string symbol_on_right: false, # position the symbol symbol_space: false, # add a space between symbol and number fractional_unit: true, # display units after the delimiter strip_insignificant_zeros: false, # don’t display the insignificant zeros or the delimiter code: false, # add the currency code after the number minus_sign_first: true, # display the minus sign before the currency symbol for Money.to_string strip_insignificant_fractional_unit: false # don't display the delimiter or fractional units if the fractional units are only insignificant zeros """ @type t :: %__MODULE__{ amount: integer, currency: atom } defstruct amount: 0, currency: :USD alias Money.Currency alias Money.DisplayOptions alias Money.ParseOptions @spec new(integer) :: t @doc ~S""" Create a new `Money` struct using a default currency. The default currency can be set in the system Mix config. ## Config config :money, default_currency: :USD ## Examples Money.new(123) %Money{amount: 123, currency: :USD} """ def new(amount) do currency = Application.get_env(:money, :default_currency) if currency do new(amount, currency) else raise ArgumentError, "to use Money.new/1 you must set a default currency in your application config." end end @spec new(integer, atom | String.t()) :: t @doc """ Create a new `Money` struct from currency sub-units (cents) ## Examples iex> Money.new(1_000_00, :USD) %Money{amount: 1_000_00, currency: :USD} """ def new(int, currency) when is_integer(int), do: %Money{amount: int, currency: Currency.to_atom(currency)} @spec parse(String.t() | number | Decimal.t(), atom | String.t(), Keyword.t()) :: {:ok, t} | :error @doc ~S""" Parse a value into a `Money` type. The following options are available: * `:separator` - default `","`, sets the separator for groups of thousands. "1,000" * `:delimiter` - default `"."`, sets the decimal delimiter. "1.23" ## Examples iex> Money.parse("$1,234.56", :USD) {:ok, %Money{amount: 123456, currency: :USD}} iex> Money.parse("1.234,56", :EUR, separator: ".", delimiter: ",") {:ok, %Money{amount: 123456, currency: :EUR}} iex> Money.parse("1.234,56", :WRONG) :error iex> Money.parse(1_234.56, :USD) {:ok, %Money{amount: 123456, currency: :USD}} iex> Money.parse(1_234, :USD) {:ok, %Money{amount: 123400, currency: :USD}} iex> Money.parse(-1_234.56, :USD) {:ok, %Money{amount: -123456, currency: :USD}} iex> Money.parse(Decimal.from_float(1_234.56), :USD) {:ok, %Money{amount: 123456, currency: :USD}} """ def parse(value, currency \\ nil, opts \\ []) def parse(value, nil, opts) do currency = Application.get_env(:money, :default_currency) if currency do parse(value, currency, opts) else raise ArgumentError, "to use Money.new/1 you must set a default currency in your application config." end end if Code.ensure_loaded?(Decimal) do @parser Decimal else @parser Float end def parse(str, currency, opts) when is_binary(str) do %ParseOptions{separator: _separator, delimiter: delimiter} = ParseOptions.get(opts) value = str |> prepare_parse_string(delimiter) |> add_missing_leading_digit case @parser.parse(value) do {float, _} -> parse(float, currency, []) :error -> :error end rescue _ -> :error end def parse(number, currency, _opts) when is_number(number) do {:ok, new(Kernel.round(number * Currency.sub_units_count!(currency)), currency)} end if Code.ensure_loaded?(Decimal) do def parse(%Decimal{} = decimal, currency, _opts) do {:ok, decimal |> Decimal.mult(Currency.sub_units_count!(currency)) |> Decimal.round(0, Decimal.Context.get().rounding) |> Decimal.to_integer() |> new(currency)} end end defp prepare_parse_string(characters, delimiter, acc \\ []) defp prepare_parse_string([], _delimiter, acc), do: acc |> Enum.reverse() |> Enum.join() defp prepare_parse_string(["-" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["-" | acc]) defp prepare_parse_string(["0" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["0" | acc]) defp prepare_parse_string(["1" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["1" | acc]) defp prepare_parse_string(["2" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["2" | acc]) defp prepare_parse_string(["3" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["3" | acc]) defp prepare_parse_string(["4" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["4" | acc]) defp prepare_parse_string(["5" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["5" | acc]) defp prepare_parse_string(["6" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["6" | acc]) defp prepare_parse_string(["7" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["7" | acc]) defp prepare_parse_string(["8" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["8" | acc]) defp prepare_parse_string(["9" | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["9" | acc]) defp prepare_parse_string([delimiter | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, ["." | acc]) defp prepare_parse_string([_head | tail], delimiter, acc), do: prepare_parse_string(tail, delimiter, acc) defp prepare_parse_string(string, delimiter, _acc), do: prepare_parse_string(String.codepoints(string), delimiter) defp add_missing_leading_digit(<<"-.">> <> tail), do: "-0." <> tail defp add_missing_leading_digit(<<".">> <> tail), do: "0." <> tail defp add_missing_leading_digit(str), do: str @spec parse!(String.t() | number | Decimal.t(), atom | String.t(), Keyword.t()) :: t @doc ~S""" Parse a value into a `Money` type. Similar to `parse/3` but returns a `%Money{}` or raises an error if parsing fails. ## Examples iex> Money.parse!("1,234.56", :USD) %Money{amount: 123456, currency: :USD} iex> Money.parse!("wrong", :USD) ** (ArgumentError) unable to parse "wrong" with currency :USD """ def parse!(value, currency \\ nil, opts \\ []) do case parse(value, currency, opts) do {:ok, money} -> money :error -> raise ArgumentError, "unable to parse #{inspect(value)} with currency #{inspect(currency)}" end end @spec compare(t, t) :: -1 | 0 | 1 @doc ~S""" Compares two `Money` structs with each other. They must each be of the same currency and then their amounts are compared. If the first amount is larger than the second `1` is returned, if less than `-1` is returned, if both amounts are equal `0` is returned. See `cmp/2` for a similar function that returns `:lt`, `:eq` or `:gt` instead. ## Examples iex> Money.compare(Money.new(100, :USD), Money.new(100, :USD)) 0 iex> Money.compare(Money.new(100, :USD), Money.new(101, :USD)) -1 iex> Money.compare(Money.new(101, :USD), Money.new(100, :USD)) 1 """ def compare(%Money{currency: cur} = a, %Money{currency: cur} = b) do case a.amount - b.amount do x when x > 0 -> 1 x when x < 0 -> -1 x when x == 0 -> 0 end end def compare(a, b), do: fail_currencies_must_be_equal(a, b) @doc """ Compares two `Money` structs with each other. They must each be of the same currency and then their amounts are compared. If the first amount is larger than the second `:gt` is returned, if less than `:lt` is returned, if both amounts are equal `:eq` is returned. See `compare/2` for a similar function that returns `-1`, `0` or `1` instead. ## Examples iex> Money.cmp(Money.new(100, :USD), Money.new(100, :USD)) :eq iex> Money.cmp(Money.new(100, :USD), Money.new(101, :USD)) :lt iex> Money.cmp(Money.new(101, :USD), Money.new(100, :USD)) :gt """ @spec cmp(t, t) :: :lt | :eq | :gt def cmp(a, b) do case compare(a, b) do x when x == -1 -> :lt x when x == 0 -> :eq x when x == 1 -> :gt end end @spec zero?(t) :: boolean @doc ~S""" Returns true if the amount of a `Money` struct is zero ## Examples iex> Money.zero?(Money.new(0, :USD)) true iex> Money.zero?(Money.new(1, :USD)) false """ def zero?(%Money{amount: amount}) do amount == 0 end @spec positive?(t) :: boolean @doc ~S""" Returns true if the amount of a `Money` is greater than zero ## Examples iex> Money.positive?(Money.new(0, :USD)) false iex> Money.positive?(Money.new(1, :USD)) true iex> Money.positive?(Money.new(-1, :USD)) false """ def positive?(%Money{amount: amount}) do amount > 0 end @spec negative?(t) :: boolean @doc ~S""" Returns true if the amount of a `Money` is less than zero ## Examples iex> Money.negative?(Money.new(0, :USD)) false iex> Money.negative?(Money.new(1, :USD)) false iex> Money.negative?(Money.new(-1, :USD)) true """ def negative?(%Money{amount: amount}) do amount < 0 end @spec equals?(t, t) :: boolean @doc ~S""" Returns true if two `Money` of the same currency have the same amount ## Examples iex> Money.equals?(Money.new(100, :USD), Money.new(100, :USD)) true iex> Money.equals?(Money.new(101, :USD), Money.new(100, :USD)) false iex> Money.equals?(Money.new(100, :USD), Money.new(100, :CAD)) false """ def equals?(%Money{amount: amount, currency: cur}, %Money{amount: amount, currency: cur}), do: true def equals?(%Money{}, %Money{}), do: false @spec neg(t) :: t @doc ~S""" Returns a `Money` with the amount negated. ## Examples iex> Money.new(100, :USD) |> Money.neg %Money{amount: -100, currency: :USD} iex> Money.new(-100, :USD) |> Money.neg %Money{amount: 100, currency: :USD} """ def neg(%Money{amount: amount, currency: cur}), do: %Money{amount: -amount, currency: cur} @spec abs(t) :: t @doc ~S""" Returns a `Money` with the arithmetical absolute of the amount. ## Examples iex> Money.new(-100, :USD) |> Money.abs %Money{amount: 100, currency: :USD} iex> Money.new(100, :USD) |> Money.abs %Money{amount: 100, currency: :USD} """ def abs(%Money{amount: amount, currency: cur}), do: %Money{amount: Kernel.abs(amount), currency: cur} @spec add(t, t | integer | float) :: t @doc ~S""" Adds two `Money` together or an integer (cents) amount to a `Money` ## Examples iex> Money.add(Money.new(100, :USD), Money.new(50, :USD)) %Money{amount: 150, currency: :USD} iex> Money.add(Money.new(100, :USD), 50) %Money{amount: 150, currency: :USD} iex> Money.add(Money.new(100, :USD), 5.55) %Money{amount: 655, currency: :USD} """ def add(%Money{amount: a, currency: cur}, %Money{amount: b, currency: cur}), do: Money.new(a + b, cur) def add(%Money{amount: amount, currency: cur}, addend) when is_integer(addend), do: Money.new(amount + addend, cur) def add(%Money{} = m, addend) when is_float(addend), do: add(m, Kernel.round(addend * 100)) def add(a, b), do: fail_currencies_must_be_equal(a, b) @spec subtract(t, t | integer | float) :: t @doc ~S""" Subtracts one `Money` from another or an integer (cents) from a `Money` ## Examples iex> Money.subtract(Money.new(150, :USD), Money.new(50, :USD)) %Money{amount: 100, currency: :USD} iex> Money.subtract(Money.new(150, :USD), 50) %Money{amount: 100, currency: :USD} iex> Money.subtract(Money.new(150, :USD), 1.25) %Money{amount: 25, currency: :USD} """ def subtract(%Money{amount: a, currency: cur}, %Money{amount: b, currency: cur}), do: Money.new(a - b, cur) def subtract(%Money{amount: a, currency: cur}, subtractend) when is_integer(subtractend), do: Money.new(a - subtractend, cur) def subtract(%Money{} = m, subtractend) when is_float(subtractend), do: subtract(m, Kernel.round(subtractend * 100)) def subtract(a, b), do: fail_currencies_must_be_equal(a, b) @spec multiply(t, integer | float | Decimal.t()) :: t @doc ~S""" Multiplies a `Money` by an amount ## Examples iex> Money.multiply(Money.new(100, :USD), 10) %Money{amount: 1000, currency: :USD} iex> Money.multiply(Money.new(100, :USD), 1.5) %Money{amount: 150, currency: :USD} """ def multiply(%Money{amount: amount, currency: cur}, multiplier) when is_integer(multiplier), do: Money.new(amount * multiplier, cur) def multiply(%Money{amount: amount, currency: cur}, multiplier) when is_float(multiplier), do: Money.new(Kernel.round(amount * multiplier), cur) if Code.ensure_loaded?(Decimal) do def multiply(%Money{amount: amount, currency: cur}, %Decimal{} = multiplier), do: amount |> Decimal.mult(multiplier) |> Decimal.round(0, Decimal.Context.get().rounding) |> Decimal.to_integer() |> Money.new(cur) end @spec divide(t, integer) :: [t] @doc ~S""" Divides up `Money` by an amount ## Examples iex> Money.divide(Money.new(100, :USD), 2) [%Money{amount: 50, currency: :USD}, %Money{amount: 50, currency: :USD}] iex> Money.divide(Money.new(101, :USD), 2) [%Money{amount: 51, currency: :USD}, %Money{amount: 50, currency: :USD}] """ def divide(%Money{amount: amount, currency: cur}, denominator) when is_integer(denominator) do value = div(amount, denominator) rem = rem(amount, denominator) do_divide(cur, value, rem, denominator, []) end defp do_divide(_currency, _value, _rem, 0, acc), do: acc |> Enum.reverse() defp do_divide(currency, value, 0, count, acc) do acc = [new(next_amount(value, 0, count), currency) | acc] count = decrement_abs(count) do_divide(currency, value, 0, count, acc) end defp do_divide(currency, value, rem, count, acc) do acc = [new(next_amount(value, rem, count), currency) | acc] rem = decrement_abs(rem) count = decrement_abs(count) do_divide(currency, value, rem, count, acc) end defp next_amount(0, -1, count) when count > 0, do: -1 defp next_amount(value, 0, _count), do: value defp next_amount(value, _rem, _count), do: increment_abs(value) defp increment_abs(n) when n >= 0, do: n + 1 defp increment_abs(n) when n < 0, do: n - 1 defp decrement_abs(n) when n >= 0, do: n - 1 defp decrement_abs(n) when n < 0, do: n + 1 @spec to_string(t, Keyword.t()) :: String.t() @doc ~S""" Converts a `Money` struct to a string representation The following options are available: * `:separator` - default `","`, sets the separator for groups of thousands. "1,000" * `:delimiter` - default `"."`, sets the decimal delimiter. "1.23" * `:symbol` - default `true`, sets whether to display the currency symbol or not. * `:symbol_on_right` - default `false`, display the currency symbol on the right of the number, eg: 123.45€ * `:symbol_space` - default `false`, add a space between currency symbol and number, eg: € 123,45 or 123.45 € * `:fractional_unit` - default `true`, show the remaining units after the delimiter * `:strip_insignificant_zeros` - default `false`, strip zeros after the delimiter * `:code` - default `false`, append the currency code after the number * `:minus_sign_first` - default `true`, display the minus sign before the currency symbol for negative values * `:strip_insignificant_fractional_unit` - default `false`, don't display the delimiter or fractional units if the fractional units are only insignificant zeros ## Examples iex> Money.to_string(Money.new(123456, :GBP)) "£1,234.56" iex> Money.to_string(Money.new(123456, :EUR), separator: ".", delimiter: ",") "€1.234,56" iex> Money.to_string(Money.new(123456, :EUR), symbol: false) "1,234.56" iex> Money.to_string(Money.new(123456, :EUR), symbol: false, separator: "") "1234.56" iex> Money.to_string(Money.new(123456, :EUR), fractional_unit: false) "€1,234" iex> Money.to_string(Money.new(123450, :EUR), strip_insignificant_zeros: true) "€1,234.5" iex> Money.to_string(Money.new(123450, :EUR), code: true) "€1,234.50 EUR" iex> Money.to_string(Money.new(-123450, :EUR)) "-€1,234.50" iex> Money.to_string(Money.new(-123450, :EUR), minus_sign_first: false) "€-1,234.50" iex> Money.to_string(Money.new(123400, :EUR), strip_insignificant_fractional_unit: true) "€1,234" iex> Money.to_string(Money.new(123450, :EUR), strip_insignificant_fractional_unit: true) "€1,234.50" It can also be interpolated (It implements the String.Chars protocol) To control the formatting, you can use the above options in your config, more information is in the introduction to `Money` ## Examples iex> "Total: #{Money.new(100_00, :USD)}" "Total: $100.00" """ def to_string(%Money{} = money, opts \\ []) do %DisplayOptions{ symbol: symbol, symbol_on_right: symbol_on_right, symbol_space: symbol_space, code: code, minus_sign_first: minus_sign_first } = opts = DisplayOptions.get(money, opts) number = format_number(money, opts) sign = if negative?(money), do: "-" space = if symbol_space, do: " " code = if code, do: " #{money.currency}" parts = cond do symbol_on_right -> [sign, number, space, symbol, code] negative?(money) and symbol == " " -> [sign, number, code] negative?(money) and minus_sign_first -> [sign, symbol, space, number, code] true -> [symbol, space, sign, number, code] end parts |> Enum.join() |> String.trim() end if Code.ensure_loaded?(Decimal) do @spec to_decimal(t) :: Decimal.t() @doc ~S""" Converts a `Money` struct to a `Decimal` representation ## Examples iex> Money.to_decimal(Money.new(123456, :GBP)) #Decimal<1234.56> iex> Money.to_decimal(Money.new(-123420, :EUR)) #Decimal<-1234.20> """ def to_decimal(%Money{} = money) do sign = if money.amount >= 0, do: 1, else: -1 coef = Money.abs(money).amount exp = -Money.Currency.exponent!(money) Decimal.new(sign, coef, exp) end @spec round(t, integer()) :: t @doc ~S""" Rounds a `Money` struct using a given number of places. `round` respects the rounding mode within the current Decimal context. By default `round` rounds to zero decimal places, using the currency's exponent. This results in rounding to whole values of the currency. Currencies without an exponent are not rounded unless a different value is passed for `places` other than the default. ## Examples iex> Money.round(Money.new(123456, :GBP)) %Money{amount: 123500, currency: :GBP} iex> Money.round(Money.new(-123420, :EUR)) %Money{amount: -123400, currency: :EUR} iex> Money.round(Money.new(-123420, :EUR), -3) %Money{amount: -100000, currency: :EUR} # Round to tenth of exponent iex> Money.round(Money.new(123425, :EUR), 1) %Money{amount: 123430, currency: :EUR} # Currencies round based on their exponent iex> Money.round(Money.new(820412, :JPY)) %Money{amount: 820412, currency: :JPY} iex> Money.round(Money.new(820412, :JPY), -3) %Money{amount: 820000, currency: :JPY} """ def round(%Money{} = money, places \\ 0) when is_integer(places) do {:ok, result} = money |> Money.to_decimal() |> Decimal.round(places, Decimal.Context.get().rounding) |> Money.parse(money.currency) result end end defp format_number(%Money{amount: amount} = money, %DisplayOptions{ separator: separator, delimiter: delimiter, fractional_unit: fractional_unit, strip_insignificant_zeros: strip_insignificant_zeros, strip_insignificant_fractional_unit: strip_insignificant_fractional_unit }) do exponent = Currency.exponent(money) amount_abs = if amount < 0, do: -amount, else: amount amount_str = Integer.to_string(amount_abs) [sub_unit, super_unit] = amount_str |> String.pad_leading(exponent + 1, "0") |> String.reverse() |> String.split_at(exponent) |> Tuple.to_list() |> Enum.map(&String.reverse/1) super_unit = super_unit |> reverse_group(3) |> Enum.join(separator) sub_unit = sub_unit |> prepare_sub_unit(%{strip_insignificant_zeros: strip_insignificant_zeros}) |> prepare_sub_unit(%{strip_insignificant_fractional_unit: strip_insignificant_fractional_unit}) if fractional_unit and sub_unit != "" do [super_unit, sub_unit] |> Enum.join(delimiter) else super_unit end end defp prepare_sub_unit([value], options), do: prepare_sub_unit(value, options) defp prepare_sub_unit([], _), do: "" defp prepare_sub_unit(value, %{strip_insignificant_zeros: false}), do: value defp prepare_sub_unit(value, %{strip_insignificant_zeros: true}), do: Regex.replace(~r/0+$/, value, "") defp prepare_sub_unit(value, %{strip_insignificant_fractional_unit: false}), do: value defp prepare_sub_unit(value, %{strip_insignificant_fractional_unit: true}) do if Regex.match?(~r/[1-9]+/, value), do: value, else: "" end defp fail_currencies_must_be_equal(a, b) do raise ArgumentError, message: "Currency of #{a.currency} must be the same as #{b.currency}" end defp reverse_group(str, count) when is_binary(str) do reverse_group(str, Kernel.abs(count), []) end defp reverse_group("", _count, list) do list end defp reverse_group(str, count, list) do {first, last} = String.split_at(str, -count) reverse_group(first, count, [last | list]) end defimpl String.Chars do def to_string(%Money{} = m) do Money.to_string(m) end end end