Packages

Add a dice-rolling DSL with sigil support and reusable compiled functions to your application.

Current section

Files

Jump to
ex_dice_roller lib compilers math.ex
Raw

lib/compilers/math.ex

defmodule ExDiceRoller.Compilers.Math do
@moduledoc """
Handles compiling expressions using common mathematical operators.
iex> {:ok, tokens} = ExDiceRoller.Tokenizer.tokenize("1+x")
{:ok, [{:digit, 1, '1'}, {:basic_operator, 1, '+'}, {:var, 1, 'x'}]}
iex> {:ok, parse_tree} = ExDiceRoller.Parser.parse(tokens)
{:ok, {{:operator, '+'}, {:digit, '1'}, {:var, 'x'}}}
iex> fun = ExDiceRoller.Compilers.Math.compile(parse_tree)
iex> fun.([x: 2], [])
3
ExDiceRoller uses [infix notation](https://en.wikipedia.org/wiki/Infix_notation)
when working with mathematical operators. Below is the list of operators
currently supported by ExDiceRoller:
* `+`: adds the values on both sides of the expression
* `-`: subtracts the value on the right from the value on the left
* `*`: multiplies the values on both sides of the expression
* `/`: divides, with the left value as the dividend, the right the divisor
* `%`: [modulo](https://en.wikipedia.org/wiki/Modulo_operation), with the
left the dividend, the right the divisor
* `^`: exponentiation, with the left the base, the right the exponent
"""
@behaviour ExDiceRoller.Compiler
alias ExDiceRoller.Compiler
@operators [
{'+', &Kernel.+/2, "add"},
{'-', &Kernel.-/2, "sub"},
{'*', &Kernel.*/2, "mul"},
{'/', &Kernel.//2, "div"},
{'%', &__MODULE__.modulo/2, "mod"},
{'^', &:math.pow/2, "exp"}
]
@impl true
def compile({{:operator, op}, left_expr, right_expr}) do
compile_op(op, Compiler.delegate(left_expr), Compiler.delegate(right_expr))
end
@doc "Function used for modulo calculations."
@spec modulo(number, number) :: integer
def modulo(l, r), do: rem(Compiler.round_val(l), Compiler.round_val(r))
@spec compile_op(charlist, Compiler.compiled_val(), Compiler.compiled_val()) ::
Compiler.compiled_val()
for {char, _, name} <- @operators do
defp compile_op(unquote(char), l, r), do: unquote(:"compile_#{name}")(l, r)
end
for {_, fun, name} <- @operators do
@spec unquote(:"compile_#{name}")(Compiler.compiled_val(), Compiler.compiled_val()) ::
Compiler.compiled_val()
defp unquote(:"compile_#{name}")(l, r) when is_function(l) and is_function(r) do
fn args, opts -> op(l.(args, opts), r.(args, opts), unquote(fun)) end
end
defp unquote(:"compile_#{name}")(l, r) when is_function(l) do
fn args, opts -> op(l.(args, opts), r, unquote(fun)) end
end
defp unquote(:"compile_#{name}")(l, r) when is_function(r) do
fn args, opts -> op(l, r.(args, opts), unquote(fun)) end
end
defp unquote(:"compile_#{name}")(l, r), do: op(l, r, unquote(fun))
end
@spec op(list | number, list | number, function) :: list | number
defp op(l, r, fun) when is_list(l) and is_list(r) do
if length(l) != length(r) do
raise ArgumentError, "you cannot add two lists of different sizes"
end
Enum.map(0..(length(l) - 1), &fun.(Enum.at(l, &1), Enum.at(r, &1)))
end
defp op(l, r, fun) when is_list(l), do: Enum.map(l, &fun.(&1, r))
defp op(l, r, fun) when is_list(r), do: Enum.map(r, &fun.(&1, l))
defp op(l, r, fun), do: fun.(l, r)
end