0
votes

Let's say that I have a couple of lists with the same key id inside:

[
 %{id: 1, total: 10},
 %{id: 2, total: 20},
 %{id: 3, total: 30}
]

and

[
 %{id: 1, name: "what", age: 23},
 %{id: 2, name: "pro", age: 56},
 %{id: 3, name: "rider", age: 25}
]

How could I get the following list?

[
 %{id: 1, total: 10, name: "what", age: 23},
 %{id: 2, total: 20, name: "pro", age: 56},
 %{id: 3, total: 30, name: "rider", age: 25}
]

Thanks!

3

3 Answers

1
votes

Here is one way:

iex(1)> totals = [
...(1)>  %{id: 1, total: 10},
...(1)>  %{id: 2, total: 20},
...(1)>  %{id: 3, total: 30}
...(1)> ]
[%{id: 1, total: 10}, %{id: 2, total: 20}, %{id: 3, total: 30}]
iex(2)> entries = [
...(2)>  %{id: 1, name: "what", age: 23},
...(2)>  %{id: 2, name: "pro", age: 56},
...(2)>  %{id: 3, name: "rider", age: 25}
...(2)> ]
[
  %{age: 23, id: 1, name: "what"},
  %{age: 56, id: 2, name: "pro"},
  %{age: 25, id: 3, name: "rider"}
]
iex(3)> for entry <- entries do
...(3)>   Map.merge(entry, Enum.find(totals, & &1.id == entry.id))
...(3)> end
[
  %{age: 23, id: 1, name: "what", total: 10},
  %{age: 56, id: 2, name: "pro", total: 20},
  %{age: 25, id: 3, name: "rider", total: 30}
]

Note it assumes you will always have a matching entry on both lists.

2
votes

I would index one of the lists for faster matching to the maps in the other list:

maps1 = [
 %{id: 1, total: 10},
 %{id: 2, total: 20},
 %{id: 3, total: 30}
]

indexed_maps1 = for map <- maps1, into: %{} do
  {map[:id], map}
end 

IO.inspect indexed_maps1

maps2 = [
 %{id: 1, name: "what", age: 23},
 %{id: 2, name: "pro", age: 56},
 %{id: 3, name: "rider", age: 25}
]

Enum.map(maps2, &(Map.merge(&1, indexed_maps1[&1[:id]] )))

Output:

%{1 => %{id: 1, total: 10}, 2 => %{id: 2, total: 20}, 3 => %{id: 3, total: 30}}

[
  %{age: 23, id: 1, name: "what", total: 10},
  %{age: 56, id: 2, name: "pro", total: 20},
  %{age: 25, id: 3, name: "rider", total: 30}
]

If the lists are in order, then you can do:

  def merge(list1, list2), do: merge(list1, list2, [])

  defp merge([map1|tail1], [map2|tail2], result) do
    merge(tail1, tail2, [Map.merge(map1, map2)|result])
  end
  defp merge([], [], result), do: Enum.reverse(result)
0
votes

For the sake of completeness, I’d post more erlangish solution that uses bare recursion.

def merge_am(elems, totals, acc \\ []) do
  elems
  |> Enum.sort_by(& &1.id)
  |> do_merge_am(Enum.sort_by(totals, & &1.id), acc)
  |> Enum.reverse()
end

defp do_merge_am([], [], acc), do: acc
defp do_merge_am([], list, acc), do: Enum.reverse(list) ++ acc
defp do_merge_am(list, [], acc), do: Enum.reverse(list) ++ acc

defp do_merge_am([%{id: id} = he | te], [%{id: id} = ht | tt], acc),
  do: do_merge_am(te, tt, [Map.merge(he, ht) | acc])

defp do_merge_am([%{id: ide} = he | te], [%{id: idt} = ht | tt], acc)
  when ide < idt, do: do_merge_am(te, [ht | tt], [he | acc])

defp do_merge_am([%{id: _ide} = he | te], [%{id: _idt} = ht | tt], acc),
  do: do_merge_am([he | te], tt, [ht | acc])

The idea is to walk through both lists, picking up the next closest by id item, wherever it’s found. If both lists have it, Map.merge/2 is called (3rd clause,) otherwise the closest by id is prepended to the result. The approach requires the lists to be sorted upfront.

It might seem a bit verbose, but it wins the benchmark battle (slightly against @7stud’s solution and drastically beating José’s one :)

defmodule Merger.Bench do
  use Benchfella

  @entries for i <- 1..1000, do: %{id: i, total: 10 * i}
  @totals for i <- 1..1000,
    do: %{id: i, name: "Name_#{i}", age: Enum.random(18..100)}

  bench("7s", do: Merger.merge_7s(@entries, @totals))
  bench("jv", do: Merger.merge_jv(@entries, @totals))
  bench("am", do: Merger.merge_am(@entries, @totals))
end

Resulting in:

Settings:
  duration:      1.0 s

## Merger.Bench
[06:27:16] 1/3: 7s
[06:27:20] 2/3: am
[06:27:22] 3/3: jv

Finished in 7.39 seconds

## Merger.Bench
ben iterations   average time 
am        5000   355.31 µs/op
7s        5000   511.49 µs/op
jv         100   18755.67 µs/op