lib/result/calc.ex

defmodule Result.Calc do
  @moduledoc """
  Result calculations
  """

  @doc """
  Calculate the AND of two results

  r_and :: Result e1 a -> Result e2 b -> Result [e1, e2] [a, b]

  ## Examples

      iex> Result.Calc.r_and({:ok, 1}, {:ok, 2})
      {:ok, [1, 2]}

      iex> Result.Calc.r_and({:ok, 1}, {:error, 2})
      {:error, [2]}

      iex> Result.Calc.r_and({:error, 1}, {:ok, 2})
      {:error, [1]}

      iex> Result.Calc.r_and({:error, 1}, {:error, 2})
      {:error, [1, 2]}

  """
  @spec r_and(Result.t(any, any), Result.t(any, any)) :: Result.t([...], [...])
  def r_and({:ok, val1}, {:ok, val2}) do
    {:ok, [val1, val2]}
  end

  def r_and({:ok, _}, {:error, val2}) do
    {:error, [val2]}
  end

  def r_and({:error, val1}, {:ok, _}) do
    {:error, [val1]}
  end

  def r_and({:error, val1}, {:error, val2}) do
    {:error, [val1, val2]}
  end

  @doc """
  Calculate the OR of two results

  r_or :: Result e1 a -> Result e2 b -> Result [e1, e2] [a, b]

  ## Examples

      iex> Result.Calc.r_or({:ok, 1}, {:ok, 2})
      {:ok, [1, 2]}

      iex> Result.Calc.r_or({:ok, 1}, {:error, 2})
      {:ok, [1]}

      iex> Result.Calc.r_or({:error, 1}, {:ok, 2})
      {:ok, [2]}

      iex> Result.Calc.r_or({:error, 1}, {:error, 2})
      {:error, [1, 2]}

  """
  @spec r_or(Result.t(any, any), Result.t(any, any)) :: Result.t([...], [...])
  def r_or({:ok, val1}, {:ok, val2}) do
    {:ok, [val1, val2]}
  end

  def r_or({:ok, val1}, {:error, _}) do
    {:ok, [val1]}
  end

  def r_or({:error, _}, {:ok, val2}) do
    {:ok, [val2]}
  end

  def r_or({:error, val1}, {:error, val2}) do
    {:error, [val1, val2]}
  end

  @doc """
  Calculate product of Results

  product :: List (Result e a) -> Result (List e) (List a)

  ## Examples

      iex> data = [{:ok, 1}, {:ok, 2}, {:ok, 3}]
      iex> Result.Calc.product(data)
      {:ok, [1, 2, 3]}

      iex> data = [{:error, 1}, {:ok, 2}, {:error, 3}]
      iex> Result.Calc.product(data)
      {:error, [1, 3]}

      iex> data = [{:error, 1}]
      iex> Result.Calc.product(data)
      {:error, [1]}

      iex> data = []
      iex> Result.Calc.product(data)
      {:ok, []}
  """
  @spec product([Result.t(any, any)]) :: Result.t([...], [...])
  def product(list) do
    product(list, {:ok, []})
  end

  defp product([head | tail], acc) do
    result =
      acc
      |> r_and(head)
      |> flatten()

    product(tail, result)
  end

  defp product([], acc) do
    acc
  end

  @doc """
  Calculate sum of Results

  sum :: List (Result e a) -> Result (List e) (List a)

  ## Examples

      iex> data = [{:ok, 1}, {:ok, 2}, {:ok, 3}]
      iex> Result.Calc.sum(data)
      {:ok, [1, 2, 3]}

      iex> data = [{:error, 1}, {:ok, 2}, {:error, 3}]
      iex> Result.Calc.sum(data)
      {:ok, [2]}

      iex> data = [{:error, 1}, {:error, 2}, {:error, 3}]
      iex> Result.Calc.sum(data)
      {:error, [1, 2, 3]}

      iex> data = [{:error, 1}]
      iex> Result.Calc.sum(data)
      {:error, [1]}

      iex> data = []
      iex> Result.Calc.sum(data)
      {:error, []}
  """
  @spec sum([Result.t(any, any)]) :: Result.t([...], [...])
  def sum(list) do
    sum(list, {:error, []})
  end

  defp sum([head | tail], acc) do
    result =
      acc
      |> r_or(head)
      |> flatten

    sum(tail, result)
  end

  defp sum([], acc) do
    acc
  end

  defp flatten({state, [head | tail]}) when is_list(head) do
    {state, head ++ tail}
  end

  defp flatten({state, list}) do
    {state, list}
  end
end