Reputation: 45
I have two lists of maps a and b.
a = [
%{"school" => "a", "class" => 1, "student" => "jane doe"},
%{"school" => "b", "class" => 9, "student" => "jane1 doe"},
%{"school" => "c", "class" => 6, "student" => "jane doe2"}
]
b = [
%{"choice" => "arts", "class" => 1, "school" => "a"},
%{"choice" => "science", "class" => 9, "school" => "a"},
%{"choice" => "maths", "class" => 6, "school" => "b"}
]
I want to be able to compare the two lists and produce a list with items of the following structure
desired_result = [
%{
"school" => "a",
"class" => 1,
"student" => "jane doe" or nil (if student exists only in list b but not in a),
"choices" => ["arts"] or [] (if student exists only in list a but not in b),
"is_common" => yes(if the student exists in both lists) OR only list a OR only list b
}
]
I have tried using the Enum.into and Enum.member? functions and I have been able to achieve 60% of the solution that I want.
Enum.into(a, [], fn item ->
if Enum.member?(b, %{
"school" => item["school"],
"class" => item["class"]
}) do
%{
"school" => item["school"],
"class" => item["class"],
"student" => item["student"],
"choices" => [],
"is_common" => "yes"
}
else
%{
"school" => item["school"],
"class" => item["class"],
"student" => item["student"],
"choices" => [],
"is_common" => "only list a"
}
end
end)
The problem with the above is that it covers the cases of the common ones in both lists and the ones that are only in list a; but it doesn't cover the ones that are only in list b. And also, I couldn't find a way to get the value of choice in my final result from list b (as you can see I left the value of "choice" as []). How to get all three cases covered and get a list in the desired structure with the values?
Upvotes: 2
Views: 958
Reputation: 121000
Let’s start with producing a bare result out of what you have. I assume the pair school
+ class
is what the defines uniquity.
[a, b]
|> Enum.map(fn list ->
Enum.group_by(list, & {&1["class"], &1["school"]})
end)
|> Enum.reduce(
&Map.merge(&1, &2, fn _, [v1], [v2] -> [Map.merge(v1, v2)] end))
|> Enum.map(fn {_, [v]} -> v end)
#⇒ [
# %{"choice" => "arts", "class" => 1, "school" => "a", "student" => "jane doe"},
# %{"choice" => "maths", "class" => 6, "school" => "b"},
# %{"class" => 6, "school" => "c", "student" => "jane doe2"},
# %{"choice" => "science", "class" => 9, "school" => "a"},
# %{"class" => 9, "school" => "b", "student" => "jane1 doe"}
# ]
Feel free to run the above clause by clause to review all the transformations involved.
The list above guarantees the uniqueness by %{"school" => any(), "class" => any()}
amongst list elements. Now simply iterate through and update elements according to your needs.
Upvotes: 2
Reputation: 104
I will go with a different approach, trying to go through both lists using tail recursion.
In order to use this approach we need guarantee that both lists a
and b
will be ordered by the fields that allow us to make the match, in this case school
and class
.
This is needed because during the tail recursion we will be making the match between lists on the fly and it is mandatory to guarantee that if we are leaving an unmatched a
element it is not possible to find an b
match later
# With this both lists will be ordered ascendently by school and class fields.
ordered_a = Enum.sort(a, &((&1["school"] < &2["school"]) || (&1["class"] <= &2["class"] )))
ordered_b = Enum.sort(b, &((&1["school"] < &2["school"]) || (&1["class"] <= &2["class"] )))
With this both list will be ascending ordered by school
and class fields
.
Let's go with the hard part. Now we need to think about going through two ordered lists. The recursion will be done over the match_lists
function.
We can have these 6 possibles pattern match of the headers:
school
and class
fields of the Head
of the two lists are the same, so they make a match. In this case we build the new element and add it to the accumulator. On next call we just pass the tail of both lists.Head
element of a
is ahead Head
element of b
, this is school
field (or class
field if school
is the same) has a bigger value. That means there is no match available for current Head
element of list b
since the list a
is already ahead it. So an unmatched b
element will be built and added to the accumulator. On next call we just passed the tail of b
but the full a list.
a
. The Head
element of list b
is ahead the Head
element of list a
. That means there is no match available for Head
element in a
since Head
in b
is already ahead. An unmatched a
element will be build and added to the accumulator.a
is empty. An unmatched B will generated with the Head
of b
and added to the accumulator.b
is empty. An unmatched A will generated with the Head
of a
and added to the accumulator.def match_lists(a, b, acc \\ [] )
# Case: Element in both lists
def match_lists(
[%{"school" => school, "class" => class, "student" => student} | rest_a],
[%{"school" => school, "class" => class, "choice" => choice} | rest_b],
acc
) do
element = build(school, class, student, [choice], true)
match_lists(rest_a, rest_b, [element | acc])
end
# Case: Element only in list B case. So it is a B case
def match_lists(
[%{"school" => school_a, "class" => class_a} | _] = a,
[%{"school" => school_b, "class" => class_b, "choice" => choice} | rest_b],
acc
)
when school_a > school_b or class_a > class_b do
element = build(school_b, class_b, nil, [choice], "only_list_b")
match_lists(a, rest_b, [element | acc])
end
# Case: No more elementes in A. So It is a B case
def match_lists([], [%{"school" => school, "class" => class, "choice" => choice} | rest_b], acc) do
element = build(school, class, nil, [choice], "only_list_b")
match_lists([], rest_b, [element | acc])
end
# Case: Element only in list A
def match_lists(
[%{"school" => school_a, "class" => class_a, "student" => student} | rest_a],
[%{"school" => school_b, "class" => class_b} | _] = b,
acc
)
when school_b > school_a or class_b > class_a do
element = build(school_a, class_a, student, [], "only_list_a")
match_lists(rest_a, b, [element | acc])
end
# Case: No more elementes in B. So It is an uncommon A case
def match_lists([%{"school" => school, "class" => class, "student" => student} | rest_a], [], acc) do
element = build(school, class, student, [], "only_list_a")
match_lists(rest_a, [], [element | acc])
end
def match_lists([], [], acc) do
acc
end
defp build(school, class, student, choices, is_common) do
%{
"school" => school,
"class" => class,
"student" => student,
"choices" => choices,
"is_common" => is_common
}
end
iex(1)> match_lists(ordered_a, ordered_b)
[
%{
"choices" => [],
"class" => 6,
"is_common" => "only_list_a",
"school" => "c",
"student" => "jane doe2"
},
%{
"choices" => [],
"class" => 9,
"is_common" => "only_list_a",
"school" => "b",
"student" => "jane1 doe"
},
%{
"choices" => ["maths"],
"class" => 6,
"is_common" => "only_list_b",
"school" => "b",
"student" => nil
},
%{
"choices" => ["science"],
"class" => 9,
"is_common" => "only_list_b",
"school" => "a",
"student" => nil
},
%{
"choices" => ["arts"],
"class" => 1,
"is_common" => true,
"school" => "a",
"student" => "jane doe"
}
]
Hope it helps.
Upvotes: 0