I have a list I want to chunk up based on a transition from struct type B to A. So for example, I have the following:
iex(1)> defmodule A, do: defstruct []
{:module, A ...
iex(2)> defmodule B, do: defstruct []
{:module, B ...
iex(3)> values = [ %A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{} ]
[%A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{}]
I want to have that data chunked up into a 2-element list containing:
[ [ %A{}, %A{}, %B{}, %B{}, %B{} ], [ %A{}, %A{}, %B{} ] ]
If the input were to be all A's or all B's initially, the output would be unchanged, since no B->A transition occurred.
I imagine Enum.chunk_by/2
is the way to go, but I'm having trouble figuring out how to maintain the context of the previous element to know when to split.
What does an idiomatic solution to something like this look like?
Another alternative is to chunk_by the struct type then do another pass merging the lists (except when the list contains %B{}
):
def chunk(structs) do
structs
|> Enum.chunk_by(& &1.__struct__)
|> merge()
end
# Don't merge when current is %B
defp merge([[%B{}|_]=h|t]), do: [h|merge(t)]
# Merge all others
defp merge([curr, next|t]), do: [curr ++ next|merge(t)]
# We are done
defp merge([]), do: []
Yet another approach is to use pure recursion:
def collect_chunks([]), do: []
def collect_chunks(list) do
{chunk, post_chunk} = collect_chunk(list)
[chunk | collect_chunks(post_chunk)]
end
defp collect_chunk([]), do: {[], []}
defp collect_chunk([%B{} = last_element | [%A{} | _] = post_chunk]), do: {[last_element], post_chunk}
defp collect_chunk([el | rest]) do
{remaining_chunk, post_chunk} = collect_chunk(rest)
{[el | remaining_chunk], post_chunk}
end
Enum.chunk_by/2
currently does not provide access to the previous element so we can not use Enum.chunk_by/2
in this case. We will have to fallback to reduce/3
Of all the Enum
functions, reduce/3
is the most flexible and is used internally by most, if not all, of the Enum
functions.
Below is one way to go about producing the output you want, given the values [ %A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{} ]
:
values
|> Enum.reduce([[]], fn (elem, acc) ->
prev_list = List.first(acc)
prev_elem = List.first(prev_list)
b_changed_to_a? = fn -> prev_elem.__struct__ == B && elem.__struct__ == A end
if is_nil(prev_elem) || !b_changed_to_a?.() do
List.replace_at(acc, 0, [elem|prev_list])
else
[[elem]|acc]
end
end)
|> Enum.map(&Enum.reverse/1)
|> Enum.reverse
Notice that I always prepend an element to a list. This is because appending to a list in Elixir is an expensive operation.
Hope this solution helps!