Packages
iteraptor
0.2.4
1.15.0
1.14.0
1.13.1
1.13.0
1.12.2
1.12.1
1.12.0
1.11.0
1.10.3
1.10.2
1.10.1
1.9.0
1.8.0
1.7.3
1.7.2
1.7.1
1.7.0
1.6.1
1.6.0
1.4.1
1.4.0
1.3.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc1
0.7.0
0.6.2
0.6.1
0.5.2
0.5.1
0.5.0
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.1.1
0.1.0
This small library allows the deep iteration / mapping of Enumerables.
Current section
Files
Jump to
Current section
Files
lib/iteraptor.ex
defmodule Iteraptor do
@moduledoc """
`Iteraptor` makes complicated nested structures (currently `map`s and `list`s)
iteration easier.
"""
@joiner "."
@doc """
Build a flatmap out of nested structure, concatenating the names of keys.
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
%{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
Lists are handled gracefully, index is used as a key in resulting map.
**More examples:**
iex> [:a, 42] |> Iteraptor.to_flatmap
%{"0": :a, "1": 42}
iex> %{a: 42} |> Iteraptor.to_flatmap
%{a: 42}
iex> %{a: 42, b: 42} |> Iteraptor.to_flatmap
%{a: 42, b: 42}
iex> %{a: %{b: 42}, d: 42} |> Iteraptor.to_flatmap
%{"a.b": 42, d: 42}
iex> %{a: [:b, 42], d: 42} |> Iteraptor.to_flatmap
%{"a.0": :b, "a.1": 42, d: 42}
iex> %{a: %{b: [:c, 42]}, d: 42} |> Iteraptor.to_flatmap
%{"a.b.0": :c, "a.b.1": 42, d: 42}
iex> %{a: %{b: 42}} |> Iteraptor.to_flatmap
%{"a.b": 42}
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.to_flatmap
%{"a.b.c": 42}
iex> %{a: %{b: %{c: 42}}, d: 42} |> Iteraptor.to_flatmap
%{"a.b.c": 42, d: 42}
iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
%{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
"""
def to_flatmap(input, joiner \\ @joiner) when is_map(input) or is_list(input) do
process(input, joiner)
end
@doc """
Build a nested structure out of a flatmap given, decomposing the names of keys
and handling lists carefully.
%{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
**More examples:**
iex> %{"a.b.c": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42}}}
iex> %{"a.b.c": 42, "a.b.d": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: 42}}}
iex> %{"a.b.c": 42, "a.b.d": 42, "a.e": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: 42}, e: 42}}
iex> %{"0": 42, "1": 42} |> Iteraptor.from_flatmap
[42, 42]
iex> %{"1": :a1, "0": :a0, "2": :a2, "3": :a3, "4": :a4, "5": :a5,
...> "6": :a6, "7": :a7, "8": :a8, "9": :a9, "10": :a10, "11": :a11}
...> |> Iteraptor.from_flatmap
[:a0, :a1, :a2, :a3, :a4, :a5, :a6, :a7, :a8, :a9, :a10, :a11]
iex> %{"0.a": 42, "0.b": 42} |> Iteraptor.from_flatmap
[%{a: 42, b: 42}]
iex> %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
...> |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
"""
def from_flatmap(input, joiner \\ @joiner) when is_map(input) do
unprocess(input, joiner)
end
@doc """
Iterates the given nested structure, calling the callback provided on each
value. The key returned is a concatenated names of all the parent keys
(and/or indices in a case of an array.)
The return value is the result of call to `to_flatmap`.
**More examples:**
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.each(fn {k, v} -> IO.inspect({k, v}) end)
%{"a.b.c": 42}
"""
def each(input, joiner \\ @joiner, fun) do
unless is_function(fun, 1), do: raise "Function or arity fun/1 is required"
process(input, joiner, "", %{}, fun)
end
##############################################################################
defp process(input, joiner, prefix \\ "", acc \\ %{}, fun \\ nil)
### -----------------------------------------------------------------------###
defp process(input, joiner, prefix, acc, fun) when is_map(input) do
input |> Enum.reduce(acc, fn({k, v}, memo) ->
prefix = join(prefix, k, joiner)
if is_map(v) or is_list(v) do
process(v, joiner, prefix, memo, fun)
else
unless is_nil(fun), do: fun.({prefix, v})
Map.put memo, prefix, v
end
end)
end
defp process(input, joiner, prefix, acc, fun) when is_list(input) do
input
|> Enum.with_index
|> Enum.map(fn({k, v}) -> {v, k} end)
|> Enum.into(%{})
|> process(joiner, prefix, acc, fun)
end
##############################################################################
defp unprocess(input, joiner, fun \\ nil)
### -----------------------------------------------------------------------###
defp unprocess(input, joiner, fun) when is_map(input) do
input
|> Enum.reduce(%{}, fn({key, value}, acc) ->
put_or_update(acc, joiner, key, value, fun)
end)
|> imply_lists(joiner)
end
##############################################################################
defp join(l, r \\ "", joiner \\ @joiner)
defp join(l, "", _) do
String.to_atom(to_string(l))
end
defp join("", r, _) do
String.to_atom(to_string(r))
end
defp join(l, r, joiner) do
String.to_atom(to_string(l) <> joiner <> to_string(r))
end
##############################################################################
@lint [{Credo.Check.Refactor.Nesting, false}, {Credo.Check.Refactor.ABCSize, false}]
defp put_or_update(input, joiner, prefix, value, fun, path \\ "") when is_map(input) do
case prefix |> to_string |> String.split(joiner, parts: 2) do
[key, rest] ->
{_, target} = input |> Map.get_and_update(join(key), fn current ->
old = case current do
nil -> %{}
_ -> current
end
{current, old |> put_or_update(joiner, rest, value, fun, join(path, key, joiner))}
end)
target
[key] ->
unless is_nil(fun), do: fun.({path, value})
cond do
is_map(input) -> input |> Map.put(join(key), value)
is_list(input) -> input ++ [value]
true -> input # FIXME raise ??
end
end
end
##############################################################################
defp parse_key(key, joiner, prefix) do
k = key
|> to_string
|> String.split(joiner)
|> Enum.at((prefix |> String.split(joiner) |> Enum.count) - 1)
try do
k |> String.to_integer
rescue
ArgumentError -> k
end
end
defp filter_keys(input, prefix) do
case prefix do
"" -> input
_ -> input |> Enum.filter(fn e -> e |> to_string |> String.starts_with?(to_string(prefix)) end)
end
end
defp quacks_as_list(input, joiner, prefix \\ "") do
input = input |> Map.keys |> filter_keys(prefix)
(input
|> Enum.map(fn k ->
k |> parse_key(joiner, prefix)
end)
|> Enum.sort) == (0..Enum.count(input) - 1 |> Enum.to_list)
end
defp imply_lists(input, joiner) when is_map(input) do
if quacks_as_list(input, joiner) do
sorted = input
|> Enum.sort(fn ({k1, _}, {k2, _}) ->
String.to_integer(to_string(k1)) < String.to_integer(to_string(k2))
end)
for {_, v} <- sorted do
if is_map(v), do: imply_lists(v, joiner), else: v
end
else
Enum.into(for {k, v} <- input do
{k, (if is_map(v), do: imply_lists(v, joiner), else: v)}
end, %{})
end
end
end