Arithmetic Trainer Round 3 - Generating Problems on Arbitrary Intervals
The previous two posts discussed Arithmetic Trainer from zero to an initial prototype and implementing multiple configurable training activities. However, all questions are generated on the interval 2 to 99. This post revisits the Arithmetic Trainer project and discusses question generation on arbitrary intervals.
For example, training on the interval from -20 to 20 will look like this:
Starting 10 Problems Test on Mixed (+-/*) On Interval From -20 To 20.
-15 + 2 = ?
-13
Correct. Problems Remaining: 9
-13 - 1 = ?
-14
Correct. Problems Remaining: 8
-19 - (-2) = ?
-17
Correct. Problems Remaining: 7
9 * 1 = ?
9
Correct. Problems Remaining: 6
-19 - (-8) = ?
-11
Correct. Problems Remaining: 5
-13 * (-1) = ?
13
Correct. Problems Remaining: 4
0 / (-11) = ?
0
Correct. Problems Remaining: 3
-1 * 19 = ?
-19
Correct. Problems Remaining: 2
8 * 0 = ?
0
Correct. Problems Remaining: 1
-3 * 1 = ?
-3
Correct. Problems Remaining: 0
Finish Time: 00:20 (including 00:00 penalty time)
Try Again? [y/n] (y): n
The project’s source code is available on GitHub here
- The starting state of the project: here.
- The final state of the project: here.
- The pull request with all the changes: here.
Intervals
The goal is to have questions with parameters from arbitrary intervals, yet the codebase lacks a class to represent an interval.
The first commit fixes it by introducing the Range
class to represent any collection of parameters and the Interval
class as a specific type of range. While currently only intervals are used, some of non-interval ranges planned for the future include unions of multiple intervals and positive integers with no prime factors besides 2
and 3
.
By design, intervals are inclusive and cannot have start point be greater than end point, which is verified by unit tests.
Problems
How to ensure that every generated problem is correct? "Correct" meaning that
- All problem parameters are within the specified interval.
- The answer is correct.
- The question presentation includes brackets around negative numbers, e.g. "2 + (-2) = ?" instead of "2 + -2 = ?"
The current representation of problems is a record with two string properties: question and answer, which does not lend itself to checking for correctness:
public record Problem(string Question, string Answer);
A possible solution is to subclass the Problem
class for every type of problem. The second commit lays the groundwork for this. It makes the Problem
record into an abstract class and includes a temporary GenericProblem
class for backwards compatibility:
public abstract class Problem
{
public abstract string Question { get; }
public abstract string Answer { get; }
}
public sealed class GenericProblem(string question, string answer) : Problem
{
public override string Question => question;
public override string Answer => answer;
}
The third commit implements addition on interval problems. The implementation has two notable features:
- It includes unit tests, which check all correctness conditions.
- Similarly to the intervals, the constructor throws an exception instead of proceeding to create an invalid object. This ensures that all successfully instantiated objects are correct.
The fourth commit proceeds in the same way for all other operations. One unique case is checking for division by zero in DivisionOnIntervalProblem
, while the rest is fairly repetitive.
Problem Generators
By now, problems on arbitrary intervals can be created manually, and invalid problems cannot be created. Next is problem generation.
Similarly to problems, the first step is to identify what makes a problem generator correct. Put simply, a correct problem generator can generate problems. This means:
- If there are no problems on the specified interval, the problem generator fails to get created (similarly to intervals and problems).
- A successfully created problem generator never fails to generate a problem.
Addition
The fifth commit represents the above two conditions in unit tests for addition problems. All of these unit tests are currently failing. The sixth commit implements the logic to pass them.
Here is how it works. The minimum possible sum of two numbers on interval [a,b]
is a+a
but it cannot be lower than a
. Likewise the maximum possible sum is b+b
but not higher than b
. This leads to the sum generation as follows:
int result = Random.Next(
Math.Max(Range.From + Range.From, Range.From),
Math.Min(Range.To + Range.To, Range.To) + 1);
And the generation is impossible precisely when the lower bound is greater than the upper bound:
if (Math.Max(interval.From + interval.From, interval.From) > Math.Min(interval.To + interval.To, interval.To))
{
throw new ArgumentException("Invalid interval");
}
Subtraction
The seventh commit implements the subtraction problems generation, which is analogous to addition. Now the minimum result on the interval [a, b]
is a - b
while the maximum is b - a
. The generator correctness condition becomes:
if (Math.Max(interval.From, interval.From - interval.To) > Math.Min(interval.To, interval.To - interval.From))
{
throw new ArgumentException("Invalid interval");
}
Multiplication
The eight commit implements multiplication problem generation.
There are only two cases when generation on interval [a, b]
is not possible:
- All numbers are negative, i.e.
b < 0
, because the product of two negatives is positive. - All numbers are large and positive, i.e.
0 < a
anda * a > b
.
The generation is split into three cases:
- All numbers are positive, e.g. interval from 2 to 99.
- The maximum number is
0
. - The interval includes both
0
and1
.
To see that these cases are exhaustive, exclude the first two cases and impossible cases, what's left is the third case.
The implementation also replaces while(true)
loop with MaxIterations
because the newly introduced tests made the original algorithm stuck in the infinite loop. In practice, the MaxIterations
limit is highly unlikely to get exhausted.
Division
The ninth commit implements division problem generation, which is analogous to multiplication, except it has one extra invalid interval - the zero interval since 0 / 0 = 0
is invalid unlike 0 * 0 = 0
.
Problem Collections
Previously known as TrainingMode
, the class is reworked into ProblemCollection
in the tenth commit. This makes for a more specific name because a problem collection represents, well, a collection of problems. The ProblemCollection
class also abstracts away the selection of the next problem generator to generate the next problem.
Here is the new look of the problem collection selection:
Choose Problem Collection:
> Mixed (+-/*) On Interval From 2 To 99
Mixed (+-/*) On Interval From -20 To 20
Addition & Subtraction On Interval From -999 To 999
Cancel
Conclusion
This post has revisited the Arithmetic Trainer project and showcased problem generation on arbitrary intervals, which makes Arithmetic Trainer even more configurable.