Reputation: 19410
I have two list of maps that look like this:
list_one = [
%{id: :a, value: 1},
%{id: :b, value: 2},
%{id: :c, value: 3}
]
list_two = [
%{id: :a, value: 1},
%{id: :b, value: 4},
%{id: :d, value: 5}
]
and I know the following:
id
s are unique in each listid
s might appear in both listsid
that does not appear in the otherI wish to merge those in a single map, with the values from the inner map's id
as key and the two values if present, or a "null value" (let's say 0) if one of the two lists does not contain one of the ids
(this last thing is optional). The desired output for the above example would be:
%{
a: %{
value_one: 1,
value_two: 1
},
b: %{
value_one: 2,
value_two: 4
},
c: %{
value_one: 3,
value_two: 0
},
d: %{
value_one: 0,
value_two: 5
}
}
I know I could do this by doing a couple of Enum.reduce
but it feels like I'm missing something easier
Upvotes: 0
Views: 297
Reputation: 23091
This is a refactored version of Nezteb's answer using Enum.zip_reduce/4
, which is a pretty cool approach but just needed to be stripped down to the basics with the help of put_in/3
and Access.key/2
. Note that this only works if the two lists have the same number of items.
Enum.zip_reduce(list_one, list_two, %{}, fn %{id: id_one, value: value_one},
%{id: id_two, value: value_two},
acc ->
acc
|> put_in([Access.key(id_one, %{value_two: 0}), :value_one], value_one)
|> put_in([Access.key(id_two, %{value_one: 0}), :value_two], value_two)
end)
Here is a version that uses a similar approach, but without zip_reduce
, to do it in a single pass of both lists. This works even if the lists are of different length:
a =
for %{id: id, value: value} <- list_one, reduce: %{} do
acc -> put_in(acc, [Access.key(id, %{value_two: 0}), :value_one], value)
end
for %{id: id, value: value} <- list_two, reduce: a do
acc -> put_in(acc, [Access.key(id, %{value_one: 0}), :value_two], value)
end
Upvotes: 0
Reputation: 56
I like Adam's answer the most, but since you said "no maps in either list have more or less keys" and mentioned Enum.reduce
I figured I'd offer an answer that took both into account! The following solution will only work for two lists, but it is another option. 😄
list_one = [
%{id: :a, value: 1},
%{id: :b, value: 2},
%{id: :c, value: 3}
]
list_two = [
%{id: :a, value: 1},
%{id: :b, value: 4},
%{id: :d, value: 5}
]
labels = [:value_one, :value_two]
# Helper function for building a single map out of a list of maps
build_map_with_label = fn acc, map_from_list, label ->
%{id: id, value: value} = map_from_list
{_, acc} =
Map.get_and_update(acc, id, fn current_value ->
map_from_list =
if current_value,
do: current_value,
else: %{}
{current_value, Map.put(map_from_list, label, value)}
end)
acc
end
# Zip reduce two lists using our map builder helper to produce a single map
Enum.zip_reduce(list_one, list_two, %{}, fn left, right, acc ->
acc
|> build_map_with_label.(left, Enum.at(labels, 0))
|> build_map_with_label.(right, Enum.at(labels, 1))
end)
# This second pass fills in any missing keys with 0
|> Enum.reduce(%{}, fn {id, id_map}, acc ->
new_id_map =
Enum.reduce(labels, id_map, fn label, acc ->
Map.put_new(acc, label, 0)
end)
Map.put(acc, id, new_id_map)
end)
|> IO.inspect()
Which prints:
%{
a: %{value_one: 1, value_two: 1},
b: %{value_one: 2, value_two: 4},
c: %{value_one: 3, value_two: 0},
d: %{value_one: 0, value_two: 5}
}
Upvotes: 1
Reputation: 23091
I would first convert the lists into maps of the desired shape using Map.new/2
, and then merge them together using Map.merge/3
:
def merge(list_one, list_two) do
a =
Map.new(list_one, fn %{id: id, value: value_one} ->
{id, %{value_one: value_one, value_two: 0}}
end)
b =
Map.new(list_two, fn %{id: id, value: value_two} ->
{id, %{value_one: 0, value_two: value_two}}
end)
Map.merge(a, b, fn _id, %{value_one: value_one}, %{value_two: value_two} ->
%{value_one: value_one, value_two: value_two}
end)
end
Output:
%{
a: %{value_one: 1, value_two: 1},
b: %{value_one: 2, value_two: 4},
c: %{value_one: 3, value_two: 0},
d: %{value_one: 0, value_two: 5}
}
Upvotes: 3
Reputation: 121000
First, you need to transform lists into maps for instant O(log N)
access later.
[map_one, map_two] =
Enum.map([list_one, list_two], fn list ->
for %{id: id, value: value} <- list, into: %{}, do: {id, value}
end)
Then, you need to get all the keys upfront, otherwise, there is no way to figure out where we need zeroes added.
keys =
list_one
|> Kernel.++(list_two)
|> get_in([Access.all(), :id])
|> Enum.uniq()
Now we are all set to build the result up.
for k <- keys, into: %{} do
{k,
%{number_one: Map.get(map_one, k, 0),
number_two: Map.get(map_two, k, 0)}}
end
Giving us back the desired
%{
a: %{number_one: 1, number_two: 1},
b: %{number_one: 2, number_two: 4},
c: %{number_one: 3, number_two: 0},
d: %{number_one: 0, number_two: 5}
}
Upvotes: 1