defmodule REnum.Enumerable.Ruby do @moduledoc """ Summarized all of Ruby 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.Enumerable.Ruby`. """ @spec __using__(any) :: list defmacro __using__(_opts) do REnum.Utils.define_all_functions!(__MODULE__) end @type type_enumerable :: Enumerable.t() @type type_pattern :: number() | String.t() | Range.t() | Regex.t() import REnum.Enumerable.Support # https://ruby-doc.org/core-3.1.0/Enumerable.html # ruby_enumerable = [: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] # |> Enum.reject(fn method -> # Enum.module_info()[:exports] # |> Keyword.keys() # |> Enum.find(&(&1 == method)) # end) # ✔ 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 result |> is_tuple() -> result |> Tuple.to_list() true -> result end end @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]] """ @spec first(type_enumerable) :: type_enumerable() def first(enumerable, n) do 0..(n - 1) |> Enum.with_index(fn _, index -> enumerable |> Enum.at(index) end) |> compact() |> Enum.map(fn el -> if(el |> is_tuple, do: el |> Tuple.to_list(), else: el) end) 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 """ Returns all elements. ## Examples iex> REnum.to_a([1, 2, 3]) [1, 2, 3] iex> REnum.to_a(%{:a => 1, 1 => :a, 3 => :b, :b => 5}) [ [1, :a], [3, :b], [:a, 1], [:b, 5] ] iex> REnum.to_a(0..5) [0, 1, 2, 3, 4, 5] iex> REnum.to_a(a: 1, b: 2, c: 2, d: 4) [{:a, 1}, {:b, 2}, {:c, 2}, {:d, 4}] """ @spec to_a(type_enumerable()) :: list() def to_a(enumerable) do cond do range?(enumerable) -> enumerable |> Enum.to_list() is_map(enumerable) -> enumerable |> Enum.map(fn {k, v} -> [k, v] end) true -> enumerable |> Enum.to_list() end 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(is_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 """ Returns an Stream generated from given enumerables. ## Examples iex> ["a", "b", "c"] iex> |> REnum.chain(["d", "e"]) iex> |> REnum.to_list() ["a", "b", "c", "d", "e"] iex> %{a: 1, b: 2} iex> |> REnum.chain(1..3) iex> |> REnum.to_list() [{:a, 1}, {:b, 2}, 1, 2, 3] """ @spec chain(type_enumerable(), type_enumerable()) :: Stream def chain(enumerable_1, enumerable_2) do Stream.concat([enumerable_1, enumerable_2]) 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 """ 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"] ["a", "b", "c", "d", "e"] iex> %{a: 1, b: 2, c: 3} iex> |> REnum.each_slice(2, &IO.inspect(&1)) # [a: 1, b: 2] # [c: 3] %{a: 1, b: 2, c: 3} """ @spec each_slice(type_enumerable(), non_neg_integer(), function()) :: type_enumerable() def each_slice(enumerable, amount, func) do enumerable |> each_slice( 0, amount, func ) enumerable end defp each_slice(enumerable, start_index, amount, func) do enumerable |> Enum.slice(start_index, amount) |> func.() next_start_index = start_index + amount if(Enum.count(enumerable) > next_start_index) do each_slice(enumerable, next_start_index, amount, func) end end @doc """ Returns an 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() """ @spec lazy(type_enumerable()) :: Stream 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 @doc """ Returns a map containing the counts of equal elements. ## Examples iex> ~w(a c d b c a) iex> |> REnum.tally() %{"a" => 2, "c" => 2, "d" => 1, "b" => 1} iex> [a: 1, b: 2, c: 3, c: 3] iex> |> REnum.tally() %{{:a, 1} => 1, {:b, 2} => 1, {:c, 3} => 2} """ def tally(enumerable) do enumerable |> Enum.group_by(& &1) |> Enum.map(fn el -> {elem(el, 0), elem(el, 1) |> Enum.count()} end) |> Map.new() 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, func), to: Enum, as: :member? defdelegate collect_concat(enumerable, func), to: Enum, as: :flat_map defdelegate entries(enumerable), to: __MODULE__, as: :to_a # TODO: add info defdelegate each_with_object(enumerable, object, 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 end