How to merge two list with maps based on particular key

For two list of maps

products =
[
  %{id: 7, name: "A", count: 1},
  %{id: 8, name: "B", count: 1},
  %{id: 9, name: "C", count: 0}
]

price=
[
  %{price: "$14.95", p_id: 8},
  %{price: "$10.00", p_id: 7},
  %{price: "$29.95", p_id: 10},
  %{price: "$1.00", p_id: 9}

]

How can we combine/merge the two ,for the final list to look like

products=
[
  %{id: 7, name: "A", count: 1, price: "$10.00"},
  %{id: 8, name: "B", count: 1, price: "$14.95"},
  %{id: 9, name: "C", count: 0, price: "$1.00"}
]

??

Try Enum.map along with Enum.find

Can you please elaborate?? I am new to this… :slightly_smiling_face:

Here is one example which takes the following steps:

  1. Sort each list by its ID
  2. Merge the second list into the first
defmodule Merge do

  # Assumes the lists are sorted
  def run do
    products = [
      %{id: 7, name: "A", count: 1},
      %{id: 8, name: "B", count: 1},
      %{id: 9, name: "C", count: 0}
    ]

    price = [
      %{price: "$10.00", p_id: 7},
      %{price: "$14.95", p_id: 8},
      %{price: "$1.00", p_id: 9},
      %{price: "$29.95", p_id: 10}
    ]

    merge_lists(products, price, &Map.merge/2)
  end

  def merge_lists([], _, _merger) do
    []
  end

  # A match so we merge the two maps and advance
  # each list
  def merge_lists([%{id: id} = head_a | rest_a], [%{p_id: id} = head_b | rest_b], merger) do
    [merger.(head_a, head_b) | merge_lists(rest_a, rest_b, merger)]
  end

  # No match but there are more items in list_b we can try
  def merge_lists([%{id: id_a} | _rest_a] = list_a, [%{p_id: id_b}| rest_b], merger)
      when id_a > id_b do
    merge_lists(list_a, rest_b, merger)
  end

  # No merge possible = No matching list b
  def merge_lists([%{id: id_a} = head_a | _rest_a], [%{p_id: id_b} | _rest_b], _merger)
      when id_a < id_b do
    raise ArgumentError, "No match for #{inspect head_a}"
  end
end

And running it:

iex> Merge.run
[
  %{count: 1, id: 7, name: "A", p_id: 7, price: "$10.00"},
  %{count: 1, id: 8, name: "B", p_id: 8, price: "$14.95"},
  %{count: 0, id: 9, name: "C", p_id: 9, price: "$1.00"}
]
2 Likes

Thank you…

Enum.map(products, fn p -> 
  relevant_price  = Enum.find(price, fn rp -> rp.p_id == p.id end)
  p |> Map.put_new(:price, relevant_price.price)
end)
1 Like

@sreyansjain’s solution is expressive and the intent is easy to understand. It has the challenge that Enum.find/2 will be called for each entry in products and since Enum.find/2 is a linear search that has a potential impact.

My solution has only pass over the lists but requires that the lists are sorted first.

Another solution would be to change the list of maps into a list for both price and products. Then cross-linking them would be quite efficient.

WIth Elixir I typically find that a process of get the data in the right shape for processing -> process the data -> put the data in the right shape for returning to be a strategy that holds up over time and decouples the processing stage from potential changes to data structures over time. That may only be my opinion of course.

3 Likes

@sreyansjain , @kip
I am using
Enum.map(products, &Map.put(&1, :price, Enum.find(price, fn %{p_id: pid} -> pid == &1.id end).price))
But it raises an error if id is not present in the list. How to use Enum.find_value for the same?`

Enum.map(products, fn p -> 
  relevant_price  = Enum.find(price, fn rp -> rp.p_id == p.id end)
  if relevant_price do
    p |> Map.put_new(:price, relevant_price.price)
  else
    p |> Map.put_new(:price, 0)
  end
end)
products =
[
  %{id: 7, name: "A", count: 1},
  %{id: 8, name: "B", count: 1},
  %{id: 9, name: "C", count: 0}
]

price=
[
  %{price: "$14.95", id: 8},
  %{price: "$10.00", id: 7},
  %{price: "$29.95", id: 10},
  %{price: "$1.00", id: 9}

]

Enum.group_by(products++price, & Map.get(&1, :id))
|> Enum.map(fn {_, l} ->
  Enum.concat(l)
  |> Enum.into(%{}) 
end)
1 Like