Filter nested list of maps by not matching id

Hello, I have problem with comparing two lists with lists of maps. I have such data:

[
  "list1": [
    %{
      id: 12345,
# other fields

    },
    %{
      id: 54321,
# other fields
    }
  ],
  "list 2": [
    %{
      id: 56789
      other_id: 12345,
#other fields
    },
    %{
      id: 0987,
      other_id: 7456,
# other fields
    }
  ]
]

In the first list are id and in the second list there are other_id. What I want to do is get maps where id have no match in any map with other_id and vice versa (if other_id has not matching any identical id in any map in list1).

In above case it should return two maps

    %{
      id: 54321,
# other fields
    },
    %{
      id: 0987,
      other_id: 7456,
# other fields
    }

I’m totally lost in how to do it. I would appreciate a little hint/guidance (best with explanation) how to achieve that.

Here is my generic solution which works with any number of lists and supports all edge cases including missing id and/or other_id fields:

defmodule Example do
  def sample(keyword) when is_list(keyword) do
    # keyword is a list of {atom, any} pairs
    # similar to map, but allows only atom as key and keys does not need to be unique
    keyword
    # reduce every {atom, any} pair over accumulator using 2-arity function
    # our acc is more than empty list - let's exaplin it in main logic
    |> List.foldr({[], [], []}, fn {_key, list}, acc -> merge_lists(list, acc) end)
    # finally we only need a list of maps
    |> then(fn {maps, _ids, _other_ids} -> maps end)
  end

  # main logic
  defp merge_lists(list, acc) do
  	# reduce every map in nested list over same accumulator
  	# we want 3 data to be stored
  	# list of maps, list of ids known so far and list of other_ids known so far
    Enum.reduce(list, acc, fn item, {maps, ids, other_ids} ->
    	# [:atom] notation to avoid edge case i.e. raise if item does not have id
      id = item[:id]
    	# [:atom] notation to avoid edge case i.e. raise if item does not have other_id
      other_id = item[:other_id]

      cond do
      	# if we already saved an item with specified other_id we need to reject it
        id in other_ids -> {Enum.reject(maps, &(&1[:other_id] == id)), ids, other_ids}
      	# if we already saved an item with specified id we need to reject it
        other_id in ids -> {Enum.reject(maps, &(&1[:id] == other_id)), ids, other_ids}
        # edge case: item does not have id and other_id
        # this avoids adding nil to ids and other_ids lists
        is_nil(id) and is_nil(other_id) -> {[item | maps], ids, other_ids}
        # in case id is nil and other_id is not yet saved
        # add item to list of maps and ensure that other_id is included in list of known other_ids
        is_nil(id) -> {[item | maps], ids, [other_id | other_ids]}
        # in case id is not yet saved and other_id is nil
        # add item to list of maps and ensure that id is included in list of known ids
        is_nil(other_id) -> {[item | maps], [id | ids], other_ids}
        # in case id and other_id are not nil and are not yet saved
        # add item to list of maps and ensure that id is included in list of known ids
        #   and other_id is included in list of known other_ids
        true -> {[item | maps], [id | ids], [other_id | other_ids]}
      end
    end)
  end
end

Example.sample([
  list1: [%{id: 12345}, %{id: 54321}],
  "list 2": [%{id: 56789, other_id: 12345}, %{id: 0987, other_id: 7456}]
])

Helpful resources:

  1. Guides |> Patterns and Guards @ Elixir documentation
  2. Enum.reduce/3
  3. Enum.reject/2
  4. Kernel.is_list/1
  5. Kernel.is_nil/1
  6. List.foldr/3
defmodule Example do
  def run do
    list1 = [%{id: 12345}, %{id: 54321}]
    list2 = [%{id: 56789, other_id: 12345}, %{id: 0987, other_id: 7456}]

    # Extract the IDs from each list into sets
    list1_ids = MapSet.new(list1, & &1.id)
    list2_other_ids = MapSet.new(list2, & &1.other_id)

    # Find only the IDs that are not in the other list
    list1_ids_not_in_list2 = MapSet.difference(list1_ids, list2_other_ids)
    list2_other_ids_not_in_list1 = MapSet.difference(list2_other_ids, list1_ids)

    # Select only the items from the original lists corresponding to the above IDs
    list1_with_no_match = Enum.filter(list1, &(&1.id in list1_ids_not_in_list2))
    list2_with_no_match = Enum.filter(list2, &(&1.other_id in list2_other_ids_not_in_list1))

    # Join the lists together
    list1_with_no_match ++ list2_with_no_match
  end
end
iex(1)> Example.run
[%{id: 54321}, %{id: 987, other_id: 7456}]
1 Like