From c2aa8ab9754b504dd01c53bfca9bb4c8cf2df131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0tefan=20=C4=BDupt=C3=A1k?= Date: Mon, 4 Apr 2022 17:02:42 +0200 Subject: [PATCH] Display minus sign before the currency symbol (fixes #154) (#173) --- lib/money.ex | 84 +++++++++++++++++------------------- lib/money/display_options.ex | 59 +++++++++++++++++++++++++ lib/money/parse_options.ex | 22 ++++++++++ test/money_test.exs | 15 +++++-- 4 files changed, 132 insertions(+), 48 deletions(-) create mode 100644 lib/money/display_options.ex create mode 100644 lib/money/parse_options.ex diff --git a/lib/money.ex b/lib/money.ex index 172b218..0e5fee7 100644 --- a/lib/money.ex +++ b/lib/money.ex @@ -20,13 +20,14 @@ defmodule Money do 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 delimeter 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 delimeter - strip_insignificant_zeros: false # don’t display the insignificant zeros or the delimeter + 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 """ @@ -38,6 +39,8 @@ defmodule Money do defstruct amount: 0, currency: :USD alias Money.Currency + alias Money.DisplayOptions + alias Money.ParseOptions @spec new(integer) :: t @doc ~S""" @@ -132,7 +135,7 @@ defmodule Money do end def parse(str, currency, opts) when is_binary(str) do - {_separator, delimiter} = get_parse_options(opts) + %ParseOptions{separator: _separator, delimiter: delimiter} = ParseOptions.get(opts) value = str @@ -545,8 +548,8 @@ defmodule Money do * `: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 delimeter - * `:strip_insignificant_zeros` - default `false`, strip zeros after the delimeter + * `: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 ## Examples @@ -583,22 +586,36 @@ defmodule Money do """ def to_string(%Money{} = money, opts \\ []) do - {separator, delimeter, symbol, symbol_on_right, symbol_space, fractional_unit, strip_insignificant_zeros, code} = - get_display_options(money, opts) - - number = format_number(money, separator, delimeter, fractional_unit, strip_insignificant_zeros, money) + %DisplayOptions{ + symbol: symbol, + symbol_on_right: symbol_on_right, + symbol_space: symbol_space, + code: code + } = 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 = - if symbol_on_right do - [sign, number, space, symbol, code] - else - [symbol, space, sign, number, code] + cond do + symbol_on_right -> + [sign, number, space, symbol, code] + + negative?(money) and symbol == " " -> + [sign, number, code] + + negative?(money) -> + [sign, symbol, number, code] + + true -> + [symbol, space, sign, number, code] end - parts |> Enum.join() |> String.trim_leading() + parts + |> Enum.join() + |> String.trim() end if Code.ensure_loaded?(Decimal) do @@ -667,7 +684,12 @@ defmodule Money do end end - defp format_number(%Money{amount: amount}, separator, delimeter, fractional_unit, strip_insignificant_zeros, money) do + defp format_number(%Money{amount: amount} = money, %DisplayOptions{ + separator: separator, + delimiter: delimiter, + fractional_unit: fractional_unit, + strip_insignificant_zeros: strip_insignificant_zeros + }) do exponent = Currency.exponent(money) amount_abs = if amount < 0, do: -amount, else: amount amount_str = Integer.to_string(amount_abs) @@ -684,7 +706,7 @@ defmodule Money do sub_unit = prepare_sub_unit(sub_unit, %{strip_insignificant_zeros: strip_insignificant_zeros}) if fractional_unit && sub_unit != "" do - [super_unit, sub_unit] |> Enum.join(delimeter) + [super_unit, sub_unit] |> Enum.join(delimiter) else super_unit end @@ -695,34 +717,6 @@ defmodule Money 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 get_display_options(m, opts) do - {separator, delimiter} = get_parse_options(opts) - - default_symbol = Application.get_env(:money, :symbol, true) - default_symbol_on_right = Application.get_env(:money, :symbol_on_right, false) - default_symbol_space = Application.get_env(:money, :symbol_space, false) - default_fractional_unit = Application.get_env(:money, :fractional_unit, true) - default_strip_insignificant_zeros = Application.get_env(:money, :strip_insignificant_zeros, false) - default_code = Application.get_env(:money, :code, false) - - symbol = if Keyword.get(opts, :symbol, default_symbol), do: Currency.symbol(m), else: "" - symbol_on_right = Keyword.get(opts, :symbol_on_right, default_symbol_on_right) - symbol_space = Keyword.get(opts, :symbol_space, default_symbol_space) - fractional_unit = Keyword.get(opts, :fractional_unit, default_fractional_unit) - strip_insignificant_zeros = Keyword.get(opts, :strip_insignificant_zeros, default_strip_insignificant_zeros) - code = Keyword.get(opts, :code, default_code) - - {separator, delimiter, symbol, symbol_on_right, symbol_space, fractional_unit, strip_insignificant_zeros, code} - end - - defp get_parse_options(opts) do - default_separator = Application.get_env(:money, :separator, ",") - separator = Keyword.get(opts, :separator, default_separator) - default_delimiter = Application.get_env(:money, :delimiter) || Application.get_env(:money, :delimeter, ".") - delimiter = Keyword.get(opts, :delimiter) || Keyword.get(opts, :delimeter, default_delimiter) - {separator, delimiter} - 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 diff --git a/lib/money/display_options.ex b/lib/money/display_options.ex new file mode 100644 index 0000000..c85cda2 --- /dev/null +++ b/lib/money/display_options.ex @@ -0,0 +1,59 @@ +defmodule Money.DisplayOptions do + @moduledoc false + alias Money.Currency + alias Money.ParseOptions + + @type t :: %__MODULE__{ + separator: String.t(), + delimiter: String.t(), + symbol: String.t(), + symbol_on_right: boolean(), + symbol_space: boolean(), + fractional_unit: boolean(), + strip_insignificant_zeros: boolean(), + code: boolean() + } + + @all_fields [ + :separator, + :delimiter, + :symbol, + :symbol_on_right, + :symbol_space, + :fractional_unit, + :strip_insignificant_zeros, + :code + ] + @enforce_keys @all_fields + defstruct @all_fields + + @spec get(Money.t(), Keyword.t()) :: t() + def get(%Money{} = money, opts) do + %{separator: separator, delimiter: delimiter} = ParseOptions.get(opts) + + default_symbol = Application.get_env(:money, :symbol, true) + default_symbol_on_right = Application.get_env(:money, :symbol_on_right, false) + default_symbol_space = Application.get_env(:money, :symbol_space, false) + default_fractional_unit = Application.get_env(:money, :fractional_unit, true) + default_strip_insignificant_zeros = Application.get_env(:money, :strip_insignificant_zeros, false) + default_code = Application.get_env(:money, :code, false) + + symbol = if Keyword.get(opts, :symbol, default_symbol), do: Currency.symbol(money), else: "" + symbol_on_right = Keyword.get(opts, :symbol_on_right, default_symbol_on_right) + symbol_space = Keyword.get(opts, :symbol_space, default_symbol_space) + fractional_unit = Keyword.get(opts, :fractional_unit, default_fractional_unit) + strip_insignificant_zeros = Keyword.get(opts, :strip_insignificant_zeros, default_strip_insignificant_zeros) + code = Keyword.get(opts, :code, default_code) + + %__MODULE__{ + separator: separator, + delimiter: delimiter, + symbol: symbol, + symbol_on_right: symbol_on_right, + symbol_space: symbol_space, + fractional_unit: fractional_unit, + strip_insignificant_zeros: strip_insignificant_zeros, + code: code + } + end +end diff --git a/lib/money/parse_options.ex b/lib/money/parse_options.ex new file mode 100644 index 0000000..8292d04 --- /dev/null +++ b/lib/money/parse_options.ex @@ -0,0 +1,22 @@ +defmodule Money.ParseOptions do + @moduledoc false + + @type t :: %__MODULE__{ + separator: String.t(), + delimiter: String.t() + } + + @enforce_keys [:separator, :delimiter] + defstruct [:separator, :delimiter] + + @spec get(Keyword.t()) :: t() + def get(opts) do + default_separator = Application.get_env(:money, :separator, ",") + separator = Keyword.get(opts, :separator, default_separator) + + default_delimiter = Application.get_env(:money, :delimiter) || Application.get_env(:money, :delimeter, ".") + delimiter = Keyword.get(opts, :delimiter) || Keyword.get(opts, :delimeter, default_delimiter) + + %__MODULE__{separator: separator, delimiter: delimiter} + end +end diff --git a/test/money_test.exs b/test/money_test.exs index b1624ce..0308126 100644 --- a/test/money_test.exs +++ b/test/money_test.exs @@ -251,10 +251,19 @@ defmodule MoneyTest do end test "to_string with negative values" do - assert Money.to_string(usd(-500)) == "$-5.00" - assert Money.to_string(eur(-1234)) == "€-12.34" + assert Money.to_string(usd(-500)) == "-$5.00" + assert Money.to_string(eur(-1234)) == "-€12.34" assert Money.to_string(xau(-20305)) == "-203.05" - assert Money.to_string(zar(-1_234_567_890)) == "R-12,345,678.90" + assert Money.to_string(zar(-1_234_567_890)) == "-R12,345,678.90" + end + + test "to_string with negative values symbol_on_right true" do + opts = [symbol_on_right: true] + + assert Money.to_string(usd(-500), opts) == "-5.00$" + assert Money.to_string(eur(-1234), opts) == "-12.34€" + assert Money.to_string(xau(-20305), opts) == "-203.05" + assert Money.to_string(zar(-1_234_567_890), opts) == "-12,345,678.90R" end test "to_string with fractional_unit false" do