denis.peplin
denis.peplin

Reputation: 9841

How to "intersperse" two lists by a key value?

I have two lists of maps and I want to form a list that would contain values from both lists basing on some common values.

list1 = [
  %{email: "email1", name: "name1"},
  %{email: "email2", name: "name2"}
]

list2 = [
  %{"email" => "email3", "name" => "new name3"},
  %{"email" => "email2", "name" => "new name2"}
]

The lists may be different size, one of them may even be empty.

The desired output would be:

[
  {%{email: "email1", name: "name1"}, nil},
  {%{email: "email2", name: "name2"},
   %{"email" => "email2", "name" => "new name2"}},
  {nil, %{"email" => "email3", "name" => "new name3"}}
]

Here is an implementation I came up with:

  # to produce the output above, call it with
  # list_intersperse(list1, list2, :email, "email")
  def list_intersperse(list1, list2, key1, key2) do
    map2 = Enum.map(list2, fn elem -> {elem[key2], elem} end) |> Map.new()

    result =
      Enum.map(list1, fn elem ->
        common_value = Map.get(elem, key1)
        {elem, map2[common_value]}
      end)

    keys1 = Enum.map(list1, fn elem -> Map.get(elem, key1) end) |> MapSet.new()

    result ++
      (list2
       |> Enum.reject(&MapSet.member?(keys1, &1[key2]))
       |> Enum.map(&{nil, &1}))
  end

The implementation feels sub-optimal to me and it's hard to read. Am I missing something, could it be done in a more concise and more readable way?

Upvotes: 1

Views: 321

Answers (2)

denis.peplin
denis.peplin

Reputation: 9841

The accepted answer works perfectly for the data set I've posted in the question, but it turned out that my data set has some duplicated data on both sides.

For example:

list1 = [
  %{email: nil, name: "nil1"},
  %{email: nil, name: "nil2"},
  %{email: "email1", name: "name1"},
  %{email: "email2", name: "name2"}
]

list2 = [
  %{"email" => "email3", "name" => "new name3"},
  %{"email" => "email2", "name" => "new name2"},
  %{"email" => "email2", "name" => "new name2"}
]

In the first list there are two items with duplicated keys and both of them nil, and in the second list there are two keys with the same values equals to email2.

I need to keep all these values in the resulting data set, so mapping by key won't work.

What I ended up doing is using List.myers_difference function and transforming its results into the list I want to see:

def list_intersperse(list1, list2, key1, key2) do
  list1 = Enum.sort_by(list1, &Map.get(&1, key1))
  list2 = Enum.sort_by(list2, &Map.get(&1, key2))

  List.myers_difference(list1, list2, fn elem1, elem2 ->
    if Map.get(elem1, key1) == Map.get(elem2, key2), do: {elem1, elem2}
  end)
  |> Enum.map(&myers_to_intersperse/1)
  |> List.flatten()
end

defp myers_to_intersperse({:del, list}), do: Enum.map(list, &{&1, nil})
defp myers_to_intersperse({:ins, list}), do: Enum.map(list, &{nil, &1})
defp myers_to_intersperse({:diff, tuple}), do: tuple

The code above gives the result I wanted:

[
  {%{email: nil, name: "nil1"}, nil},
  {%{email: nil, name: "nil2"}, nil},
  {%{email: "email1", name: "name1"}, nil},
  {%{email: "email2", name: "name2"}, %{"email" => "email2", "name" => "new name2"}},
  {nil, %{"email" => "email2", "name" => "new name2"}},
  {nil, %{"email" => "email3", "name" => "new name3"}}
]

Upvotes: 0

Adam Millerchip
Adam Millerchip

Reputation: 23091

How about this?

def pair(list1, list2) do
  map1 = Map.new(list1, fn item -> {item.email, item} end)
  map2 = Map.new(list2, fn item -> {item["email"], item} end)

  all_emails = MapSet.new(Map.keys(map1) ++ Map.keys(map2))
  for email <- all_emails, do: {map1[email], map2[email]}
end

Usage:

iex(1)> Example.pair(list1, list2)
[
  {%{email: "email1", name: "name1"}, nil},
  {%{email: "email2", name: "name2"},
   %{"email" => "email2", "name" => "new name2"}},
  {nil, %{"email" => "email3", "name" => "new name3"}}
]

Upvotes: 3

Related Questions