defmodule Cldr.Number.Formatter.Decimal do
@moduledoc """
Formats a number according to a locale-specific predefined format or a user-defined format.
As a performance optimization, all decimal formats known at compile time are
compiled into function that roughly halves the time to format a number
compared to a non-precompiled format.
The available format styles for a locale can be returned by:
iex> {:ok, decimal_format_styles} = Cldr.Number.Format.decimal_format_styles_for("en", :latn, TestBackend.Cldr)
iex> Enum.sort(decimal_format_styles)
[
:accounting,
:accounting_alpha_next_to_number,
:accounting_no_symbol,
:currency,
:currency_alpha_next_to_number,
:currency_long,
:currency_no_symbol,
:percent,
:scientific,
:standard
]
This allows a number to be formatted in a locale-specific way but using
a standard method of describing the purpose of the format.
"""
import Cldr.Math, only: [power_of_10: 1]
import DigitalToken, only: [is_digital_token: 1]
alias Cldr.{Currency, Math, Digits}
alias Cldr.Number.Format
alias Cldr.Number.Format.Compiler
alias Cldr.Number.Format.Options
@empty_string ""
@max_token_fractional_digits 18
@doc """
Formats a number according to a decimal format string.
This is a lower level formatting function. It is strongly
advised to use `Cldr.Number.to_string/2` or even better the
`MyApp.Cldr.Number.to_string/2` function where `MyApp.Cldr`
is a Cldr backend module.
## Arguments
* `number` is an integer, float or Decimal or a string. A string
is used only when composing formats.
* `format` is a format string. See `Cldr.Number` for further
information.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module.
* `options` is a `t:Cldr.Number.Format.Options.t/0` of validated options.
See `Cldr.Number.to_string/2` for further information.
"""
@spec to_string(Math.number_or_decimal() | String.t(), String.t(), Cldr.backend(), Options.t()) ::
{:ok, String.t()} | {:error, {atom, String.t()}}
def to_string(number, format, backend, %Options{} = options) when is_binary(format) do
Module.concat(backend, Number.Formatter.Decimal).to_string(number, format, options)
end
@doc false
def update_meta(meta, number, backend, options) do
meta
|> adjust_fraction_for_currency(options.currency, options.currency_digits, backend)
|> adjust_fraction_for_significant_digits(number)
|> adjust_for_fractional_digits(options.fractional_digits)
|> adjust_for_integer_digits(options.maximum_integer_digits)
|> adjust_for_round_nearest(options.round_nearest)
|> Map.put(:number, number)
end
@doc false
# Formatting for NaN and Inf
def do_to_string(%Decimal{coef: :NaN}, meta, backend, options) do
options.symbols.nan
|> assemble_format(meta, backend, options)
end
def do_to_string(%Decimal{coef: :inf}, meta, backend, options) do
options.symbols.infinity
|> assemble_format(meta, backend, options)
end
# For when the number is actually a string. This allows formats to be
# composed.
def do_to_string(string, meta, backend, options) when is_binary(string) do
assemble_format(string, meta, backend, options)
end
# For most number formats. Note this pipleine is only used for
# formats that are compiled at runtime. For all known formats that
# are compiled at compile time, their pipeline is baked into the
# backend code (see define_to_string/1 in this module).
def do_to_string(number, %{integer_digits: _integer_digits} = meta, backend, options) do
number
|> absolute_value(meta, backend, options)
|> multiply_by_factor(meta, backend, options)
|> round_to_significant_digits(meta, backend, options)
|> round_to_nearest(meta, backend, options)
|> set_exponent(meta, backend, options)
|> round_fractional_digits(meta, backend, options)
|> output_to_tuple(meta, backend, options)
|> adjust_leading_zeros(meta, backend, options)
|> adjust_trailing_zeros(meta, backend, options)
|> set_max_integer_digits(meta, backend, options)
|> apply_grouping(meta, backend, options)
|> reassemble_number_string(meta, backend, options)
|> transliterate(meta, backend, options)
|> assemble_format(meta, backend, options)
end
# For when the format itself actually has only literal components
# and no number format.
def do_to_string(number, meta, backend, options) do
assemble_format(number, meta, backend, options)
end
# We work with the absolute value because the formatting of the sign
# is done by selecting the "negative format" rather than the "positive format"
@doc false
def absolute_value(%Decimal{} = number, _meta, _backend, _options) do
Decimal.abs(number)
end
def absolute_value(number, _meta, _backend, _options) do
abs(number)
end
# If the format includes a % (percent) or permille then we
# adjust the number by a factor. All other formats the factor
# is 1 and hence we avoid the multiplication.
@doc false
def multiply_by_factor(number, %{multiplier: 1}, _backend, _options) do
number
end
def multiply_by_factor(%Decimal{} = number, %{multiplier: factor}, _backend, _options)
when is_integer(factor) do
Decimal.mult(number, Decimal.new(factor))
end
def multiply_by_factor(number, %{multiplier: factor}, _backend, _options)
when is_number(number) and is_integer(factor) do
number * factor
end
# Round to significant digits. This is different to rounding
# to decimal places and is a more expensive mathematical
# calculation. Although the specification allows for minimum
# and maximum, I haven't found an example of where minimum is a
# useful rounding value since maximum already removes trailing
# insignificant zeros.
#
# Also note that this implementation allows for both significant
# digit rounding as well as decimal precision rounding. Its likely
# not a good idea to combine the two in a format mask and results
# are unspecified if you do.
@doc false
def round_to_significant_digits(
number,
%{significant_digits: %{min: 0, max: 0}},
_backend,
_options
) do
number
end
def round_to_significant_digits(
number,
%{significant_digits: %{min: _min, max: max}},
_backend,
_options
) do
Math.round_significant(number, max)
end
# Round to nearest rounds a number to the nearest increment specified. For example
# if `rounding: 5` then we round to the nearest multiple of 5. The appropriate rounding
# mode is used.
@doc false
def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{
rounding_mode: _rounding_mode
})
when rounding == 0 do
number
end
def round_to_nearest(%Decimal{} = number, %{round_nearest: rounding}, _backend, %{
rounding_mode: rounding_mode
}) do
rounding = Decimal.new(rounding)
number
|> Decimal.div(rounding)
|> Math.round(0, rounding_mode)
|> Decimal.mult(rounding)
end
def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{
rounding_mode: rounding_mode
})
when is_float(number) do
number
|> Kernel./(rounding)
|> Math.round(0, rounding_mode)
|> Kernel.*(rounding)
end
def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{
rounding_mode: rounding_mode
})
when is_integer(number) do
number
|> Kernel./(rounding)
|> Math.round(0, rounding_mode)
|> Kernel.*(rounding)
|> trunc
end
# For a scientific format we need to adjust to a
# coefficient * 10^exponent format.
@doc false
def set_exponent(number, %{exponent_digits: 0}, _backend, _options) do
{number, 0}
end
def set_exponent(number, meta, _backend, _options) do
{coef, exponent} = Math.coef_exponent(number)
coef = Math.round_significant(coef, meta.scientific_rounding)
{coef, exponent}
end
# Round to get the right number of fractional digits. This is
# applied after setting the exponent since we may have either
# the original number or its coef/exponentform.
@doc false
def round_fractional_digits({number, exponent}, _meta, _backend, _options)
when is_integer(number) do
{number, exponent}
end
# Don't round if we're in exponential mode. This is probably incorrect since
# we're not following the 'significant digits' processing rule for
# exponent numbers.
def round_fractional_digits(
{number, exponent},
%{exponent_digits: exponent_digits},
_backend,
_options
)
when exponent_digits > 0 do
{number, exponent}
end
def round_fractional_digits(
{number, exponent},
%{fractional_digits: %{max: max, min: _min}},
_backend,
%{rounding_mode: rounding_mode}
) do
number = Math.round(number, max, rounding_mode)
{number, exponent}
end
# Output the number to a tuple - all the other transformations
# are done on the tuple version split into its constituent
# parts.
@doc false
def output_to_tuple(number, _meta, _backend, _options) when is_integer(number) do
integer = :erlang.integer_to_list(number)
{1, integer, [], 1, [?0]}
end
def output_to_tuple({coef, exponent}, _meta, _backend, _options) do
{integer, fraction, sign} = Digits.to_tuple(coef)
exponent_sign = if exponent >= 0, do: 1, else: -1
integer = Enum.map(integer, &Kernel.+(&1, ?0))
fraction = Enum.map(fraction, &Kernel.+(&1, ?0))
exponent = if exponent == 0, do: [?0], else: Integer.to_charlist(abs(exponent))
{sign, integer, fraction, exponent_sign, exponent}
end
# Remove all the leading zeros from an integer and add back what
# is required for the format.
@doc false
def adjust_leading_zeros(
{sign, integer, fraction, exponent_sign, exponent},
%{integer_digits: integer_digits},
_backend,
_options
) do
integer =
if (count = integer_digits[:min] - length(integer)) > 0 do
:lists.duplicate(count, ?0) ++ integer
else
integer
end
{sign, integer, fraction, exponent_sign, exponent}
end
@doc false
def adjust_trailing_zeros(
{sign, integer, fraction, exponent_sign, exponent},
%{fractional_digits: fraction_digits},
_backend,
_options
) do
fraction = do_trailing_zeros(fraction, fraction_digits[:min] - length(fraction))
{sign, integer, fraction, exponent_sign, exponent}
end
defp do_trailing_zeros(fraction, count) when count <= 0 do
fraction
end
defp do_trailing_zeros(fraction, count) do
fraction ++ :lists.duplicate(count, ?0)
end
# Take the rightmost maximum digits only - this is a truncation from the
# right.
@doc false
def set_max_integer_digits(number, %{integer_digits: %{max: 0}}, _backend, _options) do
number
end
def set_max_integer_digits(
{sign, integer, fraction, exponent_sign, exponent},
%{integer_digits: %{max: max}},
_backend,
_options
) do
integer = do_max_integer_digits(integer, length(integer) - max)
{sign, integer, fraction, exponent_sign, exponent}
end
defp do_max_integer_digits(integer, over) when over <= 0 do
integer
end
defp do_max_integer_digits(integer, over) do
{_rest, integer} = Enum.split(integer, over)
integer
end
# Insert the grouping placeholder in the right place in the number.
# There may be one or two different groupings for the integer part
# and one grouping for the fraction part.
@doc false
def apply_grouping(
{sign, integer, [] = fraction, exponent_sign, exponent},
%{grouping: groups},
backend,
%{locale: locale, minimum_grouping_digits: minimum_grouping_digits}
) do
integer =
do_grouping(
integer,
groups[:integer],
length(integer),
minimum_group_size(groups[:integer], minimum_grouping_digits, locale, backend),
:reverse
)
{sign, integer, fraction, exponent_sign, exponent}
end
def apply_grouping(
{sign, integer, fraction, exponent_sign, exponent},
%{grouping: groups},
backend,
%{
locale: locale,
minimum_grouping_digits: minimum_grouping_digits
}
) do
integer =
do_grouping(
integer,
groups[:integer],
length(integer),
minimum_group_size(groups[:integer], minimum_grouping_digits, locale, backend),
:reverse
)
fraction =
do_grouping(
fraction,
groups[:fraction],
length(fraction),
minimum_group_size(groups[:fraction], minimum_grouping_digits, locale, backend),
:forward
)
{sign, integer, fraction, exponent_sign, exponent}
end
defp minimum_group_size(%{first: group_size}, 0, locale, backend) do
Format.minimum_grouping_digits_for!(locale, backend) + group_size
end
defp minimum_group_size(%{first: group_size}, minimum_grouping_digits, _locale, _backend) do
minimum_grouping_digits + group_size
end
# The actual grouping function. Note there are two directions,
# `:forward` and `:reverse`. That's because we group from the decimal
# placeholder outwards and there may be a final group that is less than
# the grouping size. For the fraction part the dangling part is at the
# end (:forward direction) whereas for the integer part the dangling
# group is at the beginning (:reverse direction)
@group_separator Compiler.placeholder(:group)
@doc false
# No grouping if the length (number of digits) is less than the
# minimum grouping size.
def do_grouping(number, _, length, min_grouping, :reverse) when length < min_grouping do
number
end
# No grouping when the length of the number is less than the group size
def do_grouping(number, %{first: first, rest: first}, length, _, _) when length <= first do
number
end
# The case when there is no grouping.
def do_grouping(number, %{first: 0, rest: 0}, _, _, _) do
number
end
# The common case of grouping in 3's
def do_grouping(number, %{first: 3, rest: 3} = grouping, length, min, :reverse) do
number
|> Enum.reverse()
|> do_grouping(grouping, length, min, :forward)
|> Enum.reverse()
end
def do_grouping([a, b, c | rest], %{first: 3, rest: 3} = grouping, _length, min, :forward) do
[a, b, c, @group_separator | do_grouping(rest, grouping, length(rest), min, :forward)]
end
# Only one group size
def do_grouping(number, %{first: first, rest: first}, length, _, :forward) do
split_point = div(length, first) * first
{rest, last_group} = Enum.split(number, split_point)
add_separator(rest, first, @group_separator)
|> add_last_group(last_group, @group_separator)
end
def do_grouping(number, %{first: first, rest: first}, length, _, :reverse) do
split_point = length - div(length, first) * first
{first_group, rest} = Enum.split(number, split_point)
add_separator(rest, first, @group_separator)
|> add_first_group(first_group, @group_separator)
end
# The case when there are two different groupings. This applies only to
# The integer part, it can never be true for the fraction part.
def do_grouping(number, %{first: first, rest: rest}, length, _min_grouping, :reverse) do
{others, first_group} = Enum.split(number, length - first)
do_grouping(others, %{first: rest, rest: rest}, length(others), 1, :reverse)
|> add_last_group(first_group, @group_separator)
end
@doc false
def add_separator([], _every, _separator) do
[]
end
def add_separator(group, every, separator) do
{_, [_ | rest]} =
Enum.reduce(group, {1, []}, fn elem, {counter, list} ->
list = [elem | list]
list = if rem(counter, every) == 0, do: [separator | list], else: list
{counter + 1, list}
end)
Enum.reverse(rest)
end
@doc false
def add_first_group(groups, [], _separator) do
groups
end
def add_first_group(groups, first, separator) do
[first, separator, groups]
end
@doc false
def add_last_group(groups, [], _separator) do
groups
end
def add_last_group(groups, last, separator) do
[groups, separator, last]
end
@decimal_separator Compiler.placeholder(:decimal)
@exponent_separator Compiler.placeholder(:exponent)
@exponent_sign Compiler.placeholder(:exponent_sign)
@minus_placeholder Compiler.placeholder(:minus)
@doc false
def reassemble_number_string(
{_sign, integer, fraction, exponent_sign, exponent},
meta,
_backend,
options
) do
decimal_separator = decimal_separator(options, @decimal_separator)
integer = if integer == [], do: [~c"0"], else: integer
fraction = if fraction == [], do: fraction, else: [decimal_separator, fraction]
exponent_sign =
cond do
exponent_sign < 0 -> @minus_placeholder
meta.exponent_sign -> @exponent_sign
true -> ~c""
end
exponent =
if meta.exponent_digits > 0 do
digits =
exponent
|> List.to_string()
|> String.pad_leading(meta.exponent_digits, "0")
[@exponent_separator, exponent_sign, digits]
else
[]
end
[integer, fraction, exponent]
|> :erlang.iolist_to_binary()
end
# Now we can assemble the final format. Based upon
# whether the number is positive or negative (as indicated
# by options[:sign]) we assemble the parts and transliterate
# the currency sign, percent and permille characters.
@doc false
def assemble_format(number_string, meta, backend, options) do
format = meta.format[options.pattern]
number = meta.number
formatted =
assemble_parts(format, number_string, number, backend, meta, options)
|> :erlang.iolist_to_binary()
|> String.trim_trailing()
formatted
end
defp assemble_parts(
[{:format, _}, {:currency, _type} | rest],
number_string,
number,
backend,
meta,
%{currency_spacing: spacing} = options
)
when not is_nil(spacing) do
%{currency_symbol: symbol, wrapper: wrapper} = options
before_spacing = spacing[:before_currency]
before_currency_match? = before_currency_match?(number_string, symbol, before_spacing)
symbol = maybe_wrap(symbol, :currency_symbol, wrapper)
number_string = maybe_wrap(number_string, :number, wrapper)
if before_currency_match? do
[
number_string,
maybe_wrap(before_spacing[:insert_between], :currency_space, wrapper),
symbol
| assemble_parts(rest, number_string, number, backend, meta, options)
]
else
[
number_string,
symbol
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
end
defp assemble_parts(
[{:currency, _type}, {:format, _} | rest],
number_string,
number,
backend,
meta,
%{currency_spacing: spacing} = options
)
when not is_nil(spacing) do
%{currency_symbol: symbol, wrapper: wrapper} = options
after_spacing = spacing[:after_currency]
after_currency_match? = after_currency_match?(number_string, symbol, after_spacing)
symbol = maybe_wrap(symbol, :currency_symbol, wrapper)
number_string = maybe_wrap(number_string, :number, wrapper)
if after_currency_match? do
[
symbol,
maybe_wrap(after_spacing[:insert_between], :currency_space, wrapper),
number_string
| assemble_parts(rest, number_string, number, backend, meta, options)
]
else
[
symbol,
number_string
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
end
defp assemble_parts([], _number_string, _number, _backend, _meta, _options) do
[]
end
@nbsp "\u200b"
defp assemble_parts([{:currency, _type} | rest], number_string, number, backend, meta, options) do
%{currency_symbol: symbol, wrapper: wrapper} = options
if symbol == @nbsp do
assemble_parts(rest, number_string, number, backend, meta, options)
else
symbol = maybe_wrap(symbol, :currency_symbol, wrapper)
[symbol | assemble_parts(rest, number_string, number, backend, meta, options)]
end
end
defp assemble_parts(
[{:format, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap(number_string, :number, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts([{:pad, _} | rest], number_string, number, backend, meta, options) do
[
padding_string(meta, number_string)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:plus, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap(options.symbols.plus_sign, :plus, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:minus, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
sign =
if(number_string == "0", do: "", else: options.symbols.minus_sign)
|> maybe_wrap(:minus, wrapper)
[sign | assemble_parts(rest, number_string, number, backend, meta, options)]
end
defp assemble_parts(
[{:percent, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap(options.symbols.percent_sign, :percent, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:permille, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap(options.symbols.per_mille, :permille, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:literal, literal} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap(literal, :literal, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:quote, _} | rest],
number_string,
number,
backend,
meta,
%{wrapper: wrapper} = options
) do
[
maybe_wrap("'", :quote, wrapper)
| assemble_parts(rest, number_string, number, backend, meta, options)
]
end
defp assemble_parts(
[{:quoted_char, char} | rest],
number_string,
number,
backend,
meta,
options
) do
[char | assemble_parts(rest, number_string, number, backend, meta, options)]
end
# Invokes a wrapping function. It can return a Phoenix :safe
# string or a string.
defp maybe_wrap(string, _tag, nil), do: string
defp maybe_wrap(string, tag, wrapper) do
case wrapper.(string, tag) do
{:safe, iodata} -> iodata
iodata when is_list(iodata) -> iodata
string when is_binary(string) -> string
end
end
# Calculate the padding by subtracting the length of the number
# string from the padding length.
@doc false
def padding_string(%{padding_length: 0}, _number_string) do
@empty_string
end
# We can't make the assumption that the padding character is
# an ascii character - it could be any grapheme so we can't use
# binary pattern matching.
def padding_string(meta, number_string) do
pad_length = meta.padding_length - String.length(number_string)
if pad_length > 0 do
String.duplicate(meta.padding_char, pad_length)
else
@empty_string
end
end
@doc false
def transliterate(number_string, _meta, backend, options) do
%{locale: locale, number_system: number_system} = options
Cldr.Number.Transliterate.transliterate(number_string, locale, number_system, backend)
end
# When formatting a currency we need to adjust the number of fractional
# digits to match the currency definition. We also need to adjust the
# rounding increment to match the currency definition. Note that here
# we are just adjusting the meta data, not the number itself
@doc false
def adjust_fraction_for_currency(meta, nil, _currency_digits, _backend) do
meta
end
def adjust_fraction_for_currency(meta, currency, _currency_digits, _backend)
when is_digital_token(currency) do
%{meta | fractional_digits: %{max: @max_token_fractional_digits, min: 0}}
end
def adjust_fraction_for_currency(meta, currency, :accounting, backend) do
{:ok, currency} = Currency.currency_for_code(currency, backend)
do_adjust_fraction(meta, currency.digits, currency.rounding)
end
def adjust_fraction_for_currency(meta, currency, :cash, backend) do
{:ok, currency} = Currency.currency_for_code(currency, backend)
do_adjust_fraction(meta, currency.cash_digits, currency.cash_rounding)
end
def adjust_fraction_for_currency(meta, currency, :iso, backend) do
{:ok, currency} = Currency.currency_for_code(currency, backend)
do_adjust_fraction(meta, currency.iso_digits, currency.iso_digits)
end
defp do_adjust_fraction(meta, digits, rounding) do
rounding = power_of_10(-digits) * rounding
%{meta | round_nearest: rounding}
end
# Functions to update metadata to reflect the
# options passed at runtime.
# If we round to sigificant digits then the format won't (usually)
# have any fractional part specified and if we don't do something
# then we're truncating the number - not really what is intended
# for significant digits display.
@doc false
# For when there is no number format
def adjust_fraction_for_significant_digits(%{significant_digits: nil} = meta, _number) do
meta
end
# For no significant digits
def adjust_fraction_for_significant_digits(
%{significant_digits: %{max: 0, min: 0}} = meta,
_number
) do
meta
end
# No fractional digits for an integer
def adjust_fraction_for_significant_digits(%{significant_digits: _} = meta, number)
when is_integer(number) do
meta
end
# Decimal version of an integer => exponent > 0
def adjust_fraction_for_significant_digits(%{significant_digits: _} = meta, %Decimal{exp: exp})
when exp >= 0 do
meta
end
# For all float or Decimal fraction
def adjust_fraction_for_significant_digits(%{significant_digits: _} = meta, _number) do
%{meta | fractional_digits: %{max: 10, min: 1}}
end
# To allow overriding fractional digits
# This causes rounding of the number
@doc false
def adjust_for_fractional_digits(meta, nil) do
meta
end
def adjust_for_fractional_digits(meta, digits) do
%{meta | fractional_digits: %{max: digits, min: digits}}
end
# To allow overriding fractional digits
# This causes rounding of the number
@doc false
def adjust_for_integer_digits(meta, nil) do
meta
end
def adjust_for_integer_digits(meta, digits) do
integer_digits =
meta
|> Map.fetch!(:integer_digits)
|> Map.put(:max, digits)
%{meta | integer_digits: integer_digits}
end
# To allow overriding round nearest
# which impacts the precision of the number
# and is commonly required for currency
# formatting
@doc false
def adjust_for_round_nearest(meta, nil) do
meta
end
def adjust_for_round_nearest(meta, digits) do
%{meta | round_nearest: digits}
end
@doc false
def define_to_string(backend) do
config = Module.get_attribute(backend, :config)
compiled_artifacts =
for format <- Cldr.Config.decimal_format_list(config) do
case Compiler.compile(format) do
{:ok, meta, formatting_pipeline} ->
{format, meta, formatting_pipeline}
{:error, message} ->
raise Cldr.FormatCompileError, "#{message} compiling #{inspect(format)}"
end
end
metadata =
for {format, meta, _formatting_pipeline} <- compiled_artifacts do
{format, meta}
end
|> Map.new()
to_string_function =
for {format, _meta, formatting_pipeline} <- compiled_artifacts do
quote do
def to_string(number_or_string, unquote(format) = format, options) do
case number_or_string do
string when is_binary(string) ->
Decimal.do_to_string(string, metadata!(format), unquote(backend), options)
%Elixir.Decimal{coef: coef} = number when coef in [:NaN, :inf] ->
Decimal.do_to_string(number, metadata!(format), unquote(backend), options)
number ->
meta =
format
|> metadata!()
|> Decimal.update_meta(number, unquote(backend), options)
backend = unquote(backend)
unquote(formatting_pipeline)
end
end
end
end
metadata_function =
quote do
@doc false
def metadata(format) do
case Map.fetch(unquote(Macro.escape(metadata)), format) do
{:ok, meta} -> {:ok, meta}
:error -> Compiler.format_to_metadata(format)
end
end
def metadata!(format) do
case metadata(format) do
{:ok, meta} -> meta
{:error, reason} -> raise Cldr.FormatCompileError, reason
end
end
end
quote do
unquote(to_string_function)
unquote(metadata_function)
end
end
defp before_currency_match?(number_string, symbol, spacing) do
String.match?(number_string, Regex.compile!(spacing[:surrounding_match] <> "$", "u")) &&
String.match?(symbol, Regex.compile!("^" <> spacing[:currency_match], "u"))
end
# The unicode set "[[:^S:]&[:^Z:]]" isn't a valid Regex for Elixir/Erlang
# The following is a subsctitution
@currency_match_symbol ~r/[\P{S}]$/u
@currency_match_separator ~r/[\P{Z}]$/u
defp after_currency_match?(
number_string,
symbol,
%{currency_match: "[[:^S:]&[:^Z:]]"} = spacing
) do
# IO.inspect number_string, label: "Number string"
# IO.inspect symbol, label: "Symbol"
# IO.inspect String.match?(number_string, Regex.compile!("^" <> spacing[:surrounding_match], "u")), label: "Surrounding match"
# IO.inspect String.match?(symbol, @currency_match), label: "Currency match"
String.match?(number_string, Regex.compile!("^" <> spacing[:surrounding_match], "u")) &&
String.match?(symbol, @currency_match_symbol) &&
String.match?(symbol, @currency_match_separator)
end
defp after_currency_match?(number_string, symbol, spacing) do
String.match?(number_string, Regex.compile!("^" <> spacing[:surrounding_match], "u")) &&
String.match?(symbol, Regex.compile!(spacing[:currency_match] <> "$", "u"))
end
defp decimal_separator(%{currency: %{decimal_separator: nil}}, default_decimal_separator) do
default_decimal_separator
end
defp decimal_separator(%{currency: %{decimal_separator: separator}}, _default_decimal_separator) do
separator
end
defp decimal_separator(_options, default_decimal_separator) do
default_decimal_separator
end
end