Packages

A collection of extra utilities and extensions to the Elixir standard library

Current section

Files

Jump to
extra lib enum extra.ex
Raw

lib/enum/extra.ex

defmodule Enum.Extra do
@moduledoc """
Extensions to the standard library's Enum module.
"""
@doc """
Runs the `fn` for each element in the `enum`, or short circuits if
`fn` returns an error for any given item, returning the error.
"""
@spec each_or_error(Enum.t, (term -> {:error, reason} | any)) ::
:ok | {:error, reason} when reason: any
def each_or_error(enum, func) do
Enum.into(enum, [])
|> do_each_or_error(func)
end
defp do_each_or_error([], _), do: :ok
defp do_each_or_error([item | rest], func) do
case func.(item) do
{:error, _} = err -> err
_ -> do_each_or_error(rest, func)
end
end
@doc """
Runs the `func` over every value in the `Enum.t`, returning (`{:ok, Enum.t}`) if all the funcs
return {:ok, term}.
Short circuits if `func` does not return {:ok, term}.
## Examples
iex> Enum.Extra.map_or_error(%{a: 1, b: 2}, fn {key, val} -> {:ok, {key, val * 2}} end)
{:ok, %{a: 2, b: 4}}
iex> Enum.Extra.map_or_error([a: 1, b: 2], fn {key, val} -> {:ok, {key, val * 2}} end)
{:ok, [a: 2, b: 4]}
iex> Enum.Extra.map_or_error([1, 2], fn val -> {:ok, val * 2} end)
{:ok, [2, 4]}
## Options
* `:into`: if passed, the passed Enum.t will be collected into `:into`.
By default, this function will attempt to push the Enum.t into the same
structure that was passed in (a `map` or `list`).
"""
@spec map_or_error(Enum.t, (term -> {:ok, term} | {:error, reason}), [{:into, Collectable.t}]) ::
{:ok, Enum.t} | {:error, reason} when reason: any
def map_or_error(enum, func, opts \\ []) do
collectible =
Keyword.get_lazy(opts, :into, fn -> collectible_for(enum) end)
Enum.to_list(enum)
|> do_map_or_error(func)
|> case do
result when is_list(result) ->
collected =
Enum.into(result, collectible)
{:ok, collected}
{:error, _} = err ->
err
end
end
defp do_map_or_error(keyword, func, acc \\ [])
defp do_map_or_error([], _func, acc), do: acc |> Enum.reverse()
defp do_map_or_error([next | rest], func, acc) do
with {:ok, result} <- func.(next) do
do_map_or_error(rest, func, [result | acc])
end
end
defp collectible_for(enum) when is_list(enum), do: []
defp collectible_for(enum) when is_map(enum), do: %{}
@doc """
Behaves like `Enum.reduce/3` but will short-circuit if
`fun` returns an error tuple.
## Examples
iex> Enum.Extra.reduce_or_error(%{a: 1, b: 2}, 0, fn {_key, val}, count -> {:ok, val + count} end)
{:ok, 3}
iex> Enum.Extra.reduce_or_error([1, 2], 0, fn _val, _acc -> {:error, :fail} end)
{:error, :fail}
"""
@spec reduce_or_error(Enum.t, any, (Enum.element, any -> {:ok, any} | {:error, any}))
:: {:ok, any}
| {:error, any}
def reduce_or_error(enum, acc, fun) do
do_reduce_or_error(Enum.to_list(enum), acc, fun)
end
defp do_reduce_or_error([], acc, _), do: {:ok, acc}
defp do_reduce_or_error([item | rest], acc, fun) do
case fun.(item, acc) do
{:ok, acc} -> do_reduce_or_error(rest, acc, fun)
{:error, _} = err -> err
end
end
@doc """
Returns a `Map.t` where the elements in `list` are indexed by the value returned by calling
`index_fn` on each element. The last writer wins in this implementation.
## Examples
iex> txs = [%{id: "A", amt: 10_000}, %{id: "B", amt: 15_000}]
...> txs |> Enum.Extra.index_by(& &1.id)
%{"A" => %{id: "A", amt: 10_000}, "B" => %{id: "B", amt: 15_000}}
"""
@spec index_by([map], (map -> any)) :: %{any => map}
def index_by(list, index_fn),
do: Enum.reduce(list, %{}, &Map.put(&2, index_fn.(&1), &1))
@doc """
Applies `mapping_fun` if `predicate` is not `false` or `nil`, otherwise returns `enum`.
## Examples
iex> Enum.Extra.map_if([1, 2, 3], true, &(&1 + 1))
[2, 3, 4]
iex> Enum.Extra.map_if([1, 2, 3], false, &(&1 + 1))
[1, 2, 3]
"""
@spec map_if(Enum.t, boolean | nil, (any -> any)) :: Enum.t
def map_if(enum, predicate, mapping_fun) when is_nil(predicate) or predicate == false and is_function(mapping_fun, 1),
do: enum
def map_if(enum, _predicate = true, mapping_fun) when is_function(mapping_fun, 1),
do: Enum.map(enum, mapping_fun)
@doc """
Returns whether `enum` is unique.
Note that this implementation will stop enumerating as soon as it finds a non-unique entry (returning `false`)
making it more efficient than simply comparing `Enum.uniq(enum)` to the original `enum`.
## Examples
iex> Enum.Extra.unique?([1, 2, 3])
true
iex> Enum.Extra.unique?([1, 1, 2])
false
"""
@spec unique?(Enum.t) :: boolean
def unique?(enum) do
unique_reducer = fn (item, seen) ->
case MapSet.member?(seen, item) do
true -> {:halt, false}
false -> {:cont, MapSet.put(seen, item)}
end
end
case Enumerable.reduce(enum, {:cont, MapSet.new()}, unique_reducer) do
{:done, _} -> true
{:halted, _} -> false
end
end
@doc """
Returns the given enum with the keys in `keymap` renamed to their corresponding values in
`keymap`.
Keys in `keymap` that don't exist in the passed enum will be ignored
## Examples
iex> Enum.Extra.rename_keys([foo: "bar", baz: "qux"], %{foo: :new_foo, a: :b})
[new_foo: "bar", baz: "qux"]
iex> Enum.Extra.rename_keys(%{foo: "bar", baz: "qux"}, %{foo: :new_foo, a: :b})
%{new_foo: "bar", baz: "qux"}
iex> Enum.Extra.rename_keys([foo: "bar"], %{a: :b})
[foo: "bar"]
"""
@spec rename_keys(Enum.t, keymap :: map) :: keyword
def rename_keys(enum, keymap) do
enum
|> Enum.map(fn {key, val} = element ->
case Map.fetch(keymap, key) do
{:ok, new_key} -> {new_key, val}
:error -> element
end
end)
|> Enum.into(Monoid.identity(enum))
end
end