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