views:

114

answers:

2

When my Floating-Point Guide was yesterday published on slashdot, I got a lot of flak for my suggested comparison function, which was indeed inadequate. So I finally did the sensible thing and wrote a test suite to see whether I could get them all to pass. Here is my result so far. And I wonder if this is really as good as one can get with a generic (i.e. not application specific) float comparison function, or whether I still missed some edge cases.

(Code updated to fix error)

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
 * Test suite to demonstrate a good method for comparing floating-point values using an epsilon. Run via JUnit 4.
 *
 * Note: this function attempts a "one size fits all" solution. There may be some edge cases for which it still
 * produces unexpected results, and some of the tests it was developed to pass probably specify behaviour that is
 * not appropriate for some applications. Before using it, make sure it's appropriate for your application!
 *
 * From http://floating-point-gui.de
 *
 * @author Michael Borgwardt
 */
public class NearlyEqualsTest {
    public static boolean nearlyEqual(float a, float b, float epsilon) {
        final float absA = Math.abs(a);
        final float absB = Math.abs(b);
        final float diff = Math.abs(a - b);

        if (a * b == 0) { // a or b or both are zero
            // relative error is not meaningful here
            return diff < (epsilon * epsilon);
        } else { // use relative error
            return diff / (absA + absB) < epsilon;
        }
    }

    public static boolean nearlyEqual(float a, float b) {
        return nearlyEqual(a, b, 0.000001f);
    }

    /** Regular large numbers - generally not problematic */
    @Test
    public void big() {
        assertTrue(nearlyEqual(1000000f, 1000001f));
        assertTrue(nearlyEqual(1000001f, 1000000f));
        assertFalse(nearlyEqual(10000f, 10001f));
        assertFalse(nearlyEqual(10001f, 10000f));
    }

    /** Negative large numbers */
    @Test
    public void bigNeg() {
        assertTrue(nearlyEqual(-1000000f, -1000001f));
        assertTrue(nearlyEqual(-1000001f, -1000000f));
        assertFalse(nearlyEqual(-10000f, -10001f));
        assertFalse(nearlyEqual(-10001f, -10000f));
    }

    /** Numbers around 1 */
    @Test
    public void mid() {
        assertTrue(nearlyEqual(1.0000001f, 1.0000002f));
        assertTrue(nearlyEqual(1.0000002f, 1.0000001f));
        assertFalse(nearlyEqual(1.0002f, 1.0001f));
        assertFalse(nearlyEqual(1.0001f, 1.0002f));
    }

    /** Numbers around -1 */
    @Test
    public void midNeg() {
        assertTrue(nearlyEqual(-1.000001f, -1.000002f));
        assertTrue(nearlyEqual(-1.000002f, -1.000001f));
        assertFalse(nearlyEqual(-1.0001f, -1.0002f));
        assertFalse(nearlyEqual(-1.0002f, -1.0001f));
    }

    /** Numbers between 1 and 0 */
    @Test
    public void small() {
        assertTrue(nearlyEqual(0.000000001000001f, 0.000000001000002f));
        assertTrue(nearlyEqual(0.000000001000002f, 0.000000001000001f));
        assertFalse(nearlyEqual(0.000000000001002f, 0.000000000001001f));
        assertFalse(nearlyEqual(0.000000000001001f, 0.000000000001002f));
    }

    /** Numbers between -1 and 0 */
    @Test
    public void smallNeg() {
        assertTrue(nearlyEqual(-0.000000001000001f, -0.000000001000002f));
        assertTrue(nearlyEqual(-0.000000001000002f, -0.000000001000001f));
        assertFalse(nearlyEqual(-0.000000000001002f, -0.000000000001001f));
        assertFalse(nearlyEqual(-0.000000000001001f, -0.000000000001002f));
    }

    /** Comparisons involving zero */
    @Test
    public void zero() {
        assertTrue(nearlyEqual(0.0f, 0.0f));
        assertTrue(nearlyEqual(0.0f, -0.0f));
        assertTrue(nearlyEqual(-0.0f, -0.0f));
        assertFalse(nearlyEqual(0.00000001f, 0.0f));
        assertFalse(nearlyEqual(0.0f, 0.00000001f));
        assertFalse(nearlyEqual(-0.00000001f, 0.0f));
        assertFalse(nearlyEqual(0.0f, -0.00000001f));

        assertTrue(nearlyEqual(0.0f, 0.00000001f, 0.01f));
        assertTrue(nearlyEqual(0.00000001f, 0.0f, 0.01f));
        assertFalse(nearlyEqual(0.00000001f, 0.0f, 0.000001f));
        assertFalse(nearlyEqual(0.0f, 0.00000001f, 0.000001f));

        assertTrue(nearlyEqual(0.0f, -0.00000001f, 0.1f));
        assertTrue(nearlyEqual(-0.00000001f, 0.0f, 0.1f));
        assertFalse(nearlyEqual(-0.00000001f, 0.0f, 0.00000001f));
        assertFalse(nearlyEqual(0.0f, -0.00000001f, 0.00000001f));
    }

    /** Comparisons of numbers on opposite sides of 0 */
    @Test
    public void opposite() {
        assertFalse(nearlyEqual(1.000000001f, -1.0f));
        assertFalse(nearlyEqual(-1.0f, 1.000000001f));
        assertFalse(nearlyEqual(-1.000000001f, 1.0f));
        assertFalse(nearlyEqual(1.0f, -1.000000001f));
        assertTrue(nearlyEqual(1e10f * Float.MIN_VALUE, -1e10f * Float.MIN_VALUE));
    }

    /**
     * The really tricky part - comparisons of numbers very close to zero.
     */
    @Test
    public void ulp() {
        assertTrue(nearlyEqual(Float.MIN_VALUE, -Float.MIN_VALUE));
        assertTrue(nearlyEqual(-Float.MIN_VALUE, Float.MIN_VALUE));
        assertTrue(nearlyEqual(Float.MIN_VALUE, 0));
        assertTrue(nearlyEqual(0, Float.MIN_VALUE));
        assertTrue(nearlyEqual(-Float.MIN_VALUE, 0));
        assertTrue(nearlyEqual(0, -Float.MIN_VALUE));

        assertFalse(nearlyEqual(0.000000001f, -Float.MIN_VALUE));
        assertFalse(nearlyEqual(0.000000001f, Float.MIN_VALUE));
        assertFalse(nearlyEqual(Float.MIN_VALUE, 0.000000001f));
        assertFalse(nearlyEqual(-Float.MIN_VALUE, 0.000000001f));

        assertFalse(nearlyEqual(1e25f * Float.MIN_VALUE, 0.0f, 1e-12f));
        assertFalse(nearlyEqual(0.0f, 1e25f * Float.MIN_VALUE, 1e-12f));
        assertFalse(nearlyEqual(1e25f * Float.MIN_VALUE, -1e25f * Float.MIN_VALUE, 1e-12f));

        assertTrue(nearlyEqual(1e25f * Float.MIN_VALUE, 0.0f, 1e-5f));
        assertTrue(nearlyEqual(0.0f, 1e25f * Float.MIN_VALUE, 1e-5f));
        assertTrue(nearlyEqual(1e20f * Float.MIN_VALUE, -1e20f * Float.MIN_VALUE, 1e-5f));
    }

}
+3  A: 

The main problem I see is you don't allow the user to control epsilon.

Also epsilon changes depending on the order of magnitude of the numbers being compared. Near zero epsilon is small, near the maximum power epsilon is large.

I think whenever you need to talk about such concepts as "close enough" it becomes an application level design decision. You can't write a generic library for that.

Pyrolistical
User-controlled epsilon was omitted to reduce repetition in the tests. And the comparison does use a relative error, which has the same effect as varying epsilon. Your third statement is certainly correct.
Michael Borgwardt
A: 

After sleeping over it, I've realized that this part was rubbish:

    if (a*b==0) {
        return diff < Float.MIN_VALUE / epsilon;

This becomes less strict as epsilon gets smaller! A more sensible version:

    if (a * b == 0) {
        return diff < (epsilon * epsilon);

Still, the two branches of the if are not very consistent with each other. It's much stricter when a or b is very small than when one of them is zero. I'm really starting to think that using integer comparison is an overall better method.

Michael Borgwardt