3. Relative Equality

3.1. Definition

The GDSC Test library contains assertion functionality for relative equality between numbers.

The implementation of relative equality expresses the difference \(\delta\) between \(a\) and \(b\):

\[\delta = |a-b|\]

relative to the magnitude of \(a\) and/or \(b\).

A symmetric relative error is:

\[e = \frac { |a-b| } { \max(|a|, |b|) }\]

This term is identical if \(a\) and \(b\) are switched.

The equivalent asymmetric relative error \(e\) is:

\[e_a = \frac { |a-b| } { |a| }\]

This is the distance that \(b\) is from \(a\), relative to \(a\).

3.2. Testing Relative Equality

A simple test for relative equality may check that the equality is below a maximum threshold \(e_{max}\). For example a symmetric test of relative equality can be:

\[\frac { |a-b| } { \max(|a|, |b|) } \leq e_{max}\]

Note that this will fail if both values are zero due to division by zero. It can be rearranged to avoid this:

\[|a-b| \leq \max(|a|, |b|) \times e_{max}\]

The expression is a test that the distance between two values is less than an error, relative to the largest magnitude. This is a test for convergence of two values.

The equivalent asymmetric test:

\[|a-b| \leq |a| \times e_{max}\]

is a test that the distance between two values is less than an error, relative to \(a\). Since the test is asymmetric it can be used when \(a\) is the known (expected) value and \(b\) is an actual value to be tested, i.e. test if the actual value is close to the expected value using a relative error.

3.3. Value Around Zero

The relative equality uses the magnitude of the values to define the scale.

\[ \begin{align}\begin{aligned}e &= \frac { |a-b| } { max(|a|,|b|) }\\e_a &= \frac { |a-b| } { |a| }\end{aligned}\end{align} \]

In the limit \(|a| = 0\) then \(e_a = \infty\) but \(e = 1\).

The asymmetric relative error is thus unbounded.

The limit for the symmetric relative error is 2 when \(a\) and \(b\) are of equal magnitudes and opposite signs, e.g.

\[ \begin{align}\begin{aligned}a &= 1\\b &= -1\\e &= \frac { |a-b| } { \max(|a|, |b|) }\\e &= \frac { 2 } { 1 }\end{aligned}\end{align} \]

This allows the relative error argument to be checked that it falls within the allowed range \(0 \leq e \leq 2\).

3.4. Testing Close to Zero

The relative error increases as one of the test values approaches zero. The following demonstrates this for the symmetric relative error:

\[ \begin{align}\begin{aligned}a &= 0.01\\b &= 0.00001\\e &= \frac { |a-b| } { \max(|a|, |b|) }\\e &= \frac { 0.00999 } { 0.01 }\\e &= 0.999\end{aligned}\end{align} \]

The relative error \(e\) will be large (\(0.999\)) although the absolute difference \(\delta\) will be small (\(0.00999\)).

This can be handled by allowing the \(\delta\) to be tested against a relative error threshold \(rel_{max}\) or an absolute error threshold \(abs_{max}\) :

\[ \begin{align}\begin{aligned}|a-b| &\leq \max(|a|, |b|) \times rel_{max}\\|a-b| &\leq abs_{max}\end{aligned}\end{align} \]

3.5. Test Predicates

The GDSC Test library contains predicates that test relative equality between two values.

Support is provided for symmetric relative equality using the name AreRelativelyClose which does not imply a direction. Support is provided for asymmetric relative equality using the name IsRelativelyCloseTo which implies a direction.

These can be constructed using a helper class:

double relativeError = 0.01;

DoubleDoubleBiPredicate areClose = Predicates.doublesAreRelativelyClose(relativeError);

// The AreClose relative equality is symmetric
assert areClose.test(100, 99) : "Difference 1 should be <= 0.01 of 100";
assert areClose.test(99, 100) : "Difference 1 should be <= 0.01 of 100";

// The test identifies large relative error
assert !areClose.test(10, 9) : "Difference 1 should not be <= 0.01 of 10";
assert !areClose.test(9, 10) : "Difference 1 should not be <= 0.01 of 10";


DoubleDoubleBiPredicate isCloseTo = Predicates.doublesIsRelativelyCloseTo(relativeError);

// The IsRelativelyCloseTo relative equality is asymmetric
assert isCloseTo.test(100, 99) : "Difference 1 should be <= 0.01 of 100";
assert !isCloseTo.test(99, 100) : "Difference 1 should not be <= 0.01 of 99";

// The test identifies large relative error
assert !isCloseTo.test(10, 9) : "Difference 1 should not be <= 0.01 of 10";
assert !isCloseTo.test(9, 10) : "Difference 1 should not be <= 0.01 of 9";

Note that the predicates can be constructed using an absolute error tolerance which is combined with the relative equality test using an Or operator:

double relativeError = 0.01;
double absoluteError = 1;
DoubleDoubleBiPredicate areClose = Predicates.doublesAreClose(relativeError, absoluteError);

// This would fail using relative error.
// The test passes using absolute error.
assert areClose.test(10, 9) : "Difference 1 should be <= 1";
assert areClose.test(9, 10) : "Difference 1 should be <= 1";

3.6. Test Framework Support

Testing relative equality within a test framework is simplified using predicates. For example a test for floating-point relative equality in JUnit 5 must adapt the test for absolute difference:

double relativeError = 0.01;
double expected = 100;
double actual = 99;

// equal within relative error of expected
Assertions.assertEquals(expected, actual, Math.abs(expected) * relativeError);

This can be replaced with:

double relativeError = 0.01;
double expected = 100;
double actual = 99;

// equal within relative error of expected
DoubleDoubleBiPredicate isCloseTo = Predicates.doublesIsRelativelyCloseTo(relativeError);
Assertions.assertTrue(isCloseTo.test(expected, actual));

This will identify errors but the error message is not helpful.

In order to provide useful error messages for a true/false predicate the GDSC Test library contains a helper class for performing assertions that will raise an AssertionError if the test is false. The TestAssertions class is based on the Assertions design ideas of JUnit 5. It provides static assertion methods for pairs of all primitive types using any bi-valued test predicate to compare the two matched values. Arrays and nested arrays are supported using recursion.

This allows the test for equality to be extended to arrays and nested arrays:

double relativeError = 0.01;
double expected = 100;
double actual = 99;

DoubleDoubleBiPredicate areClose = Predicates.doublesAreRelativelyClose(relativeError);

TestAssertions.assertTest(expected, actual, areClose);

// primitive arrays
double[] expectedArray = new double[] { expected };
double[] actualArray = new double[] { actual };
TestAssertions.assertArrayTest(expectedArray, actualArray, areClose);

// nested primitive arrays of matched dimension
Object[] expectedNestedArray = new double[][][] {{{ expected }}};
Object[] actualNestedArray = new double[][][] {{{ actual }}};
TestAssertions.assertArrayTest(expectedNestedArray, actualNestedArray, areClose);

If the predicate test fails then the TestAssertions class will construct a message containing the values that failed. Additionally all the predicates provided by the GDSC Test library support a description that will be added to the AssertionError message.