defmodule Cldr.Rbnf.Processor do
@moduledoc """
Macro to define the interpreter for the compiled RBNF rules specific to a rule group (Ordinal,
Spellout, NumberingSystem)
"""
defmacro __using__(opts) do
backend = opts[:backend]
ordinal_module = Module.concat(backend, Number.Ordinal)
cardinal_module = Module.concat(backend, Number.Cardinal)
spellout_module = Module.concat(backend, Rbnf.Spellout)
quote location: :keep do
alias Cldr.Number
alias Cldr.Digits
import Cldr.Rbnf.Processor
Module.put_attribute(__MODULE__, :backend, unquote(backend))
defp do_rule(number, locale, function, rule, parsed) do
results =
Enum.map(parsed, fn {operation, argument} ->
do_operation(operation, number, locale, function, rule, argument)
end)
if Enum.any?(results, fn
{:error, _} -> true
_ -> false
end) do
{:error, collect_errors(results)}
else
:erlang.iolist_to_binary(results)
end
end
defp collect_errors(results) do
results
|> Enum.map(fn
{_, v} -> v
other -> other
end)
|> Enum.join(", ")
end
defp do_operation(:literal, _number, _locale, _function, _rule, string) do
string
end
defp do_operation(:modulo, number, locale, function, rule, nil)
when is_number(number) and number < 0 do
apply(__MODULE__, function, [abs(number), locale])
end
defp do_operation(:modulo, number, locale, function, rule, {:format, format})
when is_number(number) and number < 0 do
Cldr.Number.to_string!(abs(number), unquote(backend), locale: locale, format: format)
end
defp do_operation(:modulo, number, locale, function, rule, nil)
when is_integer(number) do
mod = number - div(number, rule.divisor) * rule.divisor
apply(__MODULE__, function, [mod, locale])
end
# For Fractional rules we format the integral part
defp do_operation(:modulo, number, locale, function, _rule, nil)
when is_float(number) do
format_fraction(number, locale)
end
defp do_operation(:modulo, number, locale, _function, rule, {:rule, rule_name}) do
mod = number - div(number, rule.divisor) * rule.divisor
apply(__MODULE__, rule_name, [mod, locale])
end
defp do_operation(:modulo, number, locale, function, rule, {:format, format}) do
mod = number - div(number, rule.divisor) * rule.divisor
Cldr.Number.to_string!(mod, unquote(backend), locale: locale, format: format)
end
# For Fractional rules we format the fraction as individual digits.
defp do_operation(:quotient, number, locale, function, rule, nil)
when is_float(number) do
apply(__MODULE__, function, [trunc(number), locale])
end
defp do_operation(:quotient, number, locale, function, rule, nil) do
divisor = div(number, rule.divisor)
apply(__MODULE__, function, [divisor, locale])
end
defp do_operation(:quotient, number, locale, _function, rule, {:rule, rule_name}) do
divisor = div(number, rule.divisor)
apply(__MODULE__, rule_name, [divisor, locale])
end
defp do_operation(:call, number, locale, _function, _rule, {:format, format}) do
Cldr.Number.to_string!(number, unquote(backend), locale: locale, format: format)
end
defp do_operation(:call, number, locale, _function, _rule, {:rule, rule_name}) do
apply(__MODULE__, rule_name, [number, locale])
end
defp do_operation(:ordinal, number, locale, _function, _rule, plurals) do
plural = unquote(ordinal_module).plural_rule(number, locale)
Map.get(plurals, plural) || Map.get(plurals, :other)
end
defp do_operation(:cardinal, number, locale, _function, _rule, plurals) do
plural = unquote(cardinal_module).plural_rule(number, locale)
Map.get(plurals, plural) || Map.get(plurals, :other)
end
defp do_operation(:conditional, number, locale, function, rule, argument) do
mod = number - div(number, rule.divisor) * rule.divisor
if mod > 0 do
do_rule(mod, locale, function, rule, argument)
else
""
end
end
defp format_fraction(number, locale) do
fraction =
number
|> Digits.fraction_as_integer()
|> Integer.to_string()
|> String.split("", trim: true)
|> Enum.map(&String.to_integer/1)
|> Enum.map(&unquote(spellout_module).spellout_cardinal(&1, locale))
|> Enum.join(" ")
end
@before_compile Cldr.Rbnf.Processor
end
end
@public_rulesets :public_rulesets
def define_rules(rule_group_name, backend, env) do
Module.register_attribute(env.module, @public_rulesets, [])
enumerate_rules(rule_group_name, backend, fn
rule_group, locale, "public", :error ->
define_rule(:error, nil, rule_group, locale, nil)
|> Code.eval_quoted([], env)
_rule_group, _locale, "private", :error ->
nil
rule_group, locale, "public", :redirect ->
define_rule(:redirect, backend, rule_group, locale, nil)
|> Code.eval_quoted([], env)
_rule_group, _locale, "private", :redirect ->
nil
rule_group, locale, access, rule ->
function_name = Cldr.Rbnf.force_valid_function_name(rule_group)
{:ok, parsed} = Cldr.Rbnf.Rule.parse(rule.definition)
function_body = rule_body(locale, rule_group, rule, parsed, backend)
rule.base_value
|> define_rule(rule.range, function_name, locale, function_body)
|> add_function_to_exports(access, env.module, locale)
|> Code.eval_quoted([], env)
end)
end
defp enumerate_rules(rule_group_type, backend, fun) do
all_rules = Cldr.Rbnf.for_all_locales(backend)[rule_group_type]
unless is_nil(all_rules) do
for {locale_name, _rule_group} <- all_rules do
for {rule_group, %{access: access, rules: rules}} <- all_rules[locale_name] do
for rule <- rules do
fun.(rule_group, locale_name, access, rule)
end
fun.(rule_group, locale_name, access, :redirect)
fun.(rule_group, locale_name, access, :error)
end
end
end
end
# If we are provided with a Decimal integer then we can call the
# equivalent integer function without loss of precision
defp define_rule(:error, _range, rule_group, locale_name, _body) do
quote location: :keep do
def unquote(rule_group)(
%Decimal{exp: 0, coef: number},
%Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)} = locale
) do
unquote(rule_group)(number, locale)
end
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)}) do
{:error, rbnf_rule_error(number, unquote(rule_group), unquote(locale_name))}
end
end
end
defp define_rule(:redirect, backend, rule_group, locale_name, _body) do
quote location: :keep do
def unquote(rule_group)(number, unquote(locale_name)) do
with {:ok, locale} <- Module.concat(unquote(backend), Locale).new(unquote(locale_name)) do
unquote(rule_group)(number, locale)
end
end
end
end
defp define_rule("-x", _range, rule_group, locale_name, body) do
quote location: :keep do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)})
when Kernel.and(is_number(number), number < 0),
do: unquote(body)
end
end
# Improper fraction rule
defp define_rule("x.x", _range, rule_group, locale_name, body) do
quote location: :keep do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)})
when is_float(number),
do: unquote(body)
end
end
defp define_rule("x,x", range, rule_group, locale, body) do
define_rule("x.x", range, rule_group, locale, body)
end
defp define_rule(0, "undefined", rule_group, locale_name, body) do
quote location: :keep do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)})
when is_integer(number),
do: unquote(body)
end
end
defp define_rule(base_value, "undefined", rule_group, locale_name, body)
when is_integer(base_value) do
quote location: :keep do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)})
when Kernel.and(is_integer(number), number >= unquote(base_value)),
do: unquote(body)
end
end
defp define_rule(base_value, range, rule_group, locale_name, body)
when is_integer(range) and is_integer(base_value) do
quote location: :keep do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale_name)})
when Kernel.and(
is_integer(number),
Kernel.and(number >= unquote(base_value), number < unquote(range))
),
do: unquote(body)
end
end
defp define_rule("Inf", _range, _rule_group, _locale_name, _body) do
{:error, "Infinite rule sets are not implemented"}
end
defp define_rule("NaN", _range, _rule_group, _locale_name, _body) do
{:error, "NaN rule sets are not implemented"}
end
defp define_rule("0.x", _range, _rule_group, _locale_name, _body) do
{:error, "Proper Fraction rule sets are not implemented"}
end
defp define_rule("x.0", _range, _rule_group, _locale_name, _body) do
{:error, "Master rule sets are not implemented"}
end
# Get the AST of the rule body
defp rule_body(locale_name, rule_group, rule, parsed, _backend) do
locale =
Cldr.Config.all_language_tags()
|> Map.get(locale_name)
quote location: :keep do
do_rule(
number,
unquote(Macro.escape(locale)),
unquote(rule_group),
unquote(Macro.escape(rule)),
unquote(Macro.escape(parsed))
)
end
end
# Keep track of the public rulesets per locale so we can introspect the
# public interface
defp add_function_to_exports(
{:def, _aliases, [{:when, _, [{name, _, _} | _]} | _]} = function,
"public",
module,
locale
) do
public_rulesets = Module.get_attribute(module, @public_rulesets) || %{}
locale_public_rulesets = [name | Map.get(public_rulesets, locale) || []]
Module.put_attribute(
module,
@public_rulesets,
Map.put(public_rulesets, locale, Enum.uniq(locale_public_rulesets))
)
function
end
defp add_function_to_exports(other, _access, _module, _locale) do
other
end
def rbnf_rule_error(number, rule_group, locale_name) do
{
Cldr.Rbnf.NoRuleForNumber,
"rule group #{inspect(rule_group)} for locale #{inspect(locale_name)} does not " <>
"know how to process #{inspect(number)}"
}
end
defmacro __before_compile__(env) do
module = env.module
backend =
module
|> Module.get_attribute(:backend)
rule_sets =
module
|> Module.get_attribute(:public_rulesets)
all_rule_sets =
rule_sets
|> Map.values()
|> List.flatten()
|> Enum.uniq()
|> Enum.sort()
rule_sets = Macro.escape(rule_sets)
quote location: :keep,
bind_quoted: [
rule_sets: rule_sets,
all_rule_sets: all_rule_sets,
backend: backend,
module: module
] do
# A map of rule sets by locale
def rule_sets do
unquote(Macro.escape(rule_sets))
end
# All rule sets for a locale
def rule_sets(%Cldr.LanguageTag{rbnf_locale_name: rbnf_locale_name}) do
rule_sets(rbnf_locale_name)
end
def rule_sets(rbnf_locale_name) when is_atom(rbnf_locale_name) do
Map.get(rule_sets(), rbnf_locale_name)
end
def rule_sets(rbnf_locale_name) when is_binary(rbnf_locale_name) do
rbnf_locale_name
|> String.to_existing_atom()
|> rule_sets
rescue
ArgumentError ->
nil
end
# All rule sets for all locales
def all_rule_sets do
unquote(all_rule_sets)
end
# Return an error for a valid rule set which
# is not supported for either the locale or
# the number
for rule_group <- all_rule_sets do
@dialyzer {:nowarn_function, [{rule_group, 2}]}
def unquote(rule_group)(number, locale_name)
when is_atom(locale_name) or is_binary(locale_name) do
with {:ok, locale} <- unquote(backend).validate_locale(locale_name) do
unquote(rule_group)(number, locale)
end
end
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: rbnf_locale_name}) do
{:error, rbnf_rule_error(number, unquote(rule_group), rbnf_locale_name)}
end
# NumberSystem rules are only in the root locale so
# lets make it easier to use them by defaulting the locale
if hd(Enum.reverse(Module.split(module))) == "NumberSystem" do
def unquote(rule_group)(number) do
unquote(rule_group)(number, unquote(Cldr.Config.root_locale_name()))
end
end
end
end
end
end