How to compare 2 maps by a particular param?

I have a quiz app where user’s can complete tests, I am trying to calculate their test score.

I have two maps:

1. test_submission
(A map of answers submitted by a particular user)

%{"student_number_1" => "A", "student_number_2" => "C", "student_number_3" => "B"

2. questions
(A list of maps containing question numbers and correct answers)

[
%{question_number: 1, correct_answer: "A"},
%{question_number: 2, correct_answer: "C"},
%{question_number: 3, correct_answer: "C"}
]

The maps in questions will always be sorted in the list by question_number. And question_number will always correspond to student_number in test_submission. For example:

student_number_1 in test_submission will always be referring to question_number: 1 in questions

With that being said, I want to receive the number of correct answers submitted by the user.

In the example above, it should be 2. Because they got Q1 and Q2 correct, but Q3 was wrong.

The number of questions will always differ, but the structure will remain the same.

How would I achieve this?

You can look at Enum.zip_with/3.

This will zip through both enumerables, and then you can just compare

Enum.zip_with(test_submissions, questions, fn {_, answer}, %{correct_answer: correct_answer} -> 
  answer == correct_answer
end)

# Result
[true, true, false]

Are you guaranteed those map keys are always present and in order?

I’d reduce over the questions and Map.fetch the answer each time.

1 Like

Ahh true, Maps loose their ordering after a certain number

This works, but I am not sure how much I like the solution :man_shrugging:

Enum.map(test_submissions, fn {"student_number_" <> student_number, answer} -> 
  question = Enum.find(questions, & to_string(&1.question_number) == student_number)
  answer  == question.correct_answer
end)

My favorite way to approach problems like this is to split them into smaller problems by transforming the data until the solution is straightforward to assemble.

The trick is to develop an understanding of what shapes make useful things “straightforward”.

In many problems with “related data” - signified by ideas like “x refers to y” - it’s helpful to transform the inputs into maps, since maps natively support a “look up the corresponding value” operation.

The thing that relates the elements of a test submission and the question list is the question number. So let’s consider transforming both into maps keyed by the question number.

For the submission, we want to strip out the student_number_ part and convert the question number to an integer to match the questions input.

mapped_submission =
  Map.new(test_submissions, fn {"student_number_" <> n_str, answer} ->
    {String.to_integer(n_str), answer}
  end)

For the sample input, this results in %{1 => "A", 2 => "C", 3 => "B"}

Then for the questions:

mapped_questions =
  Map.new(questions, fn %{question_number: n, correct_answer: a} ->
    {n, a}
  end)

For the sample input, this gives %{1 => "A", 2 => "C", 3 => "C"}

(the above can also be spelled Map.new(questions, &{&1.question_number, &1.correct_answer}) for folks who don’t like giant fn heads)

After this transformation, the problem is simplified: given two maps with the same shape, how many elements in the first agree with elements in the second?

This seems straightforward with the sample inputs, but things get trickier with potentially-malicious input. For instance, what should happen if the submission contains extra answers not in the list of questions? What if some answers are not submitted?

For concreteness, I’ve chosen these answers to those questions:

  • submitted answers outside the valid question numbers will be ignored
  • missing answers will be counted as incorrect

The best way to avoid headaches with out-of-range question numbers is to only ever look up things using a known-good question number (one from the list of questions), so mapped_questions controls the looping here:

got_right =
  Map.new(mapped_questions, fn {n, correct} ->
    {n, Map.get(mapped_submissions, n) == correct}
  end)

With the sample input, this returns %{1 => true, 2 => true, 3 => false}. If you wanted to distinguish between “got the answer wrong” and “did not submit the answer”, this is where that change would live.

Finally, all that’s left to do is count all the desired elements of got_right, which are tuples of {integer, true | false}:

count_right = Enum.count(got_right, &elem(&1,1))

This returns 2 for the sample input, as expected.

3 Likes
def total_correct(questions, test_submission) do
  Enum.reduce(questions, 0, fn question, correct_answers ->
    %{question_number: num, correct_answer: ans} = question
    student_answer = test_submission["student_number_#{num}"]

   if student_answer == ans, do: acc + 1, else: acc
  end)
end
2 Likes