Current section
Files
Jump to
Current section
Files
lib/funcio/enum/select/select.ex
defmodule Plymio.Funcio.Enum.Select do
@moduledoc ~S"""
Selecting Specific Elements from an Enum.
See `Plymio.Funcio` for overview and documentation terms.
"""
import Plymio.Funcio.Predicate,
only: [
reduce_and_predicate1_funs: 1
]
import Plymio.Funcio.Enum.ValueAt,
only: [
fetch_value_at_enum: 2,
delete_value_at_enum: 2
]
@type error :: Plymio.Funcio.error()
@type stream :: Plymio.Funcio.stream()
@doc ~S"""
`filter_enum/2` takes a *enum* as its first argument.
If the second argument is a *predicate/1*, it
filters the *enum* (`Stream.filter/2`) returning
`{:ok, stream}`.
Otherwise the second argument is assumed to be a *index range* and
`Plymio.Funcio.Enum.ValueAt.fetch_value_at_enum/2` is called.
Note: the filtered list is
returned in the **order of the indices** (so out of order and
duplicates are allowed)
## Examples
iex> {:ok, stream} = [1,2,3] |> filter_enum(&is_integer/1)
...> stream |> Enum.to_list
[1,2,3]
iex> {:ok, stream} = [1,2,3] |> filter_enum(0)
...> stream |> Enum.to_list
[1]
iex> {:ok, stream} = [1,2,3] |> filter_enum([-1, -2, 0, 1, 2])
...> stream |> Enum.to_list
[3, 2, 1, 2, 3]
iex> {:ok, stream} = [1,2,3] |> filter_enum(nil)
...> stream |> Enum.to_list
[1, 2, 3]
iex> {:ok, stream} = [1,:b,3] |> filter_enum(&is_integer/1)
...> stream |> Enum.to_list
[1,3]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> filter_enum(fn {_k,v} -> is_integer(v) end)
...> stream |> Enum.to_list
[a: 1, b: 2, c: 3]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> filter_enum([
...> fn {_k,v} -> is_integer(v) end,
...> fn {_k,v} -> v > 1 end,
...> ])
...> stream |> Enum.to_list
[b: 2, c: 3]
iex> {:ok, stream} = %{:a => 1, :b => 2, "c" => 3}
...> |> filter_enum(fn {k,_v} -> is_atom(k) end)
...> stream |> Enum.to_list
[a: 1, b: 2]
iex> {:ok, stream} = 42 |> filter_enum(&(&1))
...> try do
...> stream |> Enum.to_list
...> rescue
...> error -> error
...> end
...> |> Exception.message
...> |> String.starts_with?("protocol Enumerable not implemented for 42")
true
iex> {:error, error} = [1,2,3] |> filter_enum(:not_a_fun)
...> error |> Exception.message
"index range invalid, got: :not_a_fun"
"""
@since "0.1.0"
@spec filter_enum(any, any) :: {:ok, list} | {:ok, stream} | {:error, error}
def filter_enum(enum, filter_or_index_range)
def filter_enum(state, filter) do
filter
|> reduce_and_predicate1_funs
|> case do
{:ok, fun} ->
{:ok, state |> Stream.filter(fun)}
# try an index range
{:error, %{__struct__: _}} = _predicate_result ->
state |> fetch_value_at_enum(filter)
end
end
@doc ~S"""
`reject_enum/2` takes a *enum* as its first argument.
If the second argument is a *predicate/1*, it
rejects the *enum* (`Stream.reject/2`) returning
`{:ok, stream}`.
Otherwise the second argument is assumed to be a *index range* and
`Plymio.Funcio.Enum.ValueAt.delete_value_at_enum/2` is called.
## Examples
iex> {:ok, stream} = [1,2,3] |> reject_enum(&is_integer/1)
...> stream |> Enum.to_list
[]
iex> {:ok, stream} = [1,2,3] |> reject_enum(0)
...> stream |> Enum.to_list
[2, 3]
iex> {:ok, stream} = [1,2,3] |> reject_enum([-1, -2, 0, 1, 2])
...> stream |> Enum.to_list
[]
iex> {:ok, stream} = [1,:b,3] |> reject_enum(&is_integer/1)
...> stream |> Enum.to_list
[:b]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> reject_enum(fn {_k,v} -> v > 2 end)
...> stream |> Enum.to_list
[a: 1, b: 2]
iex> {:ok, stream} = %{:a => 1, :b => 2, "c" => 3}
...> |> reject_enum(fn {k,_v} -> is_atom(k) end)
...> stream |> Enum.to_list
[{"c", 3}]
iex> {:ok, stream} = 42 |> reject_enum(&(&1))
...> try do
...> stream |> Enum.to_list
...> rescue
...> error -> error
...> end
...> |> Exception.message
...> |> String.starts_with?("protocol Enumerable not implemented for 42")
true
iex> {:error, error} = [1,2,3] |> reject_enum(:not_a_fun)
...> error |> Exception.message
"index range invalid, got: :not_a_fun"
"""
@since "0.1.0"
@spec reject_enum(any, any) :: {:ok, list} | {:ok, stream} | {:error, error}
def reject_enum(enum, filter_or_index_range)
def reject_enum(state, filter) do
filter
|> reduce_and_predicate1_funs
|> case do
{:ok, fun} ->
{:ok, state |> Stream.reject(fun)}
# try an index range
{:error, %{__struct__: _}} = _predicate_result ->
state |> delete_value_at_enum(filter)
end
end
@doc ~S"""
`filter_with_index_enum/2` takes an *enum* and a *index tuple predicate/1*.
It first passes the *enum* through `Stream.with_index/1` before
calling `Stream.filter/2`. The *index tuple predicate/1* is called
with a 2tuple: `{value, index}` and, if the return is *truthy*, the
2tuple is used as the result.
On sucess, the result will be `{:ok, stream}` where `stream` is a `Stream`.
## Examples
iex> {:ok, stream} = [:a,:b,:c]
...> |> filter_with_index_enum(fn {v,_i} -> is_atom(v) end)
...> stream |> Enum.to_list
[{:a, 0}, {:b, 1}, {:c, 2}]
iex> {:ok, stream} = [:a,:b,:c]
...> |> filter_with_index_enum(fn {_v,i} -> i > 0 end)
...> stream |> Enum.to_list
[{:b, 1}, {:c, 2}]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> filter_with_index_enum([
...> fn {{_k,v}, _i} -> is_integer(v) end,
...> fn {{_k,v}, _i} -> v > 1 end,
...> fn {{_k,_v}, i} -> i > 1 end,
...> ])
...> stream |> Enum.to_list
[{{:c, 3}, 2}]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> filter_with_index_enum(fn {{_k,_v},i} -> i > 1 end)
...> stream |> Enum.to_list
[{{:c, 3}, 2}]
iex> {:ok, stream} = %{:a => 1, :b => 2, "c" => 3}
...> |> filter_with_index_enum(fn {{k,_v}, _i} -> is_atom(k) end)
...> stream |> Enum.to_list
[{{:a, 1}, 0}, {{:b, 2}, 1}]
iex> {:ok, stream} = 42 |> filter_with_index_enum(&(&1))
...> try do
...> stream |> Enum.to_list
...> rescue
...> error -> error
...> end
...> |> Exception.message
...> |> String.starts_with?("protocol Enumerable not implemented for 42")
true
iex> {:error, error} = [1,2,3] |> filter_with_index_enum(:not_a_fun)
...> error |> Exception.message
"predicate/1 function invalid, got: :not_a_fun"
"""
@since "0.1.0"
@spec filter_with_index_enum(any, any) :: {:ok, list} | {:ok, stream} | {:error, error}
def filter_with_index_enum(enum, filter)
def filter_with_index_enum(state, filter) do
with {:ok, fun} <- filter |> reduce_and_predicate1_funs do
try do
{:ok, state |> Stream.with_index() |> Stream.filter(fun)}
rescue
error ->
{:error, error}
end
else
{:error, %{__exception__: true}} = result -> result
end
end
@doc ~S"""
`reject_with_index_enum/2` takes an *enum* and a *index tuple predicate/1*.
It first passes the *enum* through `Stream.with_index/1` before
calling `Enum.reject/2`.
The *index tuple predicate/1* is called with a 2tuple: `{value,
index}` and, if the return is *truthy*, the 2tuple is rejected.
On sucess, the result will be `{:ok, stream}` where `stream` is a `Stream`.
## Examples
iex> {:ok, stream} = [1,2,3]
...> |> reject_with_index_enum(fn {v,_i} -> is_integer(v) end)
...> stream |> Enum.to_list
[]
iex> {:ok, stream} = [1,:b,3]
...> |> reject_with_index_enum(fn {v,_i} -> is_integer(v) end)
...> stream |> Enum.to_list
[{:b, 1}]
iex> {:ok, stream} = [a: 1, b: 2, c: 3]
...> |> reject_with_index_enum(fn {{_k,_v}, i} -> i > 1 end)
...> stream |> Enum.to_list
[{{:a, 1}, 0}, {{:b, 2}, 1}]
iex> {:ok, stream} = %{:a => 1, :b => 2, "c" => 3}
...> |> reject_with_index_enum(fn {{k,_v}, _i} -> is_atom(k) end)
...> stream |> Enum.to_list
[{{"c", 3}, 2}]
iex> {:ok, stream} = 42 |> reject_with_index_enum(&(&1))
...> try do
...> stream |> Enum.to_list
...> rescue
...> error -> error
...> end
...> |> Exception.message
...> |> String.starts_with?("protocol Enumerable not implemented for 42")
true
iex> {:error, error} = [1,2,3] |> reject_with_index_enum(:not_a_fun)
...> error |> Exception.message
"predicate/1 function invalid, got: :not_a_fun"
"""
@since "0.1.0"
@spec reject_with_index_enum(any, any) :: {:ok, list} | {:error, error}
def reject_with_index_enum(enum, filter)
def reject_with_index_enum(state, filter) do
with {:ok, fun} <- filter |> reduce_and_predicate1_funs do
try do
{:ok, state |> Stream.with_index() |> Stream.reject(fun)}
rescue
error ->
{:error, error}
end
else
{:error, %{__exception__: true}} = result -> result
end
end
end