2010-05-03 95 views
2

当我的Floating-Point Guide昨天是published on slashdot时,我对我的建议comparison function感到非常不满,这确实是不够的。所以我终于做了明智的事情,并写了一个测试套件,看看我能否让他们全部通过。这是我迄今为止的结果。我想知道这是否真的像泛型(即不是特定于应用程序)的float比较函数一样好,还是我仍然错过了一些边缘情况。这个函数比较浮点数有什么问题吗?

(代码更新来修复错误)

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)); 
    } 

} 
+1

Slashdot上的精英分子可能非常残酷。 – ChaosPandion 2010-05-03 20:57:56

+0

@Chaos:而且往往是不正确的 – 2010-05-03 21:08:11

+1

我的观点是,我完全应该尝试撰写权威的源代码,并且为我建议的“良好”比较函数编写测试套件过于草率地受到批评。 – 2010-05-03 21:19:24

回答

0

睡在它之后,我意识到,这部分是垃圾:

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

这成为严格的小量变小!一个更明智的版本:

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

尽管如此,if的两个分支不相互非常一致。当ab比其中一个为零时要小得多。我真的开始认为using integer comparison是一个更好的方法。

3

主要的问题,我看到的是你不要让用户控制小量。

另外,epsilon的变化取决于被比较的数字的数量级。接近于零的ε很小,接近最大功率ε很大。

我认为,无论何时您需要谈论“足够接近”等概念时,它都会成为应用级设计决策。你不能为此写一个通用库。

+0

用户控制的epsilon被省略以减少测试中的重复。而且比较确实使用了相对误差,这与ε变化具有相同的效果。你的第三个陈述当然是正确的。 – 2010-05-03 21:17:08