lib/r_enum/support.ex

defmodule REnum.Support do
  @moduledoc """
  Summarized other useful functions related to enumerable.
  Defines all of here functions when `use REnum.Support`.
  """
  @spec __using__(any) :: list
  defmacro __using__(_opts) do
    RUtils.define_all_functions!(__MODULE__)
  end

  @type type_enumerable :: Enumerable.t()
  @type type_pattern :: number() | String.t() | Range.t() | Regex.t()

  @doc """
  Returns true if argument is range.
  ## Examples
      iex> REnum.range?([1, 2, 3])
      false

      iex> REnum.range?(2..3)
      true
  """
  @spec range?(type_enumerable) :: boolean()
  def range?(_.._), do: true
  def range?(_), do: false

  @doc """
  Returns true if argument is map and not range.
  ## Examples
      iex> REnum.map_and_not_range?(%{})
      true

      iex> REnum.map_and_not_range?(1..3)
      false
  """
  @spec map_and_not_range?(type_enumerable) :: boolean
  def map_and_not_range?(enumerable) do
    is_map(enumerable) && !range?(enumerable)
  end

  @doc """
  Returns true if argument is list and not keyword list.
  ## Examples
      iex> REnum.list_and_not_keyword?([1, 2, 3])
      true

      iex> REnum.list_and_not_keyword?([a: 1, b: 2])
      false
  """
  @spec list_and_not_keyword?(type_enumerable) :: boolean()
  def list_and_not_keyword?(enumerable) do
    !Keyword.keyword?(enumerable) && is_list(enumerable)
  end

  @doc """
  Returns truthy count.
  ## Examples
      iex> REnum.truthy_count([1, 2, 3])
      3

      iex> REnum.truthy_count([1, nil, false])
      1
  """
  @spec truthy_count(type_enumerable) :: non_neg_integer()
  def truthy_count(enumerable) do
    enumerable
    |> Enum.count(& &1)
  end

  @doc """
  Returns truthy count that judged by given function.
  ## Examples
      iex> REnum.truthy_count([1, 2, 3], &(&1 < 3))
      2

      iex> REnum.truthy_count([1, nil, false], &(is_nil(&1)))
      1

      iex> REnum.truthy_count(["bar", "baz", "foo"], ~r/a/)
      2
  """
  @spec truthy_count(type_enumerable, function()) :: non_neg_integer()
  def truthy_count(enumerable, func) when is_function(func) do
    enumerable
    |> Enum.filter(func)
    |> Enum.count()
  end

  def truthy_count(enumerable, pattern) do
    enumerable
    |> truthy_count(match_function(pattern))
  end

  @doc """
  Returns matching function required one argument by given pattern.
  ## Examples
      iex> REnum.match_function(1..3).(2)
      true

      iex> REnum.match_function(~r/a/).("bcd")
      false
  """
  @spec match_function(type_pattern) :: function()
  def match_function(pattern) do
    cond do
      Regex.regex?(pattern) -> &(&1 =~ pattern)
      range?(pattern) -> &(&1 in pattern)
      true -> &(&1 == pattern)
    end
  end

  @doc """
  Returns the first element for which function(with each element index) returns a truthy value.
  ## Examples
      iex> REnum.find_index_with_index(1..3, fn el, index ->
      ...>     IO.inspect(index)
      ...>     el == 2
      ...>     end)
      # 0
      # 1
      1
  """
  @spec find_index_with_index(type_enumerable(), function()) :: neg_integer() | nil
  def find_index_with_index(enumerable, func) do
    enumerable
    |> Enum.to_list()
    |> find_index_with_index(func, 0)
  end

  defp find_index_with_index([], _, _), do: nil

  defp find_index_with_index([head | tail], func, index) do
    if(func.(head, index)) do
      index
    else
      find_index_with_index(tail, func, index + 1)
    end
  end
end