Skip to content

Commit

Permalink
Display minus sign before the currency symbol (fixes #154) (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanluptak authored Apr 4, 2022
1 parent 04f2166 commit c2aa8ab
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 48 deletions.
84 changes: 39 additions & 45 deletions lib/money.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand All @@ -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"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions lib/money/display_options.ex
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions lib/money/parse_options.ex
Original file line number Diff line number Diff line change
@@ -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
15 changes: 12 additions & 3 deletions test/money_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c2aa8ab

Please sign in to comment.