Packages
localize
0.1.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.41.3
0.41.2
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.1
0.30.0
retired
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
0.1.0-alpha.1
Localization (parsing, formatting) of numbers, dates/time/calendar, units of measure, messages and lists. Includes localized collation.
Current section
Files
Jump to
Current section
Files
lib/localize/currency.ex
defmodule Localize.Currency do
@moduledoc """
Defines a currency structure and functions to manage currency
codes, validate currencies, and retrieve currency metadata.
Currency data is derived from the Unicode CLDR repository and
includes all ISO 4217 currency codes and territory-to-currency
mappings.
Locale-specific currency data (display names, pluralized names,
symbols) is loaded on demand from the locale data provider.
"""
alias Localize.SupplementalData
@type currency_code :: atom()
@type currency_status :: :all | :current | :historic | :tender | :unannotated
@type filter :: list(currency_status() | currency_code()) | currency_status() | currency_code()
@type territory :: atom() | String.t()
@type t :: %__MODULE__{
code: currency_code(),
alt_code: currency_code(),
name: String.t(),
tender: boolean(),
symbol: String.t(),
digits: non_neg_integer(),
rounding: non_neg_integer(),
narrow_symbol: String.t() | nil,
cash_digits: non_neg_integer(),
cash_rounding: non_neg_integer(),
iso_digits: non_neg_integer(),
decimal_separator: String.t() | nil,
grouping_separator: String.t() | nil,
count: map() | nil,
from: Date.t() | nil,
to: Date.t() | nil
}
defstruct code: nil,
alt_code: nil,
name: "",
symbol: "",
narrow_symbol: nil,
digits: 0,
rounding: 0,
cash_digits: 0,
cash_rounding: 0,
iso_digits: 0,
decimal_separator: nil,
grouping_separator: nil,
tender: false,
count: nil,
from: nil,
to: nil
# ── Known currency codes (loaded from ETF at compile time) ───
@currency_codes SupplementalData.currency_codes()
@territory_currencies SupplementalData.territory_currencies()
# ── Currency validation ──────────────────────────────────────
@doc """
Validates a currency code and returns its canonical atom form.
Checks both ISO 4217 codes and registered custom currencies.
### Arguments
* `currency_code` is an atom or string currency code.
### Returns
* `{:ok, currency_code}` where `currency_code` is an atom.
* `{:error, Localize.UnknownCurrencyError.t()}` if the code
is not known.
### Examples
iex> Localize.Currency.validate_currency("AUD")
{:ok, :AUD}
iex> Localize.Currency.validate_currency(:USD)
{:ok, :USD}
"""
@spec validate_currency(atom() | String.t()) ::
{:ok, currency_code()} | {:error, Exception.t()}
def validate_currency(currency_code) when is_binary(currency_code) do
currency_code
|> String.upcase()
|> String.to_atom()
|> validate_currency()
end
def validate_currency(currency_code) when is_atom(currency_code) do
if currency_code in known_currency_codes() do
{:ok, currency_code}
else
{:error, Localize.UnknownCurrencyError.exception(currency: currency_code)}
end
end
# ── Known currencies ─────────────────────────────────────────
@doc """
Returns a list of all known currency codes.
### Returns
* A list of atom currency codes.
### Examples
iex> codes = Localize.Currency.known_currency_codes()
iex> :USD in codes
true
"""
@spec known_currency_codes() :: [currency_code(), ...]
def known_currency_codes do
@currency_codes
end
@doc """
Returns whether the given currency code is known.
### Arguments
* `currency_code` is an atom or string currency code.
### Returns
* `true` or `false`.
### Examples
iex> Localize.Currency.known_currency_code?(:USD)
true
iex> Localize.Currency.known_currency_code?("GGG")
false
"""
@spec known_currency_code?(atom() | String.t()) :: boolean()
def known_currency_code?(currency_code) do
case validate_currency(currency_code) do
{:ok, _} -> true
{:error, _} -> false
end
end
# ── Territory currency functions ─────────────────────────────
@doc """
Returns a map of all territory codes to their currency history.
Each territory maps to a map of currency codes with date ranges
indicating when each currency was in use.
### Returns
* A map of `%{territory_code => %{currency_code => date_info}}`.
### Examples
iex> currencies = Localize.Currency.territory_currencies()
iex> us = Map.get(currencies, :US)
iex> Map.has_key?(us, :USD)
true
"""
@dialyzer {:nowarn_function, territory_currencies: 0}
@spec territory_currencies() :: %{required(atom()) => %{required(atom()) => map()}}
def territory_currencies do
@territory_currencies
end
@doc """
Returns the currency history for a specific territory.
### Arguments
* `territory` is a territory code atom or string (e.g., `:US` or `"US"`).
### Returns
* `{:ok, currency_map}` with currency codes and date ranges.
* `{:error, Localize.UnknownCurrencyError.t()}` if no currencies
are found for the territory.
### Examples
iex> {:ok, currencies} = Localize.Currency.territory_currencies(:US)
iex> Map.has_key?(currencies, :USD)
true
"""
@spec territory_currencies(territory()) ::
{:ok, map()} | {:error, Exception.t()}
def territory_currencies(territory) when is_binary(territory) do
territory
|> String.upcase()
|> String.to_atom()
|> territory_currencies()
end
def territory_currencies(territory) when is_atom(territory) do
case Map.fetch(territory_currencies(), territory) do
{:ok, currencies} ->
{:ok, currencies}
:error ->
{:error,
Localize.UnknownCurrencyError.exception(
currency: "No currencies for #{inspect(territory)} were found"
)}
end
end
@doc """
Returns the current currency for a given territory.
The current currency is the one with no `:to` end date and
where `:tender` is not `false`.
### Arguments
* `territory` is a territory code atom or string.
### Returns
* A currency code atom, or `nil` if no current currency exists.
### Examples
iex> Localize.Currency.current_currency_for_territory(:US)
:USD
iex> Localize.Currency.current_currency_for_territory(:AU)
:AUD
"""
@spec current_currency_for_territory(atom() | String.t()) ::
currency_code() | nil
def current_currency_for_territory(territory) when is_binary(territory) do
territory
|> String.upcase()
|> String.to_atom()
|> current_currency_for_territory()
end
def current_currency_for_territory(territory) when is_atom(territory) do
case territory_currencies(territory) do
{:ok, history} ->
history
|> Enum.find(fn {_currency, info} ->
Map.has_key?(info, :from) &&
!Map.has_key?(info, :to) &&
Map.get(info, :tender, true) != false
end)
|> case do
{currency, _info} -> currency
nil -> nil
end
{:error, _} ->
nil
end
end
@doc """
Returns a map of territory codes to their current currency.
Territories with no current currency are excluded.
### Returns
* A map of `%{territory_code => currency_code}`.
### Examples
iex> map = Localize.Currency.current_territory_currencies()
iex> Map.get(map, :US)
:USD
"""
@spec current_territory_currencies() :: %{atom() => currency_code()}
def current_territory_currencies do
territory_currencies()
|> Enum.reject(fn {territory, _} -> territory == :ZZ end)
|> Enum.map(fn {territory, _} ->
{territory, current_currency_for_territory(territory)}
end)
|> Enum.reject(fn {_, currency} -> is_nil(currency) end)
|> Map.new()
end
# ── Locale-based currency functions ──────────────────────────
@doc """
Returns the effective currency for a given locale.
If the language tag has a `cu` Unicode extension key set,
that currency is returned. Otherwise, the current currency
for the tag's territory is returned.
### Arguments
* `locale` is a locale identifier string, atom, or a
`t:Localize.LanguageTag.t/0` struct.
### Returns
* `{:ok, currency_code}` where `currency_code` is an atom.
* `{:error, exception}` if the locale is not valid.
### Examples
iex> {:ok, tag} = Localize.LanguageTag.parse("en-US")
iex> Localize.Currency.currency_from_locale(tag)
{:ok, :USD}
iex> Localize.Currency.currency_from_locale("en-AU")
{:ok, :AUD}
"""
@spec currency_from_locale(Localize.LanguageTag.t() | String.t() | atom()) ::
{:ok, currency_code()} | {:error, Exception.t()}
def currency_from_locale(locale) when is_binary(locale) or is_atom(locale) do
with {:ok, language_tag} <- Localize.validate_locale(locale) do
currency_from_locale(language_tag)
end
end
def currency_from_locale(%Localize.LanguageTag{locale: %{cu: nil}} = locale) do
current_currency_from_locale(locale)
end
def currency_from_locale(%Localize.LanguageTag{locale: %{cu: currency}}) do
{:ok, currency}
end
def currency_from_locale(%Localize.LanguageTag{} = locale) do
current_currency_from_locale(locale)
end
@doc """
Returns the effective currency format for a given locale.
If the language tag has a `cf` Unicode extension key set to
`:account`, returns `:accounting`. Otherwise returns `:currency`.
### Arguments
* `locale` is a locale identifier string, atom, or a
`t:Localize.LanguageTag.t/0` struct.
### Returns
* `{:ok, format}` where `format` is `:currency` or `:accounting`.
* `{:error, exception}` if the locale is not valid.
### Examples
iex> {:ok, tag} = Localize.LanguageTag.parse("en-US")
iex> Localize.Currency.currency_format_from_locale(tag)
{:ok, :currency}
iex> Localize.Currency.currency_format_from_locale("en-US")
{:ok, :currency}
"""
@spec currency_format_from_locale(Localize.LanguageTag.t() | String.t() | atom()) ::
{:ok, :currency | :accounting} | {:error, Exception.t()}
def currency_format_from_locale(locale) when is_binary(locale) or is_atom(locale) do
with {:ok, language_tag} <- Localize.validate_locale(locale) do
currency_format_from_locale(language_tag)
end
end
def currency_format_from_locale(%Localize.LanguageTag{locale: %{cf: :account}}) do
{:ok, :accounting}
end
def currency_format_from_locale(%Localize.LanguageTag{}) do
{:ok, :currency}
end
@doc """
Returns the current currency for a locale's territory.
This function does not consider the `U` extension parameter `cu`.
Use `currency_from_locale/1` to get the effective currency
including overrides.
### Arguments
* `locale` is a locale identifier string, atom, or a
`t:Localize.LanguageTag.t/0` struct.
### Returns
* `{:ok, currency_code}` where `currency_code` is an atom.
* `{:ok, nil}` if the locale has no territory or the territory
has no current currency.
* `{:error, exception}` if the locale is not valid.
### Examples
iex> {:ok, tag} = Localize.LanguageTag.parse("en-AU")
iex> Localize.Currency.current_currency_from_locale(tag)
{:ok, :AUD}
iex> Localize.Currency.current_currency_from_locale("en-US")
{:ok, :USD}
"""
@spec current_currency_from_locale(Localize.LanguageTag.t() | String.t() | atom()) ::
{:ok, currency_code() | nil} | {:error, Exception.t()}
def current_currency_from_locale(locale) when is_binary(locale) or is_atom(locale) do
with {:ok, language_tag} <- Localize.validate_locale(locale) do
current_currency_from_locale(language_tag)
end
end
def current_currency_from_locale(%Localize.LanguageTag{} = locale) do
with {:ok, territory} <- Localize.Territory.territory_from_locale(locale) do
{:ok, current_currency_for_territory(territory)}
end
end
@doc """
Returns the full currency history for a locale's territory.
Resolves the territory from the locale using
`Localize.Territory.territory_from_locale/1` (which considers the `rg` extension,
the explicit territory, and likely subtags), then returns the
currency history for that territory.
### Arguments
* `locale` is a locale identifier string, atom, or a
`t:Localize.LanguageTag.t/0` struct.
### Returns
* `{:ok, currency_map}` where `currency_map` is a map of
`%{currency_code => %{from: date, to: date, ...}}` entries.
* `{:error, exception}` if the locale or territory cannot
be resolved.
### Examples
iex> {:ok, history} = Localize.Currency.currency_history_for_locale("en-US")
iex> Map.has_key?(history, :USD)
true
iex> {:ok, history} = Localize.Currency.currency_history_for_locale("de")
iex> Map.has_key?(history, :EUR)
true
iex> {:ok, history} = Localize.Currency.currency_history_for_locale("ja")
iex> Map.has_key?(history, :JPY)
true
"""
@spec currency_history_for_locale(Localize.LanguageTag.t() | String.t() | atom()) ::
{:ok, map()} | {:error, Exception.t()}
def currency_history_for_locale(locale) when is_binary(locale) or is_atom(locale) do
with {:ok, language_tag} <- Localize.validate_locale(locale) do
currency_history_for_locale(language_tag)
end
end
def currency_history_for_locale(%Localize.LanguageTag{} = locale) do
with {:ok, territory} <- Localize.Territory.territory_from_locale(locale) do
territory_currencies(territory)
end
end
# ── Locale-specific currency functions ────────────────────────
@doc """
Returns the currency metadata for the requested currency code
in the given locale.
Looks up localized currency data (display name, symbol, plural
forms) for a specific currency code.
### Arguments
* `currency_code` is an atom or string currency code.
* `options` is a keyword list of options.
### Options
* `:locale` is a locale identifier atom or a
`t:Localize.LanguageTag.t/0`. The default is `:en`.
### Returns
* `{:ok, Localize.Currency.t()}` with localized currency metadata.
* `{:error, exception}` if the currency code is unknown or
the locale data cannot be loaded.
"""
@spec currency_for_code(atom() | String.t(), Keyword.t()) ::
{:ok, t()} | {:error, Exception.t()}
def currency_for_code(currency_code, options \\ []) do
locale = Keyword.get(options, :locale, Localize.get_locale())
with {:ok, code} <- validate_currency(currency_code),
{:ok, currencies} <- currencies_for_locale(locale) do
case Map.get(currencies, code) do
nil -> {:error, Localize.UnknownCurrencyError.exception(currency: currency_code)}
currency -> {:ok, currency}
end
end
end
@doc """
Returns a map of all currencies for a given locale.
Each key is a currency code atom and each value is a
`t:Localize.Currency.t/0` struct with localized display names,
symbols, and plural forms. The result can be filtered by
currency status.
### Arguments
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
* `only` is a filter for currency status. The default is `:all`.
* `except` is a filter for currencies to exclude. The default
is `nil`.
### Returns
* `{:ok, currencies_map}` where `currencies_map` is a map of
`%{currency_code => Localize.Currency.t()}`.
* `{:error, exception}` if the locale data cannot be loaded.
"""
@spec currencies_for_locale(
Localize.LanguageTag.t() | atom() | String.t(),
filter(),
filter()
) ::
{:ok, map()} | {:error, Exception.t()}
def currencies_for_locale(locale, only \\ :all, except \\ nil) do
locale_id = to_locale_id(locale)
with {:ok, currencies} <- Localize.Locale.get(locale_id, [:currencies]) do
{:ok, currency_filter(currencies, only, except)}
end
end
@doc """
Returns a map matching currency strings to currency codes
for a given locale.
A currency string is a localized name or symbol representing
a currency in a locale-specific manner. The map can be used
to parse user input into currency codes.
### Arguments
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
* `only` is a filter for currency status. The default is `:all`.
* `except` is a filter for currencies to exclude. The default
is `nil`.
### Returns
* `{:ok, string_map}` where `string_map` is a map of
`%{downcased_string => currency_code}`.
* `{:error, exception}` if the locale data cannot be loaded.
"""
@spec currency_strings(
Localize.LanguageTag.t() | atom() | String.t(),
filter(),
filter()
) ::
{:ok, map()} | {:error, Exception.t()}
def currency_strings(locale, only \\ :all, except \\ nil) do
with {:ok, currencies} <- currencies_for_locale(locale, only, except) do
{:ok, build_currency_strings(currencies)}
end
end
@doc """
Returns the list of strings that map to a given currency
code in a locale.
This is the inverse of `currency_strings/3` filtered to a
single currency. It returns all localized representations
(name, symbol, plural forms) that identify the currency.
### Arguments
* `currency` is a currency code atom or string.
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
### Returns
* `{:ok, strings}` where `strings` is a list of downcased
strings that represent the currency in the locale.
* `{:error, exception}` if the currency is unknown or the
locale data cannot be loaded.
"""
@spec strings_for_currency(
currency_code() | String.t(),
Localize.LanguageTag.t() | atom() | String.t()
) ::
{:ok, [String.t()]} | {:error, Exception.t()}
def strings_for_currency(currency, locale) do
with {:ok, currency_code} <- validate_currency(currency),
{:ok, strings} <- currency_strings(locale) do
result =
strings
|> Enum.filter(fn {_string, code} -> code == currency_code end)
|> Enum.map(fn {string, _code} -> string end)
{:ok, result}
end
end
@doc """
Returns the display name for a currency.
When given a `t:Localize.Currency.t/0` struct, returns its
`:name` field directly. When given a currency code, looks up
the localized name from the locale data.
### Arguments
* `currency` is a currency code atom, string, or a
`t:Localize.Currency.t/0` struct.
* `options` is a keyword list of options.
### Options
* `:locale` is a locale identifier atom or a
`t:Localize.LanguageTag.t/0`. The default is `:en`.
### Returns
* `{:ok, display_name}` where `display_name` is a string.
* `{:error, exception}` if the currency has no display name
or is unknown.
"""
@spec display_name(atom() | String.t() | t(), Keyword.t()) ::
{:ok, String.t()} | {:error, Exception.t()}
def display_name(currency, options \\ [])
def display_name(%__MODULE__{name: nil, code: code}, _options) do
{:error, Localize.CurrencyNoDisplayNameError.exception(currency: code)}
end
def display_name(%__MODULE__{name: name}, _options) do
{:ok, name}
end
def display_name(currency_code, options) do
with {:ok, currency_data} <- currency_for_code(currency_code, options) do
display_name(currency_data, options)
end
end
@doc """
Returns the appropriate currency display name based on
plural rules for the locale.
Uses the locale's cardinal plural rules to determine which
plural form of the currency name to use for the given number.
### Arguments
* `number` is an integer, float, or Decimal.
* `currency` is a currency code atom.
* `options` is a keyword list of options.
### Options
* `:locale` is a locale identifier atom or a
`t:Localize.LanguageTag.t/0`. The default is `:en`.
### Returns
* `{:ok, pluralized_name}` where `pluralized_name` is the
appropriate plural form string.
* `{:error, exception}` if the currency is unknown or the
locale data cannot be loaded.
"""
@spec pluralize(number(), currency_code(), Keyword.t()) ::
{:ok, String.t()} | {:error, Exception.t()}
def pluralize(number, currency, options \\ []) do
locale = Keyword.get(options, :locale, Localize.get_locale())
with {:ok, currency_code} <- validate_currency(currency),
{:ok, currency_data} <- currency_for_code(currency_code, locale: locale) do
counts =
(currency_data.count || %{})
|> Map.put_new(:other, currency_data.name)
plural_category =
Localize.Number.PluralRule.Cardinal.plural_rule(number, locale)
{:ok, Map.get(counts, plural_category, counts[:other])}
end
end
# ── Bang versions ──────────────────────────────────────────
@doc """
Same as `currency_for_code/2` but raises on error.
### Arguments
* `currency_code` is an atom or string currency code.
* `options` is a keyword list of options.
### Options
* `:locale` is a locale identifier atom or a
`t:Localize.LanguageTag.t/0`. The default is `:en`.
### Returns
* A `t:Localize.Currency.t/0` struct.
### Raises
* Raises an exception if the currency code is unknown or the
locale data cannot be loaded.
"""
@spec currency_for_code!(atom() | String.t(), Keyword.t()) :: t()
def currency_for_code!(currency_code, options \\ []) do
case currency_for_code(currency_code, options) do
{:ok, currency} -> currency
{:error, exception} -> raise exception
end
end
@doc """
Same as `currencies_for_locale/3` but raises on error.
### Arguments
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
* `only` is a filter for currency status. The default is `:all`.
* `except` is a filter for currencies to exclude. The default
is `nil`.
### Returns
* A map of `%{currency_code => Localize.Currency.t()}`.
### Raises
* Raises an exception if the locale data cannot be loaded.
"""
@spec currencies_for_locale!(
Localize.LanguageTag.t() | atom() | String.t(),
filter(),
filter()
) :: map()
def currencies_for_locale!(locale, only \\ :all, except \\ nil) do
case currencies_for_locale(locale, only, except) do
{:ok, currencies} -> currencies
{:error, exception} -> raise exception
end
end
@doc """
Same as `currency_strings/3` but raises on error.
### Arguments
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
* `only` is a filter for currency status. The default is `:all`.
* `except` is a filter for currencies to exclude. The default
is `nil`.
### Returns
* A map of `%{downcased_string => currency_code}`.
### Raises
* Raises an exception if the locale data cannot be loaded.
"""
@spec currency_strings!(
Localize.LanguageTag.t() | atom() | String.t(),
filter(),
filter()
) :: map()
def currency_strings!(locale, only \\ :all, except \\ nil) do
case currency_strings(locale, only, except) do
{:ok, strings} -> strings
{:error, exception} -> raise exception
end
end
@doc """
Same as `display_name/2` but raises on error.
### Arguments
* `currency` is a currency code atom, string, or a
`t:Localize.Currency.t/0` struct.
* `options` is a keyword list of options.
### Options
* `:locale` is a locale identifier atom or a
`t:Localize.LanguageTag.t/0`. The default is `:en`.
### Returns
* A display name string.
### Raises
* Raises an exception if the currency has no display name
or is unknown.
"""
@spec display_name!(atom() | String.t() | t(), Keyword.t()) :: String.t()
def display_name!(currency, options \\ []) do
case display_name(currency, options) do
{:ok, name} -> name
{:error, exception} -> raise exception
end
end
@doc """
Same as `strings_for_currency/2` but raises on error.
### Arguments
* `currency` is a currency code atom or string.
* `locale` is a locale identifier atom, string, or a
`t:Localize.LanguageTag.t/0`.
### Returns
* A list of downcased strings that represent the currency
in the locale.
### Raises
* Raises an exception if the currency is unknown or the
locale data cannot be loaded.
"""
@spec strings_for_currency!(
currency_code() | String.t(),
Localize.LanguageTag.t() | atom() | String.t()
) ::
[String.t()]
def strings_for_currency!(currency, locale) do
case strings_for_currency(currency, locale) do
{:ok, strings} -> strings
{:error, exception} -> raise exception
end
end
@doc """
Same as `territory_currencies/1` but raises on error.
### Arguments
* `territory` is a territory code atom or string (e.g., `:US` or `"US"`).
### Returns
* A map of currency codes and date ranges.
### Raises
* Raises an exception if no currencies are found for the territory.
"""
@spec territory_currencies!(territory()) :: map()
def territory_currencies!(territory) do
case territory_currencies(territory) do
{:ok, currencies} -> currencies
{:error, exception} -> raise exception
end
end
# ── Currency filtering ─────────────────────────────────────
@doc """
Filters a map of currencies by status predicates.
### Arguments
* `currencies` is a map of `%{currency_code => Localize.Currency.t()}`.
* `only` is a filter or list of filters to include. The default
is `:all`.
* `except` is a filter or list of filters to exclude. The default
is `nil`.
### Returns
* A filtered map of currencies.
"""
@spec currency_filter(map(), filter(), filter()) :: map()
def currency_filter(currencies, only \\ :all, except \\ nil)
def currency_filter(currencies, :all, nil) do
currencies
end
def currency_filter(currencies, only, except) when is_map(currencies) do
included = expand_filter(currencies, :only, List.wrap(only))
excluded = expand_filter(currencies, :except, List.wrap(except))
included
|> Kernel.--(excluded)
|> Map.new()
end
defp expand_filter(_currencies, :only, [:all]) do
[]
end
defp expand_filter(currencies, :only, [:all | _]) do
Enum.to_list(currencies)
end
defp expand_filter(_currencies, :except, [nil]) do
[]
end
defp expand_filter(currencies, _, filter_list) do
currencies_list = Enum.to_list(currencies)
Enum.flat_map(filter_list, fn
:all ->
currencies_list
:historic ->
Enum.filter(currencies_list, fn {_, c} -> historic?(c) end)
:tender ->
Enum.filter(currencies_list, fn {_, c} -> tender?(c) end)
:current ->
Enum.filter(currencies_list, fn {_, c} -> current?(c) end)
:annotated ->
Enum.filter(currencies_list, fn {_, c} -> annotated?(c) end)
:unannotated ->
Enum.filter(currencies_list, fn {_, c} -> unannotated?(c) end)
code when is_atom(code) ->
Enum.filter(currencies_list, fn {k, _} -> k == code end)
code when is_binary(code) ->
atom_code = String.to_atom(code)
Enum.filter(currencies_list, fn {k, _} -> k == atom_code end)
end)
|> Enum.uniq()
end
# ── Currency status predicates ───────────────────────────────
@doc """
Returns whether a currency is historic (no longer in use).
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `true` or `false`.
"""
@spec historic?(t()) :: boolean()
def historic?(%__MODULE__{} = currency) do
is_nil(currency.iso_digits) ||
(is_integer(currency.to) && currency.to < Date.utc_today().year)
end
@doc """
Returns whether a currency is legal tender.
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `true` or `false`.
"""
@spec tender?(t()) :: boolean()
def tender?(%__MODULE__{} = currency) do
!!currency.tender
end
@doc """
Returns whether a currency is currently in use.
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `true` or `false`.
"""
@spec current?(t()) :: boolean()
def current?(%__MODULE__{} = currency) do
!is_nil(currency.iso_digits) && is_nil(currency.to)
end
@doc """
Returns whether a currency name contains annotations.
Annotated currencies typically have parenthetical descriptions
and are often financial instruments rather than legal tender.
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `true` or `false`.
"""
@spec annotated?(t()) :: boolean()
def annotated?(%__MODULE__{} = currency) do
String.contains?(currency.name, "(")
end
@doc """
Returns whether a currency name does not contain annotations.
### Arguments
* `currency` is a `t:Localize.Currency.t/0` struct.
### Returns
* `true` or `false`.
"""
@spec unannotated?(t()) :: boolean()
def unannotated?(%__MODULE__{} = currency) do
!annotated?(currency)
end
# ── Private helpers ──────────────────────────────────────────
@rtl_mark "\u200F"
defp to_locale_id(locale), do: Localize.Locale.to_locale_id(locale)
defp build_currency_strings(currencies) do
currency_string_pairs =
Enum.flat_map(currencies, fn {code, currency} ->
strings =
[currency.name, currency.symbol, to_string(code)]
|> Kernel.++(if currency.count, do: Map.values(currency.count), else: [])
|> Enum.reject(&is_nil/1)
|> Enum.map(&String.downcase/1)
|> Enum.map(&String.trim_trailing(&1, @rtl_mark))
|> Enum.map(&String.trim_trailing(&1, "."))
|> Enum.uniq()
Enum.map(strings, fn string -> {string, code} end)
end)
string_map =
currency_string_pairs
|> resolve_duplicate_strings(currencies)
|> Map.new()
add_unique_narrow_symbols(string_map, currencies)
end
defp resolve_duplicate_strings(pairs, currencies) do
pairs
|> Enum.sort_by(fn {string, _code} -> string end)
|> do_resolve_duplicates(currencies)
end
defp do_resolve_duplicates([], _currencies), do: []
defp do_resolve_duplicates([pair], _currencies), do: [pair]
defp do_resolve_duplicates(
[{string, code1}, {string, code2} | rest],
currencies
) do
currency1 = Map.get(currencies, code1)
currency2 = Map.get(currencies, code2)
cond do
currency1 != nil and currency2 != nil and historic?(currency1) and current?(currency2) ->
do_resolve_duplicates([{string, code2} | rest], currencies)
currency1 != nil and currency2 != nil and current?(currency1) and historic?(currency2) ->
do_resolve_duplicates([{string, code1} | rest], currencies)
true ->
do_resolve_duplicates(rest, currencies)
end
end
defp do_resolve_duplicates([pair | rest], currencies) do
[pair | do_resolve_duplicates(rest, currencies)]
end
defp add_unique_narrow_symbols(string_map, currencies) do
Enum.reduce(currencies, string_map, fn {code, currency}, acc ->
cond do
is_nil(currency.narrow_symbol) ->
acc
Map.has_key?(acc, String.downcase(currency.narrow_symbol)) ->
acc
true ->
Map.put(acc, String.downcase(currency.narrow_symbol), code)
end
end)
end
end