This is a follow up post for this question.
There aren't a lot of changes. The changes (all of them major) are:
- I've decided to ditch my previous approach using
Math.ulp()
and adopt Tim Leek's suggestion: comparing ratios. In my updated code, I calculate both ratios and ensure that their difference from 1 is within the tolerance. - The flexibility of the class is still kept in mind; getters and setters have been added to compliment the
private static
tolerance values. - The constructor is made
private
to prevent instantiation (which I simply forgot about in the previous version).
Basically, I fix my code when someone manages to break it. Tom Leek opened my eyes. Spotted's answer was nice too, but it's too bad that it must be done in a slightly different way to ensure compilation. It was a nice feeling when I saw my code pass in the cases mentioned by Tom Leek.
package library.util;
/**
* This class provides a useful interface for easy (approximate) floating
* point equality checks. This is necessary for several other classes. This
* class provides the user the power to set custom values for the
* <i>tolerances</i> used in comparison.
*
* @author Subhomoy Haldar (ambigram_maker)
* @version 1.5
*/
public class Numbers {
/**
* Private constructor to prevent instantiation.
*/
private Numbers() {
throw new AssertionError("No instances for you! ;P");
}
/**
* The default tolerance value for comparing the {@code float} values.
*/
public static final float DEFAULT_FLOAT_TOLERANCE = 5E-8f;
/**
* The default tolerance value for comparing the {@code double} values.
*/
public static final double DEFAULT_DOUBLE_TOLERANCE = 5E-16;
/**
* The current tolerance to be used for comparing floats.
*/
private static float floatTolerance = DEFAULT_FLOAT_TOLERANCE;
/**
* The current tolerance to be used for comparing doubles.
*/
private static double doubleTolerance = DEFAULT_DOUBLE_TOLERANCE;
/**
* Returns the current tolerance value for comparing {@code float}s.
*
* @return The current tolerance value for comparing {@code float}s.
*/
public static float getFloatTolerance() {
return floatTolerance;
}
/**
* This method allows the user to set a custom value for the tolerance for
* comparing {@code float}s. The value must always be positive.
*
* @param floatTolerance The new tolerance value.
* @throws IllegalArgumentException If the value given is absurd.
*/
public static void setFloatTolerance(float floatTolerance) {
if (floatTolerance <= 0) {
throw new IllegalArgumentException("Absurd tolerance value.");
}
Numbers.floatTolerance = floatTolerance;
}
/**
* Returns the current tolerance value for comparing {@code double}s.
*
* @return The current tolerance value for comparing {@code double}s.
*/
public static double getDoubleTolerance() {
return doubleTolerance;
}
/**
* This method allows the user to set a custom value for the tolerance for
* comparing {@code double}s. The value must always be positive.
*
* @param doubleTolerance The new tolerance value.
* @throws IllegalArgumentException If the value given is absurd.
*/
public static void setDoubleTolerance(double doubleTolerance) {
if (doubleTolerance <= 0) {
throw new IllegalArgumentException("Absurd tolerance value.");
}
Numbers.doubleTolerance = doubleTolerance;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param bytes The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(byte... bytes) {
int length = bytes.length;
assertProperLength(length);
byte d = bytes[0];
for (int i = 1; i < length; i++) {
if (d != bytes[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param shorts The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(short... shorts) {
int length = shorts.length;
assertProperLength(length);
short d = shorts[0];
for (int i = 1; i < length; i++) {
if (d != shorts[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param ints The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(int... ints) {
int length = ints.length;
assertProperLength(length);
int d = ints[0];
for (int i = 1; i < length; i++) {
if (d != ints[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param longs The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(long... longs) {
int length = longs.length;
assertProperLength(length);
long d = longs[0];
for (int i = 1; i < length; i++) {
if (d != longs[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param chars The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(char... chars) {
int length = chars.length;
assertProperLength(length);
char d = chars[0];
for (int i = 1; i < length; i++) {
if (d != chars[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param floats The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
* @see #areEqual(double, double) areEqual(double, double) -
* For an important note.
*/
public static boolean areEqual(float... floats) {
int length = floats.length;
assertProperLength(length);
float d = floats[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, floats[i])) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
* <p>
* <b>NOTE:</b> There are a few details which must be explained:
* <ol><li>When all the arguments are <i>infinities</i> (of the same sign),
* then this method returns {@code true}.</li>
* <li>When all the arguments are {@link Double#NaN Double.NaN}, then
* this method returns {@code true}.</li></ol>
* This goes <i>against</i> the standard specification (which requires that
* ∞ ≠ ∞ and {@code NaN} ≠ {@code NaN}). This is
* <i>counter-intuitive</i> and that is why this implementation does not
* follow the specification. The same applies for the
* {@link #areEqual(float, float) areEqual(float, float)} method.
*
* @param doubles The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(double... doubles) {
int length = doubles.length;
assertProperLength(length);
double d = doubles[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, doubles[i])) {
return false;
}
}
return true;
}
/**
* This method is used to ensure that the number of arguments is at least 2.
*
* @param length The number of arguments.
* @throws IllegalArgumentException If 1 or no arguments.
*/
private static void assertProperLength(int length) throws
IllegalArgumentException {
if (length < 2) {
throw new IllegalArgumentException
("At least two arguments required.");
}
}
private static boolean areEqual(double d1, double d2) {
// the corner cases first:
if (Double.isNaN(d1) && Double.isNaN(d2)) {
return true;
}
if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
return d1 == d2; // ensures same sign
}
if (d1 == 0 || d2 == 0) {
return Math.abs(d1 - d2) <= doubleTolerance;
}
// No more "kludgy business with Math.ulp()." ;)
// Just compare the ratios:
double ratio1 = d1 / d2; // Two ratios are there
double ratio2 = d2 / d1; // for symmetry.
return d1 == d2 ||
Math.abs(ratio1 - 1) <= doubleTolerance &&
Math.abs(ratio2 - 1) <= doubleTolerance;
}
private static boolean areEqual(float f1, float f2) {
if (Float.isNaN(f1) && Float.isNaN(f2)) {
return true;
}
if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
return f1 == f2;
}
if (f1 == 0 || f2 == 0) {
return Math.abs(f1 - f2) <= floatTolerance;
}
double ratio1 = f1 / f2;
double ratio2 = f2 / f1;
return f1 == f2 ||
Math.abs(ratio1 - 1) <= floatTolerance &&
Math.abs(ratio2 - 1) <= floatTolerance;
}
}
Now, for the cases mentioned by Tom Leek:
double a = 4.6e4; // any arbitrary value
double b = a + 0.9 * Numbers.getDoubleTolerance();
double c = a - 0.9 * Numbers.getDoubleTolerance();
assert Numbers.areEqual(a, b, c);
assert Numbers.areEqual(b, a, c);
assert !Numbers.areEqual(1e-30, 1e-36);
double d = Math.pow(2, 53);
double e = d + 1;
assert d == e;
assert Numbers.areEqual(d, e);
The above code runs without resulting in any assertion errors. My request is simple: try breaking this one as well. ;-)
Also, I would like the usual: suggestions on improving readability, reusability and so on.
areEqual
should beareNearlyEqual
since, as pointed out in a response to the earlier question, this version of equality is not transitive, hence, hides a trap. \$\endgroup\$