HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMajor

Floating point equality in "Numbers.java"

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
equalitypointfloatingnumbersjava

Problem

Floating point inaccuracies are really annoying. I understood that in its true sense while developing the next version of Point (this time I'm actually foolproofing my code). Before I upload it for review, I want a class that I wrote, called Numbers to be reviewed.

Numbers.java

```
package library.util;

/**
* This class provides a useful interface for easy (approximate) floating
* point equality checks. This is necessary for several other classes.
*
* @author Subhomoy Haldar (ambigram_maker)
* @version 1.0
*/
public class Numbers {

/**
* The tolerance value for comparing the {@code float} values.
*/
public static double FLOAT_TOLERANCE = 5E-8;
/**
* The tolerance value for comparing the {@code double} values.
*/
public static double DOUBLE_TOLERANCE = 5E-16;

/**
* Returns {@code true} if the arguments are exactly equal.
*
* @param bytes The arguments to check.
* @return {@code true} if the arguments are exactly equal.
*/
public static boolean areEqual(byte... bytes) {
int length = bytes.length;
checkLength(length);
byte d = bytes[0];
for (int i = 1; i exactly equal.
*
* @param shorts The arguments to check.
* @return {@code true} if the arguments are exactly equal.
*/
public static boolean areEqual(short... shorts) {
int length = shorts.length;
checkLength(length);
short d = shorts[0];
for (int i = 1; i exactly equal.
*
* @param ints The arguments to check.
* @return {@code true} if the arguments are exactly equal.
*/
public static boolean areEqual(int... ints) {
int length = ints.length;
checkLength(length);
int d = ints[0];
for (int i = 1; i exactly equal.
*
* @param longs The arguments to check.
* @return {@code true} if the arguments are exactly equal.
*/
public static boolean areEqual(long... longs) {
int l

Solution

Your mathematical criterion for equality has some issues.

One important point to consider is that your "equality" is not transitive. In your code, you use a threshold of \$\epsilon\$ (\$\epsilon = 5\times10^{-16}\$ in your code, when the values have type double); you declare \$x\$ and \$y\$ to be equal to each other if \$|x-y|\leq \epsilon\$. Now suppose that you have three values \$a\$, \$b\$ and \$c\$ such that \$b = a + 0.9\epsilon\$ and \$c = a - 0.9\epsilon\$. In that case, areEqual(a, b, c) will return true, but areEqual(b, a, c) will return false. This is surprising behaviour -- the documentation of areEqual() does not say that the result depends on the order of parameters.

To fix some of the symptoms, you need to compare all pairs of values: areEqual(a, b, c) shall return true only if areEqual(a, b), areEqual(a, c) and areEqual(b, c) all return true. In all generality, this is quadratic: with 10 parameters, you end up doing 45 comparisons; with 20 parameters, 190 comparisons... This can become computationally prohibitive. And while this would avoid the problem of the result depending on the order of parameters, it would still not make the relation transitive: areEqual(a, b) and areEqual(a, c) could return true while areEqual(b, c) would return false.

Moreover, using a difference is usually not the "correct" way to approximate. If \$x = 10^{-30}\$ and \$y = 10^{-36}\$ then they normally should not be considered equal to each other, even approximately, since \$x\$ is one million times bigger than \$y\$ -- but your areEqual() will declare them equal to each other. You get the dual problem with big numbers: when \$x\$ is large (close to \$2^{53}\$ for double values), \$x\$ and \$x+1\$ (for instance) have a difference equal to \$1\$ (thus greater than \$\epsilon\$) but they really differ in their least significant bit only; you explicitly added some code using Math.ulp() to handle that case.

There are usage contexts where a difference is what makes most sense; but in general, a ratio is more appropriate. In physics, values are measures, with a given precision which is more or less the number of significant digits. A ratio captures that notion. Mathematically, this means that you declare \$x\$ and \$y\$ to be approximately equal to each other if \$|(x/y)-1| \leq \epsilon\$ (and also \$|(y/x)-1| \leq \epsilon\$, to make the relation at least symmetric). This will not catch all cases, and you will have to do something about values which are very close to \$0\$; but that criterion will be a more correct notion of "approximate equality", it will scale up and down smoothly, and avoid the kludgy business with Math.ulp().

Context

StackExchange Code Review Q#102527, answer score: 23

Revisions (0)

No revisions yet.