defmodule Formula.Evaluator do
@moduledoc """
Module wrapper to evaluate the data or formula.
"""
alias Explorer.Series
alias Formula.Evaluable
@spec evaluate(map()) :: map() | {:error, term()}
def evaluate(data) do
case do_evaluate(data) do
result when is_map(result) ->
result
|> Enum.map(fn {k, v} -> {k, convert_value(v)} end)
|> Enum.into(%{})
{:error, error} ->
{:error, error}
end
end
defp do_evaluate(data) do
Enum.reduce_while(data, %{}, fn {key, formula}, acc ->
case Evaluable.evaluate(formula, data) do
{:ok, value} -> {:cont, Map.put(acc, key, value)}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp convert_value(%Series{} = series) do
series
|> Series.to_list()
|> Enum.map(&Decimal.from_float/1)
end
defp convert_value(value), do: value
end
defprotocol Formula.Evaluable do
@fallback_to_any true
@spec evaluate(term(), map()) :: {:ok, term()} | {:error, term()} | {:error, list(term())}
def evaluate(value, data)
end
defimpl Formula.Evaluable, for: Any do
@spec evaluate(term(), map()) :: {:ok, term()}
def evaluate(value, _data) do
{:ok, value}
end
end
defimpl Formula.Evaluable, for: List do
alias Formula.Helpers
@spec evaluate(list(term()), map()) :: {:ok, list(term())} | {:error, list(term())}
def evaluate(value, data) do
case Helpers.convert_list(value, &@protocol.evaluate(&1, data)) do
{:ok, list} -> {:ok, list}
{:error, errors} -> {:error, errors}
end
end
end
defimpl Formula.Evaluable, for: Formula.Symbol do
alias Formula.Errors.MissingArgumentsError
alias Formula.Symbol
@spec evaluate(Symbol.t(), map()) :: {:ok, term()} | {:error, term()}
def evaluate(%{name: name}, data) do
case Map.fetch(data, name) do
{:ok, value} ->
{:ok, value}
:error ->
{:error, MissingArgumentsError.exception(message: "Missing arguments", argument: name)}
end
end
end
defimpl Formula.Evaluable, for: Formula.Function do
alias Formula.Function
@spec evaluate(Function.t(), map()) :: {:ok, term()} | {:error, term()}
def evaluate(%{name: name, arguments: arguments}, data) do
case @protocol.evaluate(arguments, data) do
{:ok, arguments} -> @for.Runtime.exec(name, arguments)
{:error, error} -> {:error, error}
end
end
end