lib/formula/function/runtime.ex

defmodule Formula.Function.Runtime do
  @moduledoc """
  Evaluate the function
  """
  alias Explorer.Series
  alias Formula.Errors.DivideByZeroError
  alias Formula.Errors.InvalidArgumentsError
  alias Formula.Errors.InvalidFormulaError
  alias Formula.Function.DecimalOperation
  alias Formula.Function.SeriesOperation

  @type success :: {:ok, term()}
  @type failure ::
          {:error, DivideByZeroError.t() | InvalidFormulaError.t() | InvalidArgumentsError.t()}

  @spec exec(String.t(), list(Decimal.t())) :: success | failure
  def exec(name, arguments) do
    case call(name, arguments) do
      {:error, error} -> {:error, error}
      value -> {:ok, value}
    end
  end

  defp call(operation, [%Decimal{}, %Decimal{}] = arguments) do
    DecimalOperation.call(operation, arguments)
  end

  defp call(operation, [%Series{}, %Series{}] = arguments) do
    with true <- SeriesOperation.is_equal_length?(arguments),
         %Series{} = series <- SeriesOperation.call(operation, arguments) do
      series
    else
      false ->
        {:error,
         InvalidArgumentsError.exception(
           message: "list must be same length",
           arguments: Enum.map(arguments, &Series.to_list/1)
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  defp call(operation, [%Series{} = a, %Decimal{} = b]) do
    case SeriesOperation.call(operation, [a, Decimal.to_float(b)]) do
      %Series{} = series -> series
      {:error, error} -> {:error, error}
    end
  end

  defp call(operation, [%Decimal{} = a, %Series{} = b]) do
    case SeriesOperation.call(operation, [Decimal.to_float(a), b]) do
      %Series{} = series -> series
      {:error, error} -> {:error, error}
    end
  end

  defp call(operation, [%Series{} = series]) do
    case SeriesOperation.call(operation, series) do
      value when is_float(value) -> Decimal.from_float(value)
      value when is_integer(value) -> Decimal.new(value)
      {:error, error} -> {:error, error}
    end
  end

  defp call(operation, arguments) do
    {:error,
     InvalidArgumentsError.exception(
       message: "Invalid data type",
       operation: operation,
       arguments: inspect(arguments)
     )}
  end
end