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.