Packages
ex_cldr_numbers
2.19.0
2.38.3
2.38.2
2.38.1
2.38.0
2.37.0
2.36.0
2.35.2
2.35.1
2.35.0
2.34.1
2.34.0
2.33.6
2.33.5
2.33.4
2.33.3
2.33.2
2.33.1
2.33.0
2.32.4
2.32.3
2.32.2
2.32.1
2.32.0
retired
2.31.3
retired
2.31.2
2.31.1
2.31.0
2.30.1
2.30.0
2.29.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.0
2.25.2
2.25.1
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.23.0-rc.4
2.23.0-rc.3
2.23.0-rc.2
2.23.0-rc.1
2.23.0-rc.0
2.22.1
2.22.0
2.21.0
2.20.0
2.19.0
2.18.4
2.18.3
2.18.2
2.18.1
2.18.0
2.17.0
2.17.0-rc.1
2.17.0-rc.0
2.16.1
2.16.0
2.16.0-rc.0
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.0
2.13.2
2.13.1
2.13.0
2.13.0-rc.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.0
2.7.2
2.7.1
2.7.0
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.0
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
2.0.0-rc.0
1.6.0
1.5.2
1.5.1
1.5.0
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.3.0-rc.1
1.3.0-rc.0
retired
1.2.0
1.1.0
1.0.1
1.0.0
1.0.0-rc.0
retired
0.3.3
0.3.2
retired
0.3.1
retired
0.3.0
retired
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Number and currency localization and formatting functions for the Common Locale Data Repository (CLDR).
Current section
Files
Jump to
Current section
Files
lib/cldr/number/format/options.ex
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,
: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_short
]
@currency_symbol [
:standard,
:iso
]
@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 | :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}
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
def validate_option(:format, options, _backend, format)
when is_atom(format) and format not in [:accounting, :currency_short, :currency_long] 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, other) do
{:error,
{ArgumentError,
":currency_symbol must be :standard, :iso 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