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 idand/orother_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}]
])
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