Current section

Files

Jump to
ex_dinheiro lib moeda.ex
Raw

lib/moeda.ex

defmodule Moeda do
@moduledoc """
[![Build Status](https://travis-ci.org/ramondelemos/ex_dinheiro.svg?branch=master)](https://travis-ci.org/ramondelemos/ex_dinheiro?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/ramondelemos/ex_dinheiro/badge.svg?branch=master)](https://coveralls.io/github/ramondelemos/ex_dinheiro?branch=master)
"""
alias Moeda.Moedas
defstruct [:name, :symbol, :iso_code, :country_code, :exponent]
@typedoc """
Type that represents Moeda struct with:
:name as String.t that represents the name of the currency.
:symbol as String.t that represents symbol of the currency.
:iso_code as String.t that represents the ISO 4217 code.
:country_code as integer that represents the country code.
:exponent as integer that represents the exponent of the currency.
"""
@type t :: %__MODULE__{
name: String.t(),
symbol: String.t(),
iso_code: String.t(),
country_code: integer,
exponent: integer
}
@spec find!(String.t() | atom) :: t
@doc """
Return a map from an atom or string that represents an ISO 4217 code.
## Examples
iex> Moeda.find!(:BRL)
%Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}
iex> Moeda.find!(:NONE)
** (ArgumentError) 'NONE' does not represent an ISO 4217 code
"""
def find!(iso_code) when is_atom(iso_code) do
iso_code
|> Atom.to_string()
|> String.upcase()
|> String.to_atom()
|> do_find
end
def find!(iso_code) when is_binary(iso_code) do
iso_code
|> String.upcase()
|> String.to_atom()
|> do_find
end
defp do_find(iso_code) do
unofficial_currencies =
Application.get_env(:ex_dinheiro, :unofficial_currencies, %{})
currency = unofficial_currencies[iso_code]
result =
if currency do
currency
else
currencies = Moedas.get_currencies()
currencies[iso_code]
end
unless result,
do:
raise(
ArgumentError,
message: "'#{iso_code}' does not represent an ISO 4217 code"
)
raise_if_is_not_moeda(result, iso_code)
result
end
defp is_moeda(
%__MODULE__{
name: n,
symbol: s,
iso_code: i,
country_code: c,
exponent: e
} = m
)
when is_binary(n) and is_list(s) and is_binary(i) and is_integer(c) and
is_integer(e),
do: {true, m}
defp is_moeda(value), do: {false, value}
defp raise_if_is_not_moeda(value, iso_code) do
case is_moeda(value) do
{true, _} ->
true
{false, _} ->
raise(
ArgumentError,
message: ":#{iso_code} must to be associated to a Moeda struct"
)
end
end
@spec find(String.t() | atom) :: {:ok, t} | {:error, String.t()}
@doc """
Return a map from an atom or string that represents an ISO 4217 code.
## Examples
iex> Moeda.find(:BRL)
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
iex> Moeda.find("BRL")
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
iex> Moeda.find("NONE")
{:error, "'NONE' does not represent an ISO 4217 code"}
Its function ignore case sensitive.
## Examples
iex> Moeda.find(:brl)
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
iex> Moeda.find("brl")
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
Is possible to work with no official ISO currency code adding it in the system Mix config.
## Examples
iex> Moeda.find(:XBT)
{:error, "'XBT' does not represent an ISO 4217 code"}
iex> currencies = %{ XBT: %Moeda{name: "Bitcoin", symbol: '฿', iso_code: "XBT", country_code: 0, exponent: 8} }
iex> Application.put_env(:ex_dinheiro, :unofficial_currencies, currencies)
iex> Moeda.find("xbt")
{:ok, %Moeda{name: "Bitcoin", symbol: '฿', iso_code: "XBT", country_code: 0, exponent: 8}}
Is possible to override some official ISO currency code adding it in the system Mix config.
## Examples
iex> Moeda.find(:BRL)
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
iex> currencies = %{ BRL: %Moeda{name: "Moeda do Brasil", symbol: 'BR$', iso_code: "BRL", country_code: 986, exponent: 4}, USD: %Moeda{name: "Moeda do EUA", symbol: 'US$', iso_code: "USD", country_code: 986, exponent: 3} }
iex> Application.put_env(:ex_dinheiro, :unofficial_currencies, currencies)
iex> Moeda.find(:BRL)
{:ok, %Moeda{name: "Moeda do Brasil", symbol: 'BR$', iso_code: "BRL", country_code: 986, exponent: 4}}
iex> Moeda.find(:USD)
{:ok, %Moeda{name: "Moeda do EUA", symbol: 'US$', iso_code: "USD", country_code: 986, exponent: 3}}
iex> Application.delete_env(:ex_dinheiro, :unofficial_currencies)
iex> Moeda.find(:BRL)
{:ok, %Moeda{name: "Brazilian Real", symbol: 'R$', iso_code: "BRL", country_code: 986, exponent: 2}}
Be careful setting new currencies to Mix config.
## Examples
iex> Moeda.find(:XBT)
{:error, "'XBT' does not represent an ISO 4217 code"}
iex> currencies = %{ XBT: %{name: "Bitcoin", symbol: '฿', iso_code: "XBT", country_code: 0, exponent: 8} }
iex> Application.put_env(:ex_dinheiro, :unofficial_currencies, currencies)
iex> Moeda.find(:XBT)
{:error, ":XBT must to be associated to a Moeda struct"}
iex> currencies = %{ XBT: %Moeda{name: "Bitcoin", symbol: '฿', iso_code: "XBT", country_code: 0, exponent: 8} }
iex> Application.put_env(:ex_dinheiro, :unofficial_currencies, currencies)
iex> Moeda.find("xbt")
{:ok, %Moeda{name: "Bitcoin", symbol: '฿', iso_code: "XBT", country_code: 0, exponent: 8}}
"""
def find(iso_code) when is_atom(iso_code) or is_binary(iso_code) do
{:ok, find!(iso_code)}
rescue
e -> {:error, e.message}
end
@spec get_atom!(String.t() | atom) :: atom
@doc """
Return an atom from a value that represents an ISO 4217 code.
## Examples
iex> Moeda.get_atom!(:BRL)
:BRL
iex> Moeda.get_atom!("BRL")
:BRL
iex> Moeda.get_atom!(:NONE)
** (ArgumentError) 'NONE' does not represent an ISO 4217 code
Its function ignore case sensitive.
## Examples
iex> Moeda.get_atom!(:brl)
:BRL
iex> Moeda.get_atom!("brl")
:BRL
"""
def get_atom!(iso_code) do
currency = find!(iso_code)
currency.iso_code |> String.upcase() |> String.to_atom()
end
@spec get_atom(String.t() | atom) :: {:ok, atom}
@doc """
Return an atom from a value that represents an ISO 4217 code.
## Examples
iex> Moeda.get_atom(:BRL)
{:ok, :BRL}
iex> Moeda.get_atom(:NONE)
{:error, "'NONE' does not represent an ISO 4217 code"}
"""
def get_atom(iso_code) do
{:ok, get_atom!(iso_code)}
rescue
e -> {:error, e.message}
end
@spec get_factor!(String.t() | atom) :: float
@doc """
Return a multiplication factor from an ISO 4217 code.
## Examples
iex> Moeda.get_factor!(:BRL)
100.0
iex> Moeda.get_factor!("BRL")
100.0
iex> Moeda.get_factor!(:NONE)
** (ArgumentError) 'NONE' does not represent an ISO 4217 code
Its function ignore case sensitive.
## Examples
iex> Moeda.get_factor!(:brl)
100.0
iex> Moeda.get_factor!("brl")
100.0
"""
def get_factor!(iso_code) do
currency = find!(iso_code)
:math.pow(10, currency.exponent)
end
@spec get_factor(String.t() | atom) :: {:ok, float} | {:error, String.t()}
@doc """
Return a multiplication factor from an ISO 4217 code.
## Examples
iex> Moeda.get_factor(:BRL)
{:ok, 100.0}
iex> Moeda.get_factor(:NONE)
{:error, "'NONE' does not represent an ISO 4217 code"}
"""
def get_factor(iso_code) do
{:ok, get_factor!(iso_code)}
rescue
e -> {:error, e.message}
end
@spec to_string(String.t() | atom, float, Keywords.t()) ::
{:ok, String.t()} | {:error, String.t()}
@doc """
Return a formated string from a ISO 4217 code and a float value.
## Examples
iex> Moeda.to_string(:BRL, 100.0)
{:ok, "R$ 100,00"}
iex> Moeda.to_string(:NONE, 1000.5)
{:error, "'NONE' does not represent an ISO 4217 code"}
"""
def to_string(currency, valor, opts \\ []) do
{:ok, to_string!(currency, valor, opts)}
rescue
e -> {:error, e.message}
end
@spec to_string!(String.t() | atom, float, Keywords.t()) :: String.t()
@doc """
Return a formated string from a ISO 4217 code and a float value.
## Examples
iex> Moeda.to_string!(:BRL, 100.0)
"R$ 100,00"
iex> Moeda.to_string!("BRL", 1000.5)
"R$ 1.000,50"
iex> Moeda.to_string!(:BRL, -1.0)
"R$ -1,00"
iex> Moeda.to_string!(:NONE, 1000.5)
** (ArgumentError) 'NONE' does not represent an ISO 4217 code
Its function ignore case sensitive.
## Examples
iex> Moeda.to_string!(:bRl, 100.0)
"R$ 100,00"
iex> Moeda.to_string!("BrL", 1000.5)
"R$ 1.000,50"
Using options-style parameters you can change the behavior of the function.
- `thousand_separator` - default `"."`, set the thousand separator.
- `decimal_separator` - default `","`, set the decimal separator.
- `display_currency_symbol` - default `true`, put to `false` to hide de currency symbol.
- `display_currency_code` - default `false`, put to `true` to display de currency ISO 4217 code.
## Exemples
iex> Moeda.to_string!(:USD, 1000.5, thousand_separator: ",", decimal_separator: ".")
"$ 1,000.50"
iex> Moeda.to_string!(:USD, 1000.5, display_currency_symbol: false)
"1.000,50"
iex> Moeda.to_string!(:USD, 1000.5, display_currency_code: true)
"$ 1.000,50 USD"
iex> Moeda.to_string!(:USD, 1000.5, display_currency_code: true, display_currency_symbol: false)
"1.000,50 USD"
The default values also can be set in the system Mix config.
## Example:
iex> Application.put_env(:ex_dinheiro, :thousand_separator, ",")
iex> Application.put_env(:ex_dinheiro, :decimal_separator, ".")
iex> Moeda.to_string!(:USD, 1000.5)
"$ 1,000.50"
iex> Application.put_env(:ex_dinheiro, :display_currency_symbol, false)
iex> Moeda.to_string!(:USD, 5000.5)
"5,000.50"
iex> Application.put_env(:ex_dinheiro, :display_currency_code, true)
iex> Moeda.to_string!(:USD, 10000.0)
"10,000.00 USD"
The options-style parameters override values in the system Mix config.
## Example:
iex> Application.put_env(:ex_dinheiro, :thousand_separator, ",")
iex> Application.put_env(:ex_dinheiro, :decimal_separator, ".")
iex> Moeda.to_string!(:USD, 1000.5)
"$ 1,000.50"
iex> Moeda.to_string!(:BRL, 1000.5, thousand_separator: ".", decimal_separator: ",")
"R$ 1.000,50"
"""
def to_string!(currency, valor, opts \\ []) do
m = find!(currency)
unless is_float(valor),
do: raise(ArgumentError, message: "Value '#{valor}' must be float")
{thousand_separator, decimal_separator, display_currency_symbol,
display_currency_code} = get_config(opts)
parts =
valor
|> :erlang.float_to_binary(decimals: m.exponent)
|> String.split(".")
thousands =
parts
|> List.first()
|> String.reverse()
|> String.codepoints()
|> format_thousands(thousand_separator)
|> String.reverse()
decimals =
if m.exponent > 0 do
Enum.join([decimal_separator, List.last(parts)])
else
""
end
currency_symbol =
if display_currency_symbol do
m.symbol
else
""
end
currency_code =
if display_currency_code do
m.iso_code
else
""
end
[currency_symbol, " ", thousands, decimals, " ", currency_code]
|> Enum.join()
|> String.trim()
end
defp get_config(opts) do
conf_thousand_separator =
Application.get_env(:ex_dinheiro, :thousand_separator, ".")
conf_decimal_separator =
Application.get_env(:ex_dinheiro, :decimal_separator, ",")
conf_display_currency_symbol =
Application.get_env(:ex_dinheiro, :display_currency_symbol, true)
conf_display_currency_code =
Application.get_env(:ex_dinheiro, :display_currency_code, false)
thousand_separator =
Keyword.get(opts, :thousand_separator, conf_thousand_separator)
decimal_separator =
Keyword.get(opts, :decimal_separator, conf_decimal_separator)
display_currency_symbol =
Keyword.get(opts, :display_currency_symbol, conf_display_currency_symbol)
display_currency_code =
Keyword.get(opts, :display_currency_code, conf_display_currency_code)
{thousand_separator, decimal_separator, display_currency_symbol,
display_currency_code}
end
defp format_thousands([head | tail], separator, opts \\ []) do
position = Keyword.get(opts, :position, 1)
num =
if rem(position, 3) == 0 and head != "-" and tail != [] do
Enum.join([head, separator])
else
head
end
if tail != [] do
[num, format_thousands(tail, separator, position: position + 1)]
|> Enum.join()
else
num
end
end
end