Packages
iteraptor
0.3.2
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.
**Parameters**
- input: nested map/list/keyword to be flattened.
- joiner: the character to be used to join keys while flattening,
_optional_, default value is `"."`;
e.g. `%{a: {b: 42}}` will be flattened to `%{"a.b" => 42}`.
**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: 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]}}
**Parameters**
- input: flat map to be “expanded” to nested maps/lists.
- joiner: the character to be used to “un-join” keys while flattening,
_optional_, default value is `"."`;
e.g. `%{"a.b" => 42}` will be unveiled to `%{a: {b: 42}}`.
**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`.
**Parameters**
- input: nested map/list/keyword to be walked through.
- joiner: the character to be used to join keys while flattening,
is returned to the callback as iterated key name;
_optional_, default value is `"."`;
- fun: callback to be called on each _value_;
e.g. on `%{a: {b: 42}}` will be called once, with tuple `{"a.b", 42}`.
**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
if Keyword.keyword?(input) do
input |> Enum.into(%{}) |> process(joiner, prefix, acc, fun)
else
input
|> Enum.with_index
|> Enum.map(fn({k, v}) -> {v, k} end)
|> Enum.into(%{})
|> process(joiner, prefix, acc, fun)
end
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, "", joiner) do
s = to_string l
if s |> String.contains?(joiner) do
s
else
String.to_atom s
end
end
defp join("", r, joiner) do
join(r, "", joiner)
end
defp join(l, r, joiner) do
to_string 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