Skip to main content
Code Review

Return to Revisions

4 of 5
replaced http://meta.codereview.stackexchange.com/ with https://codereview.meta.stackexchange.com/

Floating point equality in Java - follow-up

This is a follow up post for this question.

There aren't a lot of changes. The changes (all of them major) are:

  1. 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.
  2. The flexibility of the class is still kept in mind; getters and setters have been added to compliment the private static tolerance values.
  3. 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
 * &infin; &ne; &infin; and {@code NaN} &ne; {@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.

Hungry Blue Dev
  • 3.1k
  • 3
  • 27
  • 49
lang-java

AltStyle によって変換されたページ (->オリジナル) /