defmodule Cldr.Message.Interpreter do
@doc """
Formats a parsed ICU message into an iolist.
"""
alias Cldr.Message
@spec format_list(list(), Message.arguments(), Message.options()) ::
{:ok, list(), list(), list()} | {:error, list(), list(), list()}
def format_list(message, args \\ [], options \\ []) do
case format_list(message, args, options, [], []) do
{iolist, bound, [] = unbound} -> {:ok, iolist, bound, unbound}
{iolist, bound, unbound} -> {:error, iolist, bound, unbound}
end
end
@spec format_list!(list(), Message.arguments(), Message.options()) ::
list() | no_return
def format_list!(message, args \\ [], options \\ []) do
case format_list(message, args, options, [], []) do
{iolist, _bound, []} ->
iolist
{_iolist, _bound, unbound} ->
raise Cldr.Message.BindError, "No binding was found for #{inspect(unbound)}"
end
end
defp format_list([], _args, _options, bound, unbound) do
{[], bound, unbound}
end
defp format_list([head], args, options, bound, unbound) do
case format_list(head, args, options, bound, unbound) do
{result, bound, unbound} when is_tuple(result) ->
{[result], bound, unbound}
{result, bound, unbound} ->
{[format_to_string(result, options)], bound, unbound}
end
end
defp format_list([head | rest], args, options, bound, unbound) do
{result_hd, bound_hd, unbound_hd} = format_list(head, args, options, bound, unbound)
{result_tl, bound_tl, unbound_tl} = format_list(rest, args, options, bound_hd, unbound_hd)
{[format_to_string(result_hd, options) | result_tl], bound_tl, unbound_tl}
end
defp format_list({:literal, literal}, _args, _options, bound, unbound)
when is_binary(literal) do
{literal, bound, unbound}
end
defp format_list({:pos_arg, arg}, args, _options, bound, unbound) when is_map(args) do
with {:ok, atom} <- atomize(arg),
{:ok, value} <- Map.fetch(args, atom) do
{[value], [arg | bound], unbound}
else
_any ->
{[{:pos_arg, arg}], bound, [arg | unbound]}
end
end
defp format_list({:pos_arg, arg}, args, _options, bound, unbound) when is_list(args) do
with {:ok, value} <- fetch_pos_arg(args, arg) do
{[value], [arg | bound], unbound}
else
_any ->
{[{:pos_arg, arg}], bound, [arg | unbound]}
end
end
defp format_list({:named_arg, arg}, args, _options, bound, unbound) when is_map(args) do
with {:ok, atom} <- atomize(arg),
{:ok, value} <- Map.fetch(args, atom) do
{[value], [arg | bound], unbound}
else
_any ->
{[{:named_arg, arg}], bound, [arg | unbound]}
end
end
defp format_list({:named_arg, arg}, args, _options, bound, unbound) when is_list(args) do
with {:ok, atom} <- atomize(arg),
{:ok, value} <- Keyword.fetch(args, atom) do
{[value], [arg | bound], unbound}
else
_any ->
{[{:named_arg, arg}], bound, [arg | unbound]}
end
end
defp format_list(:value, _args, options, bound, unbound) do
with {:ok, value} <- Keyword.fetch(options, :arg) do
{[value], [:arg | bound], unbound}
else
_any ->
{[{:value, :arg}], bound, [:arg | unbound]}
end
end
defp format_list({:simple_format, arg, type}, args, options, bound, unbound) do
case format_list(arg, args, options, bound, unbound) do
{[arg], _bound, [] = _unbound} ->
format_list({arg, type}, args, options, bound, unbound)
{[arg], bound, unbound} ->
{{:simple_format, arg, type}, bound, unbound}
end
end
defp format_list({:simple_format, arg, type, style}, args, options, bound, unbound) do
case format_list(arg, args, options, bound, unbound) do
{[arg], _bound, [] = _unbound} ->
format_list({arg, type, style}, args, options, bound, unbound)
{[arg], bound, unbound} ->
{{:simple_format, arg, type, style}, bound, unbound}
end
end
# Numbers where the number is a tuple with formatting
# options
defp format_list({{number, format_options}, :number}, args, options, bound, unbound) do
options = Keyword.merge(options, format_options)
format_list({number, :number}, args, options, bound, unbound)
end
defp format_list({{number, format_options}, :number, format}, args, options, bound, unbound) do
options = Keyword.merge(options, format_options)
format_list({number, :number, format}, args, options, bound, unbound)
end
# Formatting numbers
defp format_list({number, :number}, _args, options, bound, unbound) do
{Cldr.Number.to_string!(number, options), bound, unbound}
end
@integer_format "#"
defp format_list({number, :number, :integer}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, @integer_format)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, :short}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :short)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, :currency}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :currency)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, :percent}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :percent)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, :permille}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :permille)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, format}, _args, options, bound, unbound)
when is_binary(format) do
options = Keyword.put(options, :format, format)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :number, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options =
configured_message_format(format, options[:backend])
options =
options
|> Keyword.merge(format_options)
|> Keyword.put_new(:format, format)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :spellout}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :spellout)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :spellout, :verbose}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :spellout_verbose)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :spellout, :year}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :spellout_year)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
defp format_list({number, :spellout, :ordinal}, _args, options, bound, unbound) do
options = Keyword.put(options, :format, :spellout_ordinal)
{Cldr.Number.to_string!(number, options), bound, unbound}
end
if Code.ensure_loaded?(Cldr.Date) do
defp format_list({date, :date}, _args, options, bound, unbound) do
{Cldr.Date.to_string!(date, options), bound, unbound}
end
defp format_list({date, :date, format}, _args, options, bound, unbound)
when is_binary(format) do
options = Keyword.put(options, :format, format)
{Cldr.Date.to_string!(date, options), bound, unbound}
end
defp format_list({date, :date, format}, _args, options, bound, unbound)
when format in [:short, :medium, :long, :full] do
options = Keyword.put(options, :format, format)
{Cldr.Date.to_string!(date, options), bound, unbound}
end
defp format_list({number, :date, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Cldr.Date.to_string!(number, options), bound, unbound}
end
defp format_list({time, :time}, _args, options, bound, unbound) do
{Cldr.Time.to_string!(time, options), bound, unbound}
end
defp format_list({time, :time, format}, _args, options, bound, unbound)
when is_binary(format) do
options = Keyword.put(options, :format, format)
{Cldr.Time.to_string!(time, options), bound, unbound}
end
defp format_list({time, :time, format}, _args, options, bound, unbound)
when format in [:short, :medium, :long, :full] do
options = Keyword.put(options, :format, format)
{Cldr.Time.to_string!(time, options), bound, unbound}
end
defp format_list({number, :time, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Cldr.Time.to_string!(number, options), bound, unbound}
end
defp format_list({datetime, :datetime}, _args, options, bound, unbound) do
{Cldr.DateTime.to_string!(datetime, options), bound, unbound}
end
defp format_list({datetime, :datetime, format}, _args, options, bound, unbound)
when is_binary(format) do
options = Keyword.put(options, :format, format)
{Cldr.DateTime.to_string!(datetime, options), bound, unbound}
end
defp format_list({datetime, :datetime, format}, _args, options, bound, unbound)
when format in [:short, :medium, :long, :full] do
options = Keyword.put(options, :format, format)
{Cldr.DateTime.to_string!(datetime, options), bound, unbound}
end
defp format_list({number, :datetime, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Cldr.DateTime.to_string!(number, options), bound, unbound}
end
else
defp format_list({_number, type, _format}, _args, _options, _bound, _unbound)
when type in [:date, :time, :datetime] do
raise Cldr.Message.ParseError, datetime_configuration_error()
end
end
if Code.ensure_loaded?(Money) do
defp format_list({money, :money, format}, _args, options, bound, unbound)
when format in [:short, :long] do
options = Keyword.put(options, :format, format)
{Money.to_string!(money, options), bound, unbound}
end
defp format_list({number, :money, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Money.to_string!(number, options), bound, unbound}
end
else
defp format_list({_number, :money, _format}, _args, _options, _bound, _unbound) do
raise Cldr.Message.ParseError, money_configuration_error()
end
end
if Code.ensure_loaded?(Cldr.Unit) do
defp format_list({unit, :unit}, _args, options, bound, unbound) do
{Cldr.Unit.to_string!(unit, options), bound, unbound}
end
defp format_list({unit, :unit, format}, _args, options, bound, unbound)
when format in [:long, :short, :narrow] do
options = Keyword.put(options, :format, format)
{Cldr.Unit.to_string!(unit, options), bound, unbound}
end
defp format_list({number, :unit, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Cldr.Unit.to_string!(number, options), bound, unbound}
end
else
defp format_list({_unit, :unit, _format}, _args, _options, _bound, _unbound) do
raise Cldr.Message.ParseError, unit_configuration_error()
end
end
if Code.ensure_loaded?(Cldr.List) do
# The apply/3 call here is because dialyzer inexplicably complains
# if its a remote call
defp format_list({list, :list}, _args, options, bound, unbound) do
{Cldr.List.to_string!(list, options), bound, unbound}
end
defp format_list({list, :list, :and}, _args, options, bound, unbound) do
{Cldr.List.to_string!(list, options), bound, unbound}
end
list_format_function =
if function_exported?(Cldr.List, :known_list_formats, 0) do
:known_list_formats
else
:known_list_styles
end
@list_formats apply(Cldr.List, list_format_function, [])
defp format_list({list, :list, format}, _args, options, bound, unbound)
when format in @list_formats do
options = Keyword.put(options, :format, format)
{Cldr.List.to_string!(list, options), bound, unbound}
end
defp format_list({number, :list, format}, _args, options, bound, unbound)
when is_atom(format) do
format_options = configured_message_format(format, options[:backend])
options = Keyword.merge(options, format_options)
{Cldr.List.to_string!(number, options), bound, unbound}
end
else
defp format_list({_list, :list, _format}, _args, _options, _bound, _unbound) do
raise Cldr.Message.ParseError, list_configuration_error()
end
end
defp format_list({:select, arg, selections}, args, options, bound, unbound)
when is_map(selections) do
arg =
arg
|> format_list(args, options)
|> to_maybe_integer
message = Map.get(selections, arg) || other(selections, arg)
format_list(message, args, options, bound, unbound)
end
defp format_list({:plural, arg, plural_args, plurals}, args, options, bound, unbound)
when is_map(plurals) do
offset = Keyword.get(plural_args, :offset, 0)
plural_type = Keyword.get(plural_args, :plural_type, "Cardinal")
format_plural(arg, plural_type, offset, plurals, args, options, bound, unbound)
end
defp format_list({:select_ordinal, arg, plural_args, plurals}, args, options, bound, unbound) do
offset = Keyword.get(plural_args, :offset, 0)
plural_type = Keyword.get(plural_args, :plural_type, "Ordinal")
format_plural(arg, plural_type, offset, plurals, args, options, bound, unbound)
end
defp format_to_string([value], options) do
format_to_string(value, options)
end
defp format_to_string(value, options) when is_number(value) do
Cldr.Number.to_string!(value, options)
end
defp format_to_string(%Decimal{} = value, options) do
Cldr.Number.to_string!(value, options)
end
if Code.ensure_loaded?(Cldr.DateTime) do
defp format_to_string(%Date{} = value, options) do
Cldr.Date.to_string!(value, options)
end
defp format_to_string(%Time{} = value, options) do
Cldr.Time.to_string!(value, options)
end
defp format_to_string(%DateTime{} = value, options) do
Cldr.DateTime.to_string!(value, options)
end
end
defp format_to_string(value, _options) when is_tuple(value) do
value
end
defp format_to_string(value, _options) do
Kernel.to_string(value)
end
defp format_plural(arg, type, offset, plurals, args, options, bound, unbound) do
arg =
arg
|> format_list(args, options)
|> to_maybe_integer
formatted_arg = Cldr.Number.to_string!(arg - offset, options)
options =
options
|> Keyword.put(:type, type)
|> Keyword.put(:arg, formatted_arg)
plural_type = Cldr.Number.PluralRule.plural_type(arg - offset, options)
message = Map.get(plurals, arg) || Map.get(plurals, plural_type) || other(plurals, arg)
format_list(message, args, options, bound, unbound)
end
@spec other(map(), any()) :: any()
defp other(map, _arg) when is_map(map) do
Map.fetch!(map, "other")
end
@spec to_maybe_integer({:ok, [number | Decimal.t() | String.t()], [any()], []}) ::
number() | String.t() | atom()
defp to_maybe_integer({:ok, [arg], _bound, _unbound}) when is_integer(arg) do
arg
end
defp to_maybe_integer({:ok, [arg], _bound, _unbound}) when is_float(arg) do
trunc(arg)
end
defp to_maybe_integer({:ok, [%Decimal{} = arg], _bound, _unbound}) do
Decimal.to_integer(arg)
end
defp to_maybe_integer({:ok, [other], _bound, _unbound}) do
other
end
defp atomize(string) when is_binary(string) do
{:ok, String.to_existing_atom(string)}
rescue
ArgumentError ->
{:error, string}
end
defp atomize(integer) when is_integer(integer) do
{:ok, integer}
end
defp fetch_pos_arg(args, arg) when arg < length(args) do
{:ok, Enum.at(args, arg)}
end
defp fetch_pos_arg(_args, arg) do
{:error, arg}
end
@doc false
def unit_configuration_error do
"""
A `unit` formatting style cannot be applied unless the hex package `ex_cldr_units`
is configured in `mix.exs`.
"""
end
@doc false
def datetime_configuration_error do
"""
A `date`, `time` or `datetime` formatting style cannot be applied unless the hex package
`ex_cldr_dates_times` is configured in `mix.exs`.
"""
end
@doc false
def money_configuration_error do
"""
A `money` formatting style cannot be applied unless the hex package `ex_money`
is configured in `mix.exs`.
"""
end
@doc false
def list_configuration_error do
"""
A `list` formatting style cannot be applied unless the hex package `ex_cldr_lists`
is configured in `mix.exs`.
"""
end
@doc false
def configured_message_format(format, backend) do
Module.concat(backend, :Message).configured_message_format(format)
end
end