Agos
Agos

Reputation: 19410

Convert a list of maps to a single map using key from inner map

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:

I 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

Answers (4)

Adam Millerchip
Adam Millerchip

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

Nezteb
Nezteb

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

Adam Millerchip
Adam Millerchip

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

Aleksei Matiushkin
Aleksei Matiushkin

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

Related Questions