defmodule Formula do
@moduledoc """
Documentation for `Formula`.
"""
alias Explorer.Series
alias Formula.Actions.GetFields
alias Formula.Errors.InvalidFormulaError
alias Formula.Evaluator
alias Formula.Function
alias Formula.Lexer
@reserved_result_key "elixir.formula.result"
@doc """
Evaluate the formula given the data
"""
@spec evaluate(map(), String.t()) :: term() | {:error, term()}
def evaluate(data, formula) when is_binary(formula) do
evaluation_result =
data
|> Map.merge(%{@reserved_result_key => "=" <> formula})
|> evaluate()
case evaluation_result do
{:error, error} -> {:error, error}
result -> Map.get(result, @reserved_result_key)
end
end
def evaluate(_data, _formula) do
{:error, InvalidFormulaError.exception(message: "formula must be a string")}
end
@doc """
Evaluate a given map that contains function
"""
@spec evaluate(map()) :: map() | {:error, term()}
def evaluate(data) do
case parse(data) do
{:error, error} -> {:error, error}
parsed_data -> Evaluator.evaluate(parsed_data)
end
end
@doc """
Parse a given map into functions and symbols
"""
@spec parse(map()) :: map() | {:error, term()}
def parse(data) do
Enum.reduce_while(data, %{}, fn kv, acc ->
case parse_data(kv) do
{_key, {:error, error}} -> {:halt, {:error, error}}
{key, value} -> {:cont, Map.put(acc, key, value)}
end
end)
end
@doc """
Parse a formula.
Return a function struct if it is valid. Return error otherwise.
"""
@spec parse_formula(String.t()) :: Function.t() | {:error, InvalidFormulaError.t()}
def parse_formula(string) do
parse_formula(string, @reserved_result_key)
end
@spec parse_formula(String.t(), String.t()) :: Function.t() | {:error, InvalidFormulaError.t()}
def parse_formula(string, key) do
result =
string
|> Lexer.tokenize!()
|> :formula_parser.parse()
case result do
{:ok, function} ->
function
{:error, _} ->
{:error, InvalidFormulaError.exception(message: "Please check your formula", key: key)}
end
end
@doc """
Return the fields for a given formula
"""
@spec get_fields(String.t()) :: list(String.t()) | {:error, term()}
defdelegate get_fields(formula), to: GetFields, as: :call
defp parse_data({key, "=" <> value}) do
{key, parse_formula(value, key)}
end
defp parse_data({key, value}) when is_binary(value) do
case Decimal.parse(value) do
{decimal, _} -> {key, decimal}
:error -> {key, value}
end
end
defp parse_data({key, value}) when is_integer(value) do
{key, Decimal.new(value)}
end
defp parse_data({key, value}) when is_float(value) do
{key, Decimal.from_float(value)}
end
defp parse_data({key, value}) when is_list(value) do
parsed_value =
value
|> Enum.map(&parse_series_data/1)
|> Series.from_list(dtype: :float)
{key, parsed_value}
end
defp parse_data({key, value}), do: {key, value}
defp parse_series_data(%Decimal{} = value) do
Decimal.to_float(value)
end
defp parse_series_data(value) when is_binary(value) do
value
|> Float.parse()
|> elem(0)
end
defp parse_series_data(value) when is_float(value), do: value
defp parse_series_data(value) when is_integer(value) do
value
|> Integer.to_string()
|> Float.parse()
|> elem(0)
end
end