lib/r_enum/ruby.ex

defmodule REnum.Ruby do
  @moduledoc """
  Summarized all of Ruby's Enumerable functions.
  If a function with the same name already exists in Elixir, that is not implemented.
  Also, the function that returns Enumerator in Ruby is customized each behavior on the characteristics.
  Defines all of here functions when `use REnum.Ruby`.
  """
  @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()

  import REnum.Support

  # https://ruby-doc.org/core-3.1.0/Enumerable.html
  # [:all?, :any?, :chain, :chunk, :chunk_while, :collect, :collect_concat, :compact, :count, :cycle, :detect, :drop, :drop_while, :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object, :entries, :filter, :filter_map, :find, :find_all, :find_index, :first, :flat_map, :grep, :grep_v, :group_by, :include?, :inject, :lazy, :map, :max, :max_by, :member?, :min, :min_by, :minmax, :minmax_by, :none?, :one?, :partition, :reduce, :reject, :reverse_each, :select, :slice_after, :slice_before, :slice_when, :sort, :sort_by, :sum, :take, :take_while, :tally, :to_a, :to_h, :uniq, :zip]
  # |> RUtils.required_functions([Enum])
  # ✔ chain
  # ✔ collect
  # ✔ collect_concat
  # ✔ compact
  # ✔ cycle
  # ✔ detect
  # ✔ each_cons
  # ✔ each_entry
  # ✔ each_slice
  # ✔ each_with_index
  # ✔ each_with_object
  # ✔ entries
  # ✔ find_all
  # ✔ first
  # ✔ grep
  # ✔ grep_v
  # ✔ include?
  # ✔ inject
  # ✔ lazy
  # ✔ minmax
  # ✔ minmax_by
  # ✔ none?
  # ✔ one?
  # ✔ reverse_each
  # ✔ select
  # ✔ slice_after
  # ✔ slice_before
  # ✔ slice_when
  # ✔ tally
  # ✔ to_a
  # ✔ to_h

  @doc """
  Returns an list of all non-nil elements.
  ## Examples
      iex> REnum.compact([1, nil, 2, 3])
      [1, 2, 3]

      iex> REnum.compact(%{
      ...>        :truthy => true,
      ...>        false => false,
      ...>        nil => nil,
      ...>        :map => %{key: :value}
      ...>      })
      %{
        :truthy => true,
        false => false,
        :map => %{key: :value}
      }
  """
  @spec compact(type_enumerable) :: type_enumerable
  def compact(enumerable) when is_list(enumerable) do
    enumerable
    |> Enum.reject(&(&1 |> is_nil()))
  end

  def compact(enumerable) when is_map(enumerable) do
    enumerable
    |> Enum.reject(fn {key, value} ->
      is_nil(key) && is_nil(value)
    end)
    |> Enum.into(%{})
  end

  @doc """
  Returns the first element.
  ## Examples
      iex> REnum.first([1, 2, 3])
      1

      iex> REnum.first(%{a: 1, b: 2})
      {:a, 1}
  """
  @spec first(type_enumerable) :: any()
  def first(enumerable) do
    result = Enum.at(enumerable, 0)

    cond do
      result |> is_nil() -> nil
      true -> result
    end
  end

  if(VersionManager.support_version?()) do
    @doc """
    Returns leading elements.
    ## Examples
        iex> REnum.first([1, 2, 3], 2)
        [1, 2]

        iex> REnum.first(%{a: 1, b: 2}, 2)
        [{:a, 1}, {:b, 2}]
    """
  end

  @spec first(type_enumerable, non_neg_integer()) :: type_enumerable()
  def first(enumerable, n) do
    0..(n - 1)
    |> Enum.with_index(fn _, index ->
      enumerable |> Enum.at(index)
    end)
    |> compact()
  end

  @doc """
  Return true if enumerable has only one truthy element; false otherwise.
  ## Examples
      iex> REnum.one?([1, nil, false])
      true

      iex> REnum.one?(1..4)
      false
  """
  @spec one?(type_enumerable) :: boolean()
  def one?(enumerable) do
    truthy_count(enumerable) == 1
  end

  @doc """
  Returns true if exactly one element meets a specified criterion; false otherwise.
  ## Examples
      iex> REnum.one?(1..4, 1..2)
      false

      iex> REnum.one?(1..4, &(&1 < 2))
      true

      iex> REnum.one?(1..4, 1)
      true
  """
  @spec one?(type_enumerable, function() | type_pattern) :: boolean()
  def one?(enumerable, pattern_or_func) do
    truthy_count(enumerable, pattern_or_func) == 1
  end

  @doc """
  Returns true if enumerable does not include truthy value; false otherwise.
  ## Examples
      iex> REnum.none?(1..4)
      false

      iex> REnum.none?([nil, false])
      true

      iex> REnum.none?([foo: 0, bar: 1])
      false
  """
  @spec none?(type_enumerable) :: boolean()
  def none?(enumerable) do
    truthy_count(enumerable) == 0
  end

  @doc """
  Returns whether no element meets a given criterion.
  ## Examples
      iex> REnum.none?(1..4, &(&1 < 1))
      true

      iex> REnum.none?(%{foo: 0, bar: 1, baz: 2}, fn {_, v} -> v < 0 end)
      true

      iex> REnum.none?(1..4, 5)
      true

      iex> REnum.none?(1..4, 2..3)
      false
  """
  @spec none?(type_enumerable, function() | type_pattern) :: boolean()
  def none?(enumerable, pattern_or_func) do
    truthy_count(enumerable, pattern_or_func) == 0
  end

  @doc """
  When called with positive integer argument n and a function, calls the block with each element, then does so again, until it has done so n times; returns given enumerable
  When called with a function and n is nil, returns Stream cycled forever.
  ## Examples
      iex> REnum.cycle(["a", "b"], 2, &IO.puts(&1))
      # a
      # b
      # a
      # b
      ["a", "b"]

      iex> REnum.cycle(%{a: 1, b: 2}, nil, &IO.inspect(&1)) |> Enum.take(2)
      # {:a, 1}
      # {:b, 2}
      # {:a, 1}
      # {:b, 2}
      [:ok, :ok]
  """
  @spec cycle(type_enumerable, non_neg_integer(), function()) :: Stream | type_enumerable
  def cycle(enumerable, n, func) when is_nil(n) do
    Stream.repeatedly(fn ->
      enumerable
      |> Enum.each(func)
    end)
  end

  def cycle(enumerable, n, _) when n < 1, do: enumerable

  def cycle(enumerable, n, func) do
    enumerable
    |> Enum.each(func)

    enumerable
    |> cycle(n - 1, func)
  end

  @doc """
  Calls the function with each successive overlapped n-list of elements; returns given enumerable.
  ## Examples
      iex> ["a", "b", "c", "d", "e"]
      iex> |> REnum.each_cons(3, &IO.inspect(&1))
      # ["a", "b", "c"]
      # ["b", "c", "d"]
      # ["c", "d", "e"]
      ["a", "b", "c", "d", "e"]

      iex> %{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}
      iex> |> REnum.each_cons(4, &IO.inspect(&1))
      # [a: 1, b: 2, c: 3, d: 4]
      # [b: 2, c: 3, d: 4, e: 5]
      # [c: 3, d: 4, e: 5, f: 6]
      %{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}
  """
  @spec each_cons(type_enumerable, integer(), function()) :: type_enumerable
  def each_cons(enumerable, n, _) when n < 1, do: enumerable

  def each_cons(enumerable, n, func) do
    if Enum.count(enumerable) >= n do
      [_ | next_els] = enumerable |> Enum.to_list()

      enumerable
      |> Enum.take(n)
      |> func.()

      each_cons(next_els, n, func)
    end

    enumerable
  end

  @doc """
  Calls the function with each element, but in reverse order; returns given enumerable.
  ## Examples
      iex> REnum.reverse_each([1, 2, 3], &IO.inspect(&1))
      # 3
      # 2
      # 1
      [1, 2, 3]
  """
  @spec reverse_each(type_enumerable(), function()) :: type_enumerable()
  def reverse_each(enumerable, func) do
    enumerable
    |> Enum.reverse()
    |> Enum.each(func)

    enumerable
  end

  @doc """
  Returns a map each of whose entries is the key-value pair formed from one of those list.
  ## Examples
      iex> REnum.to_h([[:a, 1], [:b, 2]])
      %{a: 1, b: 2}

      iex> REnum.to_h(a: 1, b: 2)
      %{a: 1, b: 2}
  """
  @spec to_h(type_enumerable()) :: map()
  def to_h(enumerable) do
    if(list_and_not_keyword?(enumerable)) do
      enumerable
      |> Enum.map(&{Enum.at(&1, 0), Enum.at(&1, 1)})
      |> Map.new()
    else
      Map.new(enumerable)
    end
  end

  @doc """
  The function is called with each element.
  The function should return a 2-element tuple which becomes a key-value pair in the returned map.
  ## Examples
      iex> REnum.to_h([[:a, 1], [:b, 2]], fn el ->
      ...>     {REnum.at(el, 0), REnum.at(el, 1)}
      ...>   end)
      %{a: 1, b: 2}

      iex>  REnum.to_h(%{a: 1, b: 2}, fn {key, value} -> {key, value * 2} end)
      %{a: 2, b: 4}
  """
  @spec to_h(type_enumerable(), function()) :: map()
  def to_h(enumerable, func) do
    Map.new(enumerable, func)
  end

  @doc """
  Calls the given function with each element, returns given enumerable:
  ## Examples
      iex> ["a", "b", "c"]
      iex> |> REnum.each_entry(&IO.inspect(&1))
      # "a"
      # "b"
      # "c"
      ["a", "b", "c"]

      iex> %{a: 1, b: 2}
      iex> |> REnum.each_entry(&IO.inspect(&1))
      # {:a, 1}
      # {:b, 2}
      %{a: 1, b: 2}
  """
  @spec each_entry(type_enumerable(), function()) :: type_enumerable()
  def each_entry(enumerable, func) do
    enumerable
    |> Enum.each(func)

    enumerable
  end

  @doc """
  Returns Stream given enumerable sliced by each amount.
  ## Examples
      iex> ["a", "b", "c", "d", "e"]
      iex> |> REnum.each_slice(2)
      iex> |> Enum.to_list()
      [["a", "b"], ["c", "d"], ["e"]]

      iex> %{a: 1, b: 2, c: 3}
      iex> |> REnum.each_slice(2)
      iex> |> Enum.to_list()
      [[a: 1, b: 2], [c: 3]]
  """
  @spec each_slice(type_enumerable(), non_neg_integer()) :: type_enumerable() | atom()
  def each_slice(enumerable, amount) do
    if(amount < 1) do
      []
    else
      enumerable
      |> each_slice(
        0,
        amount
      )
    end
    |> lazy()
  end

  def each_slice(enumerable, start_index, amount_or_func) when is_integer(amount_or_func) do
    sliced =
      enumerable
      |> Enum.slice(start_index, amount_or_func)

    next_start_index = start_index + amount_or_func

    [sliced] ++
      if Enum.count(enumerable) > next_start_index,
        do: each_slice(enumerable, next_start_index, amount_or_func),
        else: []
  end

  @doc """
  Calls the given function with each element, returns given enumerable.
  ## Examples
      iex> ["a", "b", "c", "d", "e"]
      iex> |> REnum.each_slice(2, &IO.inspect(&1))
      # ["a", "b"]
      # ["c", "d"]
      # ["e"]
      :ok

      iex> %{a: 1, b: 2, c: 3}
      iex> |> REnum.each_slice(2, &IO.inspect(&1))
      # [a: 1, b: 2]
      # [c: 3]
      :ok
  """
  @spec each_slice(type_enumerable(), non_neg_integer(), function() | non_neg_integer()) :: atom()
  def each_slice(enumerable, amount, amount_or_func) when is_function(amount_or_func) do
    each_slice(enumerable, amount)
    |> Enum.each(fn els ->
      amount_or_func.(els)
    end)
  end

  @doc """
  Returns Stream, which redefines most Enumerable functions to postpone enumeration and enumerate values only on an as-needed basis.
  ## Examples
      iex> [1, 2, 3]
      iex> |> REnum.lazy()
      iex> |> REnum.to_list()
      [1, 2, 3]
  """
  @spec lazy(type_enumerable()) :: type_enumerable()
  def lazy(enumerable) do
    enumerable
    |> chain([])
    |> Stream.take(Enum.count(enumerable))
  end

  @doc """
  With argument pattern, returns an elements that uses the pattern to partition elements into lists (“slices”).
  An element ends the current slice if element matches pattern.
  With a function, returns an elements that uses the function to partition elements into list.
  An element ends the current slice if its function return is a truthy value.
  ## Examples
      iex> [0, 2, 4, 1, 2, 4, 5, 3, 1, 4, 2]
      iex> |> REnum.slice_after(&(rem(&1, 2) == 0))
      [[0], [2], [4], [1, 2], [4], [5, 3, 1, 4], [2]]

      iex> ["a", "b", "c"]
      iex> |> REnum.slice_after(~r/b/)
      [["a", "b"], ["c"]]
  """
  @spec slice_after(type_enumerable(), function() | type_pattern()) :: type_enumerable()
  def slice_after(enumerable, func) when is_function(func) do
    if(Enum.count(enumerable) < 1) do
      enumerable
    else
      index =
        enumerable
        |> Enum.find_index(func) ||
          Enum.count(enumerable)

      [Enum.slice(enumerable, 0..index)] ++
        slice_after(
          Enum.slice(enumerable, (index + 1)..Enum.count(enumerable)),
          func
        )
    end
  end

  def slice_after(enumerable, pattern) do
    slice_after(
      enumerable,
      match_function(pattern)
    )
  end

  @doc """
  With argument pattern, returns an elements that uses the pattern to partition elements into lists (“slices”).
  An element begins a new slice if element matches pattern. (or if it is the first element).
  With a function, returns an elements that uses the function to partition elements into list.
  An element ends the current slice if its function return is a truthy value.
  ## Examples
      iex> [0, 2, 4, 1, 2, 4, 5, 3, 1, 4, 2]
      iex> |> REnum.slice_before(&(rem(&1, 2) == 0))
      [[0], [2], [4, 1], [2], [4, 5, 3, 1], [4], [2]]

      iex> ["a", "b", "c"]
      iex> |> REnum.slice_before(~r/b/)
      [["a"], ["b", "c"]]
  """
  @spec slice_before(type_enumerable(), function() | type_pattern()) :: type_enumerable()
  def slice_before(enumerable, func) when is_function(func) do
    enumerable
    |> Enum.reverse()
    |> slice_after(func)
    |> Enum.reverse()
    |> Enum.map(&Enum.reverse(&1))
  end

  def slice_before(enumerable, pattern) do
    enumerable
    |> Enum.reverse()
    |> slice_after(match_function(pattern))
    |> Enum.reverse()
    |> Enum.map(&Enum.reverse(&1))
  end

  @doc """
  The returned elements uses the function to partition elements into lists (“slices”).
  It calls the function with each element and its successor.
  Begins a new slice if and only if the function returns a truthy value.
  &1 is current_element and &2 is next_element in function arguments.
  ## Examples
      iex> [1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21]
      iex> |> REnum.slice_when(&(&1 + 1 != &2))
      [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]
  """
  @spec slice_when(type_enumerable(), function() | type_pattern()) :: type_enumerable()
  def slice_when(enumerable, func) do
    if(Enum.count(enumerable) < 1) do
      enumerable
    else
      index =
        enumerable
        |> find_index_with_index(fn el, index ->
          next_el = Enum.at(enumerable, index + 1)
          func.(el, next_el)
        end) ||
          Enum.count(enumerable)

      [Enum.slice(enumerable, 0..index)] ++
        slice_when(
          Enum.slice(enumerable, (index + 1)..Enum.count(enumerable)),
          func
        )
    end
  end

  @doc """
  Returns elements selected by a given pattern or function.
  ## Examples
      iex> ["foo", "bar", "car", "moo"]
      iex> |> REnum.grep(~r/ar/)
      ["bar", "car"]

      iex> 1..10
      iex> |> REnum.grep(3..8)
      [3, 4, 5, 6, 7, 8]
  """
  @spec grep(type_enumerable(), function() | type_pattern()) :: type_enumerable()
  def grep(enumerable, func) when is_function(func) do
    enumerable
    |> select(func)
  end

  def grep(enumerable, pattern) do
    grep(
      enumerable,
      match_function(pattern)
    )
  end

  @doc """
  Calls the function with each matching element and returned.
  ## Examples
      iex> ["foo", "bar", "car", "moo"]
      iex> |> REnum.grep(~r/ar/, &String.upcase(&1))
      ["BAR", "CAR"]

      iex> 1..10
      iex> |> REnum.grep(3..8, &to_string(&1))
      ["3", "4", "5", "6", "7", "8"]
  """
  @spec grep(type_enumerable(), function() | type_pattern(), function()) :: type_enumerable()
  def grep(enumerable, pattern, func) do
    enumerable
    |> grep(pattern)
    |> Enum.map(func)
  end

  @doc """
  Returns elements rejected by a given pattern or function.
  ## Examples
      iex> ["foo", "bar", "car", "moo"]
      iex> |> REnum.grep_v(~r/ar/)
      ["foo", "moo"]

      iex> 1..10
      iex> |> REnum.grep_v(3..8)
      [1, 2, 9, 10]
  """
  @spec grep_v(type_enumerable(), function() | type_pattern()) :: type_enumerable()
  def grep_v(enumerable, pattern) do
    greped = enumerable |> grep(pattern)

    enumerable
    |> Enum.reject(&(&1 in greped))
  end

  @doc """
  Calls the function with each unmatching element and returned.
  ## Examples
      iex> ["foo", "bar", "car", "moo"]
      iex> |> REnum.grep_v(~r/ar/, &String.upcase(&1))
      ["FOO", "MOO"]

      iex> 1..10
      iex> |> REnum.grep_v(3..8, &to_string(&1))
      ["1", "2", "9", "10"]
  """
  @spec grep_v(type_enumerable(), function() | type_pattern(), function()) :: type_enumerable()
  def grep_v(enumerable, pattern, func) do
    enumerable
    |> grep_v(pattern)
    |> Enum.map(func)
  end

  # aliases

  defdelegate detect(enumerable, default, func), to: Enum, as: :find
  defdelegate detect(enumerable, func), to: Enum, as: :find
  defdelegate select(enumerable, func), to: Enum, as: :filter
  defdelegate find_all(enumerable, func), to: Enum, as: :filter
  defdelegate inject(enumerable, acc, func), to: Enum, as: :reduce
  defdelegate inject(enumerable, func), to: Enum, as: :reduce
  defdelegate collect(enumerable, func), to: Enum, as: :map
  defdelegate include?(enumerable, element), to: Enum, as: :member?
  defdelegate collect_concat(enumerable, func), to: Enum, as: :flat_map
  defdelegate entries(enumerable), to: __MODULE__, as: :to_a
  defdelegate each_with_object(enumerable, collectable, func), to: Enum, as: :reduce
  defdelegate each_with_index(enumerable, func), to: Enum, as: :with_index
  defdelegate each_with_index(enumerable), to: Enum, as: :with_index
  defdelegate minmax(enumerable), to: Enum, as: :min_max
  defdelegate minmax(enumerable, func), to: Enum, as: :min_max
  defdelegate minmax_by(enumerable, func), to: Enum, as: :min_max_by
  defdelegate minmax_by(enumerable, func1, func2), to: Enum, as: :min_max_by
  defdelegate minmax_by(enumerable, func1, func2, func3), to: Enum, as: :min_max_by
  defdelegate tally(enumerable), to: Enum, as: :frequencies
  defdelegate chain(enumerables), to: Stream, as: :concat
  defdelegate chain(first, second), to: Stream, as: :concat
  defdelegate to_a(enumerables), to: Enum, as: :to_list
  defdelegate to_l(enumerables), to: Enum, as: :to_list
end