Arithmetic Trainer Round 3 - Generating Problems on Arbitrary Intervals

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 and a * 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 and 1.

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.

Read more