Current section

Files

Jump to
ex_cldr_numbers lib number rbnf processor.ex
Raw

lib/number/rbnf/processor.ex

# credo:disable-for-this-file
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
quote location: :keep do
alias Cldr.Number
alias Cldr.Digits
import Cldr.Rbnf.Processor
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), 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, 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, 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 = Cldr.Number.Ordinal.plural_rule(number, locale)
Map.get(plurals, plural) || Map.get(plurals, :other)
end
defp do_operation(:cardinal, number, locale, _function, _rule, plurals) do
plural = Cldr.Number.Cardinal.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(&Cldr.Rbnf.Spellout.spellout_cardinal(&1, locale))
|> Enum.join(" ")
end
@before_compile Cldr.Rbnf.Processor
end
end
@public_rulesets :public_rulesets
def define_rules(rule_group_name, env) do
Module.register_attribute(env.module, @public_rulesets, [])
iterate_rules rule_group_name, 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, access, rule) ->
{:ok, parsed} = Cldr.Rbnf.Rule.parse(rule.definition)
function_body = rule_body(locale, rule_group, rule, parsed)
rule.base_value
|> define_rule(rule.range, rule_group, locale, function_body)
|> add_function_to_exports(access, env.module, locale)
|> Code.eval_quoted([], env)
end
end
defp iterate_rules(rule_group_type, fun) do
all_rules = Cldr.Rbnf.for_all_locales[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, :error)
end
end
end
end
defp define_rule(:error, _range, rule_group, locale_name, _body) do
quote do
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("-x", _range, rule_group, locale, body) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
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, body) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
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, body) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
when is_integer(number), do: unquote(body)
end
end
defp define_rule(base_value, "undefined", rule_group, locale, body)
when is_integer(base_value) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
when Kernel.and(is_integer(number), number >= unquote(base_value)),
do: unquote(body)
end
end
defp define_rule(base_value, range, rule_group, locale, body)
when is_integer(range) and is_integer(base_value) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
when Kernel.and(is_integer(number),
Kernel.and(number >= unquote(base_value), number < unquote(range))),
do: unquote(body)
end
end
defp define_rule(base_value, "undefined", rule_group, locale, body)
when is_integer(base_value) do
quote do
def unquote(rule_group)(number, %Cldr.LanguageTag{rbnf_locale_name: unquote(locale)})
when Kernel.and(is_integer(number), number >= unquote(base_value)),
do: unquote(body)
end
end
defp define_rule("Inf", _range, _rule_group, _locale, _body) do
{:error, "Infinite rule sets are not implemented"}
end
defp define_rule("NaN", _range, _rule_group, _locale, _body) do
{:error, "NaN rule sets are not implemented"}
end
defp define_rule("0.x", _range, _rule_group, _locale, _body) do
{:error, "Proper Fraction rule sets are not implemented"}
end
defp define_rule("x.0", _range, _rule_group, _locale, _body) do
{:error, "Master rule sets are not implemented"}
end
# Get the AST of the rule body
defp rule_body(locale, rule_group, rule, parsed) do
quote do
do_rule(number,
unquote(Macro.escape(Cldr.Locale.new(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(function, _access, _module, _locale) do
function
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
quote do
def rule_sets(%Cldr.LanguageTag{rbnf_locale_name: rbnf_locale_name}) do
unquote(Macro.escape(Module.get_attribute(env.module, :public_rulesets)))
|> Map.get(rbnf_locale_name)
end
end
end
end