Repository: elixirmoney/money
Pull request #225 (2024-07-06 22:15:39)

feat: added custom_display_options configuration
I've worked on what I said in the issue https://github.com/elixirmoney/money/issues/224 and this is an initial draft. Let me know if you find it useful.


Patch:
diff --git a/README.md b/README.md
index 20de471..7cb663e 100644
--- a/README.md
+++ b/README.md
@@ -325,6 +325,18 @@ config :money,
   ]
 ```
 
+### Custom Currencies Display Options
+
+In some cases, we may need to display the currency in a different way than the default format. For example, we may want to display the currency symbol on the right side of the amount for some currencies. To achieve this, you can add the following configuration:
+
+```elixir
+config :money,
+  custom_display_options: [
+    EUR: %{symbol_on_right: true, symbol_space: true, separator: ".", delimiter: ",", symbol: true},
+    JPY: %{symbol_on_right: true, separator: ".", delimiter: ",", symbol: true}
+  ]
+```
+
 ## Troubleshooting
 
 ### Validating amount in Ecto changeset
diff --git a/lib/money/display_options.ex b/lib/money/display_options.ex
index 425c6c6..b476c8b 100644
--- a/lib/money/display_options.ex
+++ b/lib/money/display_options.ex
@@ -31,8 +31,29 @@ defmodule Money.DisplayOptions do
   @enforce_keys @all_fields
   defstruct @all_fields
 
+  defp custom_display_options do
+    Enum.into(Application.get_env(:money, :custom_display_options, []), %{})
+  end
+
   @spec get(Money.t(), Keyword.t()) :: t()
   def get(%Money{} = money, opts) do
+    custom_currency_display_options = get_currency_custom_display_options(money.currency)
+
+    opts_with_symbol =
+      case Keyword.get(opts, :symbol) do
+        nil -> opts
+        true -> Keyword.put(opts, :symbol, Currency.symbol(money.currency))
+        false -> Keyword.put(opts, :symbol, "")
+      end
+
+    money
+    |> get_defaults(opts)
+    |> Map.merge(custom_currency_display_options)
+    |> Map.merge(Enum.into(opts_with_symbol, %{}))
+  end
+
+  @spec get_defaults(Money.t(), Keyword.t()) :: t()
+  defp get_defaults(%Money{} = money, opts) do
     %{separator: separator, delimiter: delimiter} = ParseOptions.get(opts)
 
     default_symbol = Application.get_env(:money, :symbol, true)
@@ -47,27 +68,28 @@ defmodule Money.DisplayOptions do
       Application.get_env(:money, :strip_insignificant_fractional_unit, 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)
-    minus_sign_first = Keyword.get(opts, :minus_sign_first, default_minus_sign_first)
-
-    strip_insignificant_fractional_unit =
-      Keyword.get(opts, :strip_insignificant_fractional_unit, default_strip_insignificant_fractional_unit)
 
     %__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,
-      minus_sign_first: minus_sign_first,
-      strip_insignificant_fractional_unit: strip_insignificant_fractional_unit
+      symbol_on_right: default_symbol_on_right,
+      symbol_space: default_symbol_space,
+      fractional_unit: default_fractional_unit,
+      strip_insignificant_zeros: default_strip_insignificant_zeros,
+      code: default_code,
+      minus_sign_first: default_minus_sign_first,
+      strip_insignificant_fractional_unit: default_strip_insignificant_fractional_unit
     }
   end
+
+  defp get_currency_custom_display_options(currency) do
+    display_options = Map.get(custom_display_options(), currency, %{})
+
+    case display_options[:symbol] do
+      nil -> display_options
+      true -> Map.put(display_options, :symbol, Currency.symbol(currency))
+      false -> Map.put(display_options, :symbol, "")
+    end
+  end
 end


Test patch:
diff --git a/test/money_test.exs b/test/money_test.exs
index 5ca8f85..3f42e7f 100644
--- a/test/money_test.exs
+++ b/test/money_test.exs
@@ -3,7 +3,7 @@ defmodule MoneyTest do
   doctest Money
 
   require Money.Currency
-  import Money.Currency, only: [usd: 1, eur: 1, clf: 1, jpy: 1, omr: 1, xau: 1, zar: 1]
+  import Money.Currency, only: [usd: 1, eur: 1, clf: 1, jpy: 1, omr: 1, xau: 1, zar: 1, ars: 1, pen: 1, brl: 1]
 
   test "new/1 with default currency set" do
     try do
@@ -339,6 +339,55 @@ defmodule MoneyTest do
     end
   end
 
+  test "to_string configuration with custom_display_options" do
+    try do
+      # Default configuration
+      Application.put_env(:money, :separator, ",")
+      Application.put_env(:money, :delimeter, ".")
+      Application.put_env(:money, :symbol, false)
+      Application.put_env(:money, :symbol_on_right, false)
+      Application.put_env(:money, :symbol_space, false)
+      Application.put_env(:money, :fractional_unit, true)
+      Application.put_env(:money, :strip_insignificant_zeros, false)
+
+      # Custom configuration
+      Application.put_env(:money, :custom_display_options,
+        EUR: %{symbol: true, symbol_on_right: true, symbol_space: true, separator: ".", delimiter: ","},
+        JPY: %{symbol: true, symbol_on_right: true, separator: ","},
+        ARS: %{symbol: true, symbol_on_right: false, separator: ".", delimiter: ","},
+        PEN: %{symbol: true, symbol_on_right: false, symbol_space: true, separator: ",", delimiter: "."},
+        BRL: %{symbol: false, separator: ".", delimiter: ","}
+      )
+
+      # default configuration test
+      assert Money.to_string(usd(1_234_567_890)) == "12,345,678.90"
+
+      # custom configuration test
+      assert Money.to_string(eur(1_234_567_890)) == "12.345.678,90 €"
+      assert Money.to_string(jpy(1_234_567_890)) == "1,234,567,890¥"
+      assert Money.to_string(ars(1_234_567_890)) == "$12.345.678,90"
+      assert Money.to_string(pen(1_234_567_890)) == "S/ 12,345,678.90"
+      assert Money.to_string(brl(1_234_567_8900)) == "123.456.789,00"
+
+      # overwriting with options
+      assert Money.to_string(eur(1_234_567_890), symbol: false) == "12.345.678,90"
+      assert Money.to_string(jpy(1_234_567_890), symbol_space: true) == "1,234,567,890 ¥"
+      assert Money.to_string(ars(1_234_567_890), symbol_on_right: true) == "12.345.678,90$"
+      assert Money.to_string(pen(1_234_567_890), fractional_unit: false, symbol_space: false) == "S/12,345,678"
+      assert Money.to_string(brl(1_234_567_8900), symbol: true, strip_insignificant_zeros: true) == "R$123.456.789"
+    after
+      Application.delete_env(:money, :separator)
+      Application.delete_env(:money, :delimeter)
+      Application.delete_env(:money, :symbol)
+      Application.delete_env(:money, :symbol_on_right)
+      Application.delete_env(:money, :symbol_space)
+      Application.delete_env(:money, :fractional_unit)
+      Application.delete_env(:money, :strip_insignificant_zeros)
+
+      Application.delete_env(:money, :custom_display_options)
+    end
+  end
+
   test "to_string configuration with old delimeter" do
     try do
       Application.put_env(:money, :separator, ".")