lib/ex_rose_tree/util.ex

defmodule ExRoseTree.Util do
  @moduledoc """
  Various utility functions.
  """

  @type result() :: {:ok, term()} | {:error, term()} | :error

  @type result_fn() :: (... -> result())

  @doc """
  Given a term and a list of potential functions to apply to the term,
  will return with the result of the first one that succeeds when
  applied to the subject term. If no functions succeed, returns nil.

  The list of functions should each be of type `ExRoseTree.Util.result_fn()`,
  in other words, they should return a `ExRoseTree.Util.result()` type.

  ## Examples

      iex> funs = for n <- [4,3,2,1], do: &(if n == &1 do {:ok, n} else :error end)
      ...> ExRoseTree.Util.first_of(3, funs)
      3

      iex> funs = for n <- [4,3,2,1], do: &(if n == &1 do {:ok, n} else :error end)
      ...> ExRoseTree.Util.first_of(6, funs)
      nil

  """
  @spec first_of(term(), [result_fn()]) :: term() | nil
  def first_of(_term, []), do: nil

  def first_of(term, [h | t] = _funs) when is_function(h) do
    case h.(term) do
      {:ok, result} -> result
      {:error, _error} -> first_of(term, t)
      :error -> first_of(term, t)
      nil -> first_of(term, t)
      false -> first_of(term, t)
      result -> result
    end
  end

  @doc """
  Given a term, a list of potential functions to apply to the term,
  and a keyword list of options to apply to each function, will return
  with the result of the first one that succeeds when applied to the
  subject term. If no functions succeed, returns nil.

  The list of functions should each be of type `ExRoseTree.Util.result_fn()`,
  in other words, they should return a `ExRoseTree.Util.result()` type.

  ## Examples

      iex> fun = fn x, y, z ->
      ...>   mult_by = Keyword.get(z, :mult_by, 1)
      ...>   if x == y do {:ok, x * mult_by} else :error end
      ...> end
      ...> funs = for n <- [4,3,2,1], do: &fun.(n, &1, &2)
      ...> ExRoseTree.Util.first_of_with_opts(3, funs, [mult_by: 2])
      6

  """
  @spec first_of_with_opts(term(), [function()], keyword()) :: term() | nil
  def first_of_with_opts(_term, [], _opts), do: nil

  def first_of_with_opts(term, [h | t] = _funs, opts)
      when is_function(h) and
             is_list(opts) do
    case h.(term, opts) do
      {:ok, result} -> result
      {:error, _error} -> first_of_with_opts(term, t, opts)
      :error -> first_of_with_opts(term, t, opts)
      nil -> first_of_with_opts(term, t, opts)
      false -> first_of_with_opts(term, t, opts)
      result -> result
    end
  end

  @doc """
  Given a term, a list of potential functions to apply to the term,
  and a list of arguments to apply to each function, will return
  with the result of the first one that succeeds when applied to the
  subject term. If no functions succeed, returns nil.

  The list of functions should each be of type `ExRoseTree.Util.result_fn()`,
  in other words, they should return a `ExRoseTree.Util.result()` type.

  ## Examples

      iex> fun = fn x, y, add_by, sub_by ->
      ...>   if x == y do {:ok, x + add_by - sub_by} else :error end
      ...> end
      ...> funs = for n <- [4,3,2,1], do: &fun.(n, &1, &2, &3)
      ...> ExRoseTree.Util.first_of_with_args(3, funs, [2, 1])
      4

  """
  @spec first_of_with_args(term(), [function()], [term()]) :: term() | nil
  def first_of_with_args(_term, [], _args), do: nil

  def first_of_with_args(term, [h | t] = _funs, args)
      when is_function(h) and
             is_list(args) do
    case apply(h, [term | args]) do
      {:ok, result} -> result
      {:error, _error} -> first_of_with_args(term, t, args)
      :error -> first_of_with_args(term, t, args)
      nil -> first_of_with_args(term, t, args)
      false -> first_of_with_args(term, t, args)
      result -> result
    end
  end

  @doc """
  A function that always returns true, regardless of what is passed to it.

  ## Examples

      iex> ExRoseTree.Util.always(5)
      true

      iex> ExRoseTree.Util.always(false)
      true

  """
  @spec always(term()) :: true
  def always(_term), do: true

  @doc """
  A function that always returns false, regardless of what is passed to it.

  ## Examples

      iex> ExRoseTree.Util.never(5)
      false

      iex> ExRoseTree.Util.never(true)
      false

  """
  @spec never(term()) :: false
  def never(_term), do: false

  @doc """
  A function that applies a predicate to a term. If the function application
  is true, returns the original term. If false, returns nil.

  ## Examples

      iex> ExRoseTree.Util.maybe(5, &(&1 == 5))
      5

      iex> ExRoseTree.Util.maybe(5, &(&1 == 1))
      nil

  """
  @spec maybe(term(), (term() -> boolean())) :: term() | nil
  def maybe(value, predicate) when is_function(predicate) do
    if predicate.(value) == true do
      value
    else
      nil
    end
  end

  @doc """
  Similar to `Enum.split/2` but with specialized behavior. It is
  optimized to return the list of elements that come before the
  index in reverse order. This is ideal for the context-aware nature
  of Zippers. Also unlike `Enum.split/2`, if given an index that
  is greater than or equal to the total elements in the given list or
  if given a negative index, this function will _not_ perform a split,
  and will return two empty lists.

  ## Examples

      iex> ExRoseTree.Util.split_at([1,2,3,4,5], 2)
      {[2, 1], [3, 4, 5]}

      iex> ExRoseTree.Util.split_at([1,2,3,4,5], 10)
      {[], []}

  """
  @spec split_at(list(), non_neg_integer()) :: {[term()], [term()]}
  def split_at([], _), do: {[], []}

  def split_at(elements, index)
      when is_list(elements) and
             is_integer(index) and
             index >= 0 do
    if index >= Enum.count(elements) do
      {[], []}
    else
      {_current_idx, prev, next} =
        elements
        |> Enum.reduce(
          {0, [], []},
          fn entry, {current_idx, prev, next} ->
            if current_idx >= index do
              {current_idx + 1, prev, [entry | next]}
            else
              {current_idx + 1, [entry | prev], next}
            end
          end
        )

      {prev, Enum.reverse(next)}
    end
  end

  def split_at(elements, index)
      when is_list(elements) and
             is_integer(index),
      do: {[], []}

  @doc """
  Similar to `Enum.split/2`, `Enum.split_while/2`, and `Enum.split_with/2`,
  `split_when/2` instead takes a list of elements and a predicate to apply
  to each element. The first element that passes the predicate is where
  the list of elements will be split, with the target element as the head
  of the second list in the return value. Like with `split_at/2`, the first
  list of elements are returned in reverse order.

  ## Examples

      iex> ExRoseTree.Util.split_when([1,2,3,4,5], fn x -> x == 3 end)
      {[2, 1], [3, 4, 5]}

  """
  @spec split_when(list(), predicate :: (term() -> boolean())) :: {[term()], [term()]}
  def split_when([], predicate) when is_function(predicate), do: {[], []}

  def split_when(elements, predicate)
      when is_list(elements) and is_function(predicate) do
    do_split_when([], elements, predicate)
  end

  @spec do_split_when(list(), list(), (term() -> boolean())) :: {[term()], [term()]}
  defp do_split_when(_acc, [] = _remaining, _predicate), do: {[], []}

  defp do_split_when(acc, [head | tail] = remaining, predicate) do
    if predicate.(head) == true do
      {acc, remaining}
    else
      do_split_when([head | acc], tail, predicate)
    end
  end
end