defmodule Cldr.Number.Format.Options do
@moduledoc """
Functions to validate and transform
options that guide number formatting
"""
alias Cldr.Number.{System, Symbol, Format}
alias Cldr.Number.Format.Compiler
alias Cldr.Currency
alias Cldr.LanguageTag
# These are the options set in the
# struct guide formatting
@options [
:locale,
:number_system,
:currency,
:format,
:currency_digits,
:currency_spacing,
:currency_symbol,
:symbols,
:minimum_grouping_digits,
:pattern,
:rounding_mode,
:fractional_digits,
:maximum_integer_digits,
:round_nearest
]
# These are the options that can be supplied
# through the api
@valid_options @options --
[:currency_spacing, :pattern] ++ [:cash]
@short_format_styles [
:currency_short,
:currency_long_with_symbol,
:currency_long,
:decimal_short,
:decimal_long
]
@rounding_modes [
:down,
:half_up,
:half_even,
:ceiling,
:floor,
:half_down,
:up
]
@standard_formats [
:standard,
:accounting,
:currency,
:percent
]
@currency_formats [
:currency,
:accounting,
:currency_long,
:currency_long_with_symbol,
:currency_short
]
@currency_symbol [
:standard,
:iso,
:narrow,
:symbol
]
@type fixed_formats :: :standard | :currency | :accounting | :short | :long
@type format :: binary() | fixed_formats()
@type currency_symbol :: :standard | :iso
@type short_format_styles ::
:currency_short | :currency_long | :currency_long_with_symbol | :decimal_short | :decimal_long
@type t :: %__MODULE__{
locale: LanguageTag.t(),
number_system: System.system_name(),
currency: Currency.code() | Currency.t(),
format: format(),
currency_digits: pos_integer(),
currency_spacing: map(),
symbols: Symbol.t(),
minimum_grouping_digits: pos_integer(),
pattern: String.t(),
rounding_mode: Decimal.rounding(),
fractional_digits: pos_integer(),
maximum_integer_digits: pos_integer(),
round_nearest: pos_integer()
}
defstruct @options
@spec validate_options(Cldr.Math.number_or_decimal(), Cldr.backend(), list({atom, term})) ::
{:ok, t} | {:error, {module(), String.t()}}
def validate_options(number, backend, options) do
with {:ok, options} <- ensure_only_valid_keys(@valid_options, options),
{:ok, backend} <- Cldr.validate_backend(backend) do
options =
Module.concat(backend, Number).default_options()
|> Keyword.merge(options)
|> Map.new
Enum.reduce_while(@options, options, fn option, options ->
case validate_option(option, options, backend, Map.get(options, option)) do
{:ok, result} -> {:cont, Map.put(options, option, result)}
{:error, _} = error -> {:halt, error}
end
end)
|> resolve_standard_format(backend)
|> confirm_currency_format_has_currency_code
|> set_pattern(number)
|> maybe_set_iso_currency_symbol
|> structify(__MODULE__)
|> wrap_ok
end
end
def wrap_ok(%__MODULE__{} = options) do
{:ok, options}
end
def wrap_ok(other) do
other
end
# TODO for ex_cldr_numbers 3.0
def ensure_only_valid_keys(_valid_options, options) do
{:ok, options}
end
# def ensure_only_valid_keys(valid_options, options) do
# option_keys = Keyword.keys(options)
#
# if (invalid = (option_keys -- valid_options)) == [] do
# {:ok, options}
# else
# {:error, {ArgumentError, "Invalid options found: #{inspect invalid}"}}
# end
# end
def resolve_standard_format(%{format: format} = options, backend)
when format in @standard_formats do
locale = Map.fetch!(options, :locale)
number_system = Map.fetch!(options, :number_system)
with {:ok, formats} <- Format.formats_for(locale, number_system, backend) do
if resolved_format = Map.get(formats, format, format) do
Map.put(options, :format, resolved_format)
else
{:error,
{Cldr.UnknownFormatError,
"The locale #{inspect Map.fetch!(locale, :cldr_locale_name)} " <>
"with number system #{inspect number_system} " <>
"does not define a format #{inspect format}"}}
end
end
end
def resolve_standard_format(other, _backend) do
other
end
@currency_placeholder Compiler.placeholder(:currency)
@iso_placeholder Compiler.placeholder(:currency) <> Compiler.placeholder(:currency)
def confirm_currency_format_has_currency_code(%{format: format, currency: nil} = options)
when is_binary(format) do
if String.contains?(format, @currency_placeholder) do
{:error,
{Cldr.FormatError,
"currency format #{inspect(format)} requires that " <>
"options[:currency] be specified"}}
else
options
end
end
def confirm_currency_format_has_currency_code(other) do
other
end
def maybe_set_iso_currency_symbol(%{format: format} = options) do
%{currency_symbol: currency_symbol} = options
Map.put(options, :format, maybe_adjust_currency_symbol(format, currency_symbol))
end
def maybe_set_iso_currency_symbol(other) do
other
end
def set_pattern(options, number) when is_map(options) and is_number(number) and number < 0 do
Map.put(options, :pattern, :negative)
end
def set_pattern(options, %Decimal{sign: sign}) when is_map(options) and sign < 0 do
Map.put(options, :pattern, :negative)
end
def set_pattern(options, _number) when is_map(options) do
Map.put(options, :pattern, :positive)
end
def set_pattern(other, _number) do
other
end
def structify(options, module) when is_map(options) do
struct(module, options)
end
def structify(other, _module) do
other
end
def validate_option(:locale, _options, backend, nil) do
{:ok, backend.get_locale()}
end
def validate_option(:locale, _options, backend, locale) do
with {:ok, locale} <- Cldr.validate_locale(locale, backend) do
{:ok, locale}
end
end
# Number system is extracted from the locale
def validate_option(:number_system, options, backend, number_system)
when is_nil(number_system) or number_system == :default do
number_system =
options
|> Map.fetch!(:locale)
|> System.number_system_from_locale(backend)
{:ok, number_system}
end
def validate_option(:number_system, options, backend, number_system) do
locale = Map.fetch!(options, :locale)
System.system_name_from(number_system, locale, backend)
end
def validate_option(:currency, %{format: format, locale: locale}, _backend, nil)
when format in @currency_formats do
{:ok, Cldr.Currency.currency_from_locale(locale)}
end
def validate_option(:currency, %{format: format, locale: locale}, _backend, nil)
when is_binary(format) do
if String.contains?(format, @currency_placeholder) do
{:ok, Cldr.Currency.currency_from_locale(locale)}
else
{:ok, nil}
end
end
def validate_option(:currency, _options, _backend, nil) do
{:ok, nil}
end
def validate_option(:currency, _options, _backend, %Cldr.Currency{} = currency) do
{:ok, currency}
end
def validate_option(:currency, _options, _backend, currency) do
with {:ok, currency} <- Cldr.validate_currency(currency) do
{:ok, currency}
else
{:error, _} ->
case DigitalToken.validate_token(currency) do
{:ok, token} -> {:ok, token}
{:error, _} -> {:error, Cldr.unknown_currency_error(currency)}
end
end
end
# If a currency code is provided then a currency
# format is forced
def validate_option(:format, options, _backend, nil) do
locale = Map.fetch!(options, :locale)
if Map.fetch!(options, :currency) do
{:ok, Currency.currency_format_from_locale(locale)}
else
{:ok, :standard}
end
end
def validate_option(:format, options, _backend, :short) do
if Map.get(options, :currency) do
{:ok, :currency_short}
else
{:ok, :decimal_short}
end
end
def validate_option(:format, options, _backend, :long) do
if Map.get(options, :currency) do
{:ok, :currency_long}
else
{:ok, :decimal_long}
end
end
@exclude_formats [:accounting, :currency_short, :currency_long, :currency_long_with_symbol]
def validate_option(:format, options, _backend, format)
when is_atom(format) and format not in @exclude_formats do
locale = Map.fetch!(options, :locale)
if Map.get(options, :currency) do
{:ok, Currency.currency_format_from_locale(locale)}
else
{:ok, format}
end
end
def validate_option(:format, _options, _backend, format) do
{:ok, format}
end
# Currency digits is an opaque option that is a proxy
# for the `:cash` parameter which is set to true or false
def validate_option(:currency_digits, options, _backend, _currency_digits) do
if Map.get(options, :cash) do
{:ok, :cash}
else
{:ok, :accounting}
end
end
# Currency spacing isn't really a user option
# Its derived for currency formats only
def validate_option(:currency_spacing, %{format: format} = options, backend, _spacing)
when format in [:currency, :accounting] do
locale = Map.fetch!(options, :locale)
number_system = Map.fetch!(options, :number_system)
module = Module.concat(backend, Number.Format)
{:ok, module.currency_spacing(locale, number_system)}
end
def validate_option(:currency_spacing, _options, _backend, _currency_spacing) do
{:ok, nil}
end
def validate_option(:currency_symbol, _options, _backend, nil) do
{:ok, nil}
end
def validate_option(:currency_symbol, _options, _backend, currency_symbol)
when currency_symbol in @currency_symbol do
{:ok, currency_symbol}
end
def validate_option(:currency_symbol, _options, _backend, currency_symbol)
when is_binary(currency_symbol) do
{:ok, currency_symbol}
end
def validate_option(:currency_symbol, _options, _backend, other) do
{:error,
{ArgumentError,
":currency_symbol must be :standard, :iso, :narrow, :symbol, " <>
"a string or nil. Found #{inspect other}"
}}
end
def validate_option(:symbols, options, backend, _any) do
locale = Map.fetch!(options, :locale)
number_system = Map.fetch!(options, :number_system)
case Symbol.number_symbols_for(locale, number_system, backend) do
{:ok, symbols} -> {:ok, symbols}
_other -> {:ok, nil}
end
end
def validate_option(:minimum_grouping_digits, _options, _backend, nil) do
{:ok, 0}
end
def validate_option(:minimum_grouping_digits, _options, _backend, int)
when is_integer(int) and int >= 0 do
{:ok, int}
end
def validate_option(:minimum_grouping_digits, _options, _backend, other) do
{:error,
{ArgumentError,
":minimum_grouping_digits must be a positive integer or nil. Found #{inspect other}"}}
end
def validate_option(:fractional_digits, _options, _backend, nil) do
{:ok, nil}
end
def validate_option(:fractional_digits, _options, _backend, int)
when is_integer(int) and int >= 0 do
{:ok, int}
end
def validate_option(:fractional_digits, _options, _backend, other) do
{:error,
{ArgumentError,
":fractional_digits must be a an integer >= 0 or nil. Found #{inspect other}"}}
end
def validate_option(:maximum_integer_digits, _options, _backend, nil) do
{:ok, nil}
end
def validate_option(:maximum_integer_digits, _options, _backend, int)
when is_integer(int) and int >= 0 do
{:ok, int}
end
def validate_option(:maximum_integer_digits, _options, _backend, other) do
{:error,
{ArgumentError,
":maximum_integer_digits must be a an integer >= 0 or nil. Found #{inspect other}"}}
end
def validate_option(:round_nearest, _options, _backend, nil) do
{:ok, nil}
end
def validate_option(:round_nearest, _options, _backend, int)
when is_integer(int) and int > 0 do
{:ok, int}
end
def validate_option(:round_nearest, _options, _backend, other) do
{:error,
{ArgumentError,
":round_nearest must be a positive integer or nil. Found #{inspect other}"}}
end
def validate_option(:rounding_mode, _options, _backend, nil) do
{:ok, :half_even}
end
def validate_option(:rounding_mode, _options, _backend, rounding_mode)
when rounding_mode in @rounding_modes do
{:ok, rounding_mode}
end
def validate_option(:rounding_mode, _options, _backend, other) do
{:error,
{ArgumentError,
":rounding_mode must be one of #{inspect @rounding_modes}. Found #{inspect other}"}}
end
def validate_option(:pattern, _options, _backend, _pattern) do
{:ok, nil}
end
@spec short_format_styles() :: list(atom())
def short_format_styles do
@short_format_styles
end
@doc false
# Sometimes we want the standard format for a currency but we want the
# ISO code instead of the currency symbol
def maybe_adjust_currency_symbol(format, :iso) when is_binary(format) do
String.replace(format, @currency_placeholder, @iso_placeholder)
end
def maybe_adjust_currency_symbol(format, _currency_symbol) do
format
end
# ========= This is here for compatibility and needs review =========
def validate_other_format(other_type, backend, options) do
format_module = Module.concat(backend, Number.Format)
with {:ok, formats} <- format_module.formats_for(options.locale, options.number_system) do
if format = Map.get(formats.other, other_type) do
{:ok, format}
else
locale_name = options.locale.cldr_locale_name
{
:error,
{
Cldr.UnknownFormatError,
"The locale #{inspect(locale_name)} with number system " <>
"#{inspect(options[:number_system])} does not define a format " <>
"#{inspect(other_type)}."
}
}
end
end
end
#
end