veerreddy.s
veerreddy.s

Reputation: 45

Compare two lists of maps and return a new transformed list (Elixir)

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

Answers (2)

Aleksei Matiushkin
Aleksei Matiushkin

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

jkmrto
jkmrto

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:

  1. [MATCH] The 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.
  2. [UNMATCHED B] 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.
  3. [UNMATCHED A] Same that point 2 but respect to 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.
  4. [UNMATCHED B] The list a is empty. An unmatched B will generated with the Head of band added to the accumulator.
  5. [UNMATCHED A] The list b is empty. An unmatched A will generated with the Head of a and added to the accumulator.
  6. [END] Both list are empty. The recursion has ended and the accumulator will be returned.
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

Related Questions