defmodule Bunch.Enum do
@moduledoc """
A bunch of helper functions for manipulating enums.
"""
use Bunch
alias Bunch.Type
@doc """
Generates a list consisting of `i` values `v`.
iex> #{inspect(__MODULE__)}.repeated(:abc, 4)
[:abc, :abc, :abc, :abc]
iex> #{inspect(__MODULE__)}.repeated(:abc, 0)
[]
"""
@spec repeated(v, non_neg_integer) :: [v] when v: any()
def repeated(v, i) when i >= 0 do
do_repeated(v, i, [])
end
defp do_repeated(_v, 0, acc) do
acc
end
defp do_repeated(v, i, acc) do
do_repeated(v, i - 1, [v | acc])
end
@doc """
Generates a list by calling `i` times function `f`.
iex> {:ok, pid} = Agent.start_link(fn -> 0 end)
iex> #{inspect(__MODULE__)}.repeat(fn -> Agent.get_and_update(pid, &{&1, &1+1}) end, 4)
[0, 1, 2, 3]
iex> #{inspect(__MODULE__)}.repeat(fn -> :abc end, 0)
[]
"""
@spec repeat(f :: (() -> a), non_neg_integer) :: [a] when a: any()
def repeat(fun, i) when i >= 0 do
do_repeat(fun, i, [])
end
defp do_repeat(_fun, 0, acc) do
acc |> Enum.reverse()
end
defp do_repeat(fun, i, acc) do
do_repeat(fun, i - 1, [fun.() | acc])
end
@doc """
Splits enumerable into chunks, and passes each chunk through `collector`.
New chunk is created each time `chunker` returns `false`. The `chunker` is passed
current and previous element of enumerable.
## Examples:
iex> #{inspect(__MODULE__)}.chunk_by_prev([1,2,5,5], fn x, y -> x - y <= 2 end)
[[1, 2], [5, 5]]
iex> #{inspect(__MODULE__)}.chunk_by_prev([1,2,5,5], fn x, y -> x - y <= 2 end, &Enum.sum/1)
[3, 10]
"""
@spec chunk_by_prev(Enum.t(), chunker :: (a, a -> boolean), collector :: ([a] -> b)) :: [b]
when a: any(), b: any()
def chunk_by_prev(enum, chunker, collector \\ & &1) do
enum
|> Enum.to_list()
~> (
[h | t] -> do_chunk_by_prev(t, chunker, collector, [[h]])
[] -> []
)
end
defp do_chunk_by_prev([h | t], chunker, collector, [[lh | lt] | acc]) do
do_chunk_by_prev(
t,
chunker,
collector,
if chunker.(h, lh) do
[[h, lh | lt] | acc]
else
[[h], [lh | lt] |> Enum.reverse() |> collector.() | acc]
end
)
end
defp do_chunk_by_prev([], _chunker, collector, [l | acc]) do
[l |> Enum.reverse() |> collector.() | acc] |> Enum.reverse()
end
@doc """
Works like `Enum.reduce/3`, but breaks on error.
Behaves like `Enum.reduce/3` as long as given `fun` returns `{:ok, new_acc}`.
If it happens to return `{{:error, reason}, new_acc}`, reduction is stopped and
the error is returned.
## Examples:
iex> fun = fn
...> x, acc when acc >= 0 -> {:ok, x + acc}
...> _, acc -> {{:error, :negative_prefix_sum}, acc}
...> end
iex> #{inspect(__MODULE__)}.try_reduce([1,5,-2,8], 0, fun)
{:ok, 12}
iex> #{inspect(__MODULE__)}.try_reduce([1,5,-7,8], 0, fun)
{{:error, :negative_prefix_sum}, -1}
"""
@spec try_reduce(Enum.t(), acc, fun :: (a, acc -> result)) :: result
when a: any(), acc: any(), result: Type.stateful_try_t(acc)
def try_reduce(enum, acc, f) do
Enum.reduce_while(enum, {:ok, acc}, fn e, {:ok, acc} ->
case f.(e, acc) do
{:ok, new_acc} -> {:cont, {:ok, new_acc}}
{{:error, reason}, new_acc} -> {:halt, {{:error, reason}, new_acc}}
end
end)
end
@doc """
Works like `Enum.reduce_while/3`, but breaks on error.
Behaves like `Enum.reduce_while/3` as long as given `fun` returns
`{{:ok, :cont | :halt}, new_acc}`. If it happens to return
`{{:error, reason}, new_acc}`, reduction is stopped and the error is returned.
## Examples:
iex> fun = fn
...> 0, acc -> {{:ok, :halt}, acc}
...> x, acc when acc >= 0 -> {{:ok, :cont}, x + acc}
...> _, acc -> {{:error, :negative_prefix_sum}, acc}
...> end
iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,-2,8], 0, fun)
{:ok, 12}
iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,0,8], 0, fun)
{:ok, 6}
iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,-7,8], 0, fun)
{{:error, :negative_prefix_sum}, -1}
"""
@spec try_reduce_while(
Enum.t(),
acc,
reducer :: (a, acc -> Type.stateful_try_t(:cont | :halt, acc))
) :: Type.stateful_try_t(acc)
when a: any(), acc: any()
def try_reduce_while(enum, acc, f) do
Enum.reduce_while(enum, {:ok, acc}, fn e, {:ok, acc} ->
case f.(e, acc) do
{{:ok, :cont}, new_acc} -> {:cont, {:ok, new_acc}}
{{:ok, :halt}, new_acc} -> {:halt, {:ok, new_acc}}
{{:error, reason}, new_acc} -> {:halt, {{:error, reason}, new_acc}}
end
end)
end
@doc """
Works like `Enum.each/2`, but breaks on error.
Behaves like `Enum.each/2` as long as given `fun` returns `:ok`.
If it happens to return `{:error, reason}`, traversal is stopped and the
error is returned.
## Examples:
iex> fun = fn 0 -> {:error, :zero}; x -> send(self(), 1/x); :ok end
iex> #{inspect(__MODULE__)}.try_each([1,2,3], fun)
:ok
iex> #{inspect(__MODULE__)}.try_each([1,0,3], fun)
{:error, :zero}
"""
@spec try_each(Enum.t(), fun :: (a -> result)) :: result
when a: any(), result: Type.try_t()
def try_each(enum, f), do: do_try_each(enum |> Enum.to_list(), f)
defp do_try_each([], _f), do: :ok
defp do_try_each([h | t], f) do
case f.(h) do
:ok -> do_try_each(t, f)
{:error, _error} = error -> error
end
end
@doc """
Works like `Enum.map/2`, but breaks on error.
Behaves like `Enum.map/2` as long as given `fun` returns `{:ok, value}`.
If it happens to return `{:error, reason}`, reduction is stopped and the
error is returned.
## Examples:
iex> fun = fn 0 -> {:error, :zero}; x -> {:ok, 1/x} end
iex> #{inspect(__MODULE__)}.try_map([1,5,-2,8], fun)
{:ok, [1.0, 0.2, -0.5, 0.125]}
iex> #{inspect(__MODULE__)}.try_map([1,5,0,8], fun)
{:error, :zero}
"""
@spec try_map(Enum.t(), fun :: (a -> Type.try_t(b))) :: Type.try_t([b])
when a: any(), b: any()
def try_map(enum, f), do: do_try_map(enum |> Enum.to_list(), f, [])
defp do_try_map([], _f, acc), do: {:ok, acc |> Enum.reverse()}
defp do_try_map([h | t], f, acc) do
case f.(h) do
{:ok, res} -> do_try_map(t, f, [res | acc])
{:error, reason} -> {:error, reason}
end
end
@doc """
Works like `Enum.flat_map/2`, but breaks on error.
Behaves like `Enum.flat_map/2` as long as reducing function returns `{:ok, values}`.
If it happens to return `{:error, reason}`, reduction is stopped and the
error is returned.
## Examples:
iex> fun = fn 0 -> {:error, :zero}; x -> {:ok, [1/x, 2/x, 3/x]} end
iex> #{inspect(__MODULE__)}.try_flat_map([1,5,-2,8], fun)
{:ok, [1.0, 2.0, 3.0, 0.2, 0.4, 0.6, -0.5, -1.0, -1.5, 0.125, 0.25, 0.375]}
iex> #{inspect(__MODULE__)}.try_flat_map([1,5,0,8], fun)
{:error, :zero}
"""
@spec try_flat_map(Enum.t(), fun :: (a -> result)) :: result
when a: any(), b: any(), result: Type.try_t([b])
def try_flat_map(enum, f), do: do_try_flat_map(enum |> Enum.to_list(), f, [])
defp do_try_flat_map([], _f, acc), do: {:ok, acc |> Enum.reverse()}
defp do_try_flat_map([h | t], f, acc) do
case f.(h) do
{:ok, res} -> do_try_flat_map(t, f, res |> Enum.reverse(acc))
{:error, reason} -> {:error, reason}
end
end
@doc """
Works like `Enum.map_reduce/3`, but breaks on error.
Behaves like `Enum.map_reduce/3` as long as given `fun` returns
`{{:ok, value}, new_acc}`. If it happens to return `{{:error, reason}, new_acc}`,
reduction is stopped and the error is returned.
## Examples:
iex> fun = fn
...> x, acc when acc >= 0 -> {{:ok, x+1}, x + acc}
...> _, acc -> {{:error, :negative_prefix_sum}, acc}
...> end
iex> #{inspect(__MODULE__)}.try_map_reduce([1,5,-2,8], 0, fun)
{{:ok, [2,6,-1,9]}, 12}
iex> #{inspect(__MODULE__)}.try_map_reduce([1,5,-7,8], 0, fun)
{{:error, :negative_prefix_sum}, -1}
"""
@spec try_map_reduce(Enum.t(), acc, fun :: (a, acc -> Type.stateful_try_t(b, acc))) ::
Type.stateful_try_t([b], acc)
when a: any(), b: any(), acc: any()
def try_map_reduce(enum, acc, f), do: do_try_map_reduce(enum |> Enum.to_list(), acc, f, [])
defp do_try_map_reduce([], f_acc, _f, acc), do: {{:ok, acc |> Enum.reverse()}, f_acc}
defp do_try_map_reduce([h | t], f_acc, f, acc) do
case f.(h, f_acc) do
{{:ok, res}, f_acc} -> do_try_map_reduce(t, f_acc, f, [res | acc])
{{:error, reason}, f_acc} -> {{:error, reason}, f_acc}
end
end
@doc """
Works like `Enum.each/2`, but breaks on error.
Behaves like `Enum.flat_map_reduce/3` as long as given `fun` returns
`{{:ok, value}, new_acc}`. If it happens to return `{{:error, reason}, new_acc}`,
reduction is stopped and the error is returned.
## Examples:
iex> fun = fn
...> x, acc when acc >= 0 -> {{:ok, [x+1, x+2, x+3]}, x + acc}
...> _, acc -> {{:error, :negative_prefix_sum}, acc}
...> end
iex> #{inspect(__MODULE__)}.try_flat_map_reduce([1,5,-2,8], 0, fun)
{{:ok, [2,3,4,6,7,8,-1,0,1,9,10,11]}, 12}
iex> #{inspect(__MODULE__)}.try_flat_map_reduce([1,5,-7,8], 0, fun)
{{:error, :negative_prefix_sum}, -1}
"""
@spec try_flat_map_reduce(Enum.t(), acc, fun :: (a, acc -> result)) :: result
when a: any(), b: any(), acc: any(), result: Type.stateful_try_t([b], acc)
def try_flat_map_reduce(enum, acc, f),
do: try_flat_map_reduce(enum |> Enum.to_list(), acc, f, [])
defp try_flat_map_reduce([], f_acc, _f, acc), do: {{:ok, acc |> Enum.reverse()}, f_acc}
defp try_flat_map_reduce([h | t], f_acc, f, acc) do
case f.(h, f_acc) do
{{:ok, res}, f_acc} -> try_flat_map_reduce(t, f_acc, f, (res |> Enum.reverse()) ++ acc)
{{:error, reason}, f_acc} -> {{:error, reason}, f_acc}
{:error, reason} -> {{:error, reason}, f_acc}
end
end
@doc """
Works the same way as `Enum.zip/1`, but does not cut off remaining values.
## Examples:
iex> #{inspect(__MODULE__)}.zip_longest([[1, 2] ,[3 ,4, 5]])
[[1, 3], [2, 4], [5]]
It also returns list of lists, as opposed to tuples.
"""
@spec zip_longest(list()) :: list(list())
def zip_longest(lists) when is_list(lists) do
zip_longest_recurse(lists, [])
end
defp zip_longest_recurse(lists, acc) do
{lists, zipped} =
lists
|> Enum.reject(&Enum.empty?/1)
|> Enum.map_reduce([], fn [h | t], acc -> {t, [h | acc]} end)
if zipped |> Enum.empty?() do
Enum.reverse(acc)
else
zipped = zipped |> Enum.reverse()
zip_longest_recurse(lists, [zipped | acc])
end
end
@doc """
Implementation of `Enum.unzip/1` for more-than-two-element tuples.
Size of returned tuple is equal to size of the shortest tuple in `tuples`.
## Examples:
iex> #{inspect(__MODULE__)}.unzip([{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}])
{[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]}
iex> #{inspect(__MODULE__)}.unzip([{1,2,3}, {4,5}, {6,7,8,9}, {10,11,12}])
{[1, 4, 6, 10], [2, 5, 7, 11]}
"""
@spec unzip(tuples :: [tuple()]) :: tuple()
def unzip([]), do: {}
def unzip([h | _] = list) when is_tuple(h) do
do_unzip(
list |> Enum.reverse(),
[] |> repeated(h |> tuple_size())
)
end
defp do_unzip([], acc) do
acc |> List.to_tuple()
end
defp do_unzip([h | t], acc) when is_tuple(h) do
acc = h |> Tuple.to_list() |> Enum.zip(acc) |> Enum.map(fn {t, r} -> [t | r] end)
do_unzip(t, acc)
end
@spec duplicates(Enum.t(), pos_integer) :: list()
@doc """
Returns elements that occur at least `min_occurences` times in enumerable.
Results are NOT ordered in any sensible way, neither is the order anyhow preserved,
but it is deterministic.
## Examples
iex> Bunch.Enum.duplicates([1,3,2,5,3,2,2])
[2, 3]
iex> Bunch.Enum.duplicates([1,3,2,5,3,2,2], 3)
[2]
"""
def duplicates(enum, min_occurences \\ 2) do
enum
|> Enum.reduce({%{}, []}, fn v, {existent, duplicates} ->
{occurrences, existent} = existent |> Map.get_and_update(v, &{&1 || 1, (&1 || 1) + 1})
duplicates = if occurrences == min_occurences, do: [v | duplicates], else: duplicates
{existent, duplicates}
end)
~> ({_, duplicates} -> duplicates)
end
end