You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2014/04/18 17:58:47 UTC

svn commit: r1588500 - in /commons/proper/math/trunk/src: changes/changes.xml main/java/org/apache/commons/math3/complex/Complex.java test/java/org/apache/commons/math3/complex/ComplexTest.java

Author: erans
Date: Fri Apr 18 15:58:47 2014
New Revision: 1588500

URL: http://svn.apache.org/r1588500
Log:
MATH-1118
Fixed compatibility of "equals(Object)" with "hashCode()" ("Complex" will
behave as JDK's "Double"). Added new methods for testing floating-point
equality.
Thanks to Cyrille Artho for reporting the issue.

Modified:
    commons/proper/math/trunk/src/changes/changes.xml
    commons/proper/math/trunk/src/main/java/org/apache/commons/math3/complex/Complex.java
    commons/proper/math/trunk/src/test/java/org/apache/commons/math3/complex/ComplexTest.java

Modified: commons/proper/math/trunk/src/changes/changes.xml
URL: http://svn.apache.org/viewvc/commons/proper/math/trunk/src/changes/changes.xml?rev=1588500&r1=1588499&r2=1588500&view=diff
==============================================================================
--- commons/proper/math/trunk/src/changes/changes.xml (original)
+++ commons/proper/math/trunk/src/changes/changes.xml Fri Apr 18 15:58:47 2014
@@ -51,6 +51,11 @@ If the output is not quite correct, chec
   </properties>
   <body>
     <release version="3.3" date="TBD" description="TBD">
+      <action dev="erans" type="fix" issue="MATH-1118">
+        "Complex": Fixed compatibility of "equals(Object)" with "hashCode()".
+        Added new methods for testing floating-point equality between the real
+        (resp. imaginary) parts of two complex numbers.
+      </action>
       <action dev="luc" type="update" >
         Bracketing utility for univariate root solvers returns a tighter interval than before.
         It also allows choosing the search interval expansion rate, supporting both linear

Modified: commons/proper/math/trunk/src/main/java/org/apache/commons/math3/complex/Complex.java
URL: http://svn.apache.org/viewvc/commons/proper/math/trunk/src/main/java/org/apache/commons/math3/complex/Complex.java?rev=1588500&r1=1588499&r2=1588500&view=diff
==============================================================================
--- commons/proper/math/trunk/src/main/java/org/apache/commons/math3/complex/Complex.java (original)
+++ commons/proper/math/trunk/src/main/java/org/apache/commons/math3/complex/Complex.java Fri Apr 18 15:58:47 2014
@@ -27,6 +27,7 @@ import org.apache.commons.math3.exceptio
 import org.apache.commons.math3.exception.util.LocalizedFormats;
 import org.apache.commons.math3.util.FastMath;
 import org.apache.commons.math3.util.MathUtils;
+import org.apache.commons.math3.util.Precision;
 
 /**
  * Representation of a Complex number, i.e. a number which has both a
@@ -321,19 +322,28 @@ public class Complex implements FieldEle
     }
 
     /**
-     * Test for the equality of two Complex objects.
+     * Test for equality with another object.
      * If both the real and imaginary parts of two complex numbers
      * are exactly the same, and neither is {@code Double.NaN}, the two
      * Complex objects are considered to be equal.
-     * All {@code NaN} values are considered to be equal - i.e, if either
-     * (or both) real and imaginary parts of the complex number are equal
-     * to {@code Double.NaN}, the complex number is equal to
-     * {@code NaN}.
+     * The behavior is the same as for JDK's {@link Double#equals(Object)
+     * Double}:
+     * <ul>
+     *  <li>All {@code NaN} values are considered to be equal,
+     *   i.e, if either (or both) real and imaginary parts of the complex
+     *   number are equal to {@code Double.NaN}, the complex number is equal
+     *   to {@code NaN}.
+     *  </li>
+     *  <li>
+     *   Instances constructed with different representations of zero (i.e.
+     *   either "0" or "-0") are <em>not</em> considered to be equal.
+     *  </li>
+     * </ul>
      *
-     * @param other Object to test for equality to this
-     * @return true if two Complex objects are equal, false if object is
-     * {@code null}, not an instance of Complex, or not equal to this Complex
-     * instance.
+     * @param other Object to test for equality with this instance.
+     * @return {@code true} if the objects are equal, {@code false} if object
+     * is {@code null}, not an instance of {@code Complex}, or not equal to
+     * this instance.
      */
     @Override
     public boolean equals(Object other) {
@@ -341,17 +351,95 @@ public class Complex implements FieldEle
             return true;
         }
         if (other instanceof Complex){
-            Complex c = (Complex)other;
+            Complex c = (Complex) other;
             if (c.isNaN) {
                 return isNaN;
             } else {
-                return (real == c.real) && (imaginary == c.imaginary);
+                return MathUtils.equals(real, c.real) &&
+                    MathUtils.equals(imaginary, c.imaginary);
             }
         }
         return false;
     }
 
     /**
+     * Test for the floating-point equality between Complex objects.
+     * It returns {@code true} if both arguments are equal or within the
+     * range of allowed error (inclusive).
+     *
+     * @param x First value (cannot be {@code null}).
+     * @param y Second value (cannot be {@code null}).
+     * @param maxUlps {@code (maxUlps - 1)} is the number of floating point
+     * values between the real (resp. imaginary) parts of {@code x} and
+     * {@code y}.
+     * @return {@code true} if there are fewer than {@code maxUlps} floating
+     * point values between the real (resp. imaginary) parts of {@code x}
+     * and {@code y}.
+     *
+     * @see Precision#equals(double,double,int)
+     * @since 3.3
+     */
+    public static boolean equals(Complex x, Complex y, int maxUlps) {
+        return Precision.equals(x.real, y.real, maxUlps) &&
+            Precision.equals(x.imaginary, y.imaginary, maxUlps);
+    }
+
+    /**
+     * Returns {@code true} iff the values are equal as defined by
+     * {@link #equals(Complex,Complex,int) equals(x, y, 1)}.
+     *
+     * @param x First value (cannot be {@code null}).
+     * @param y Second value (cannot be {@code null}).
+     * @return {@code true} if the values are equal.
+     *
+     * @since 3.3
+     */
+    public static boolean equals(Complex x, Complex y) {
+        return equals(x, y, 1);
+    }
+
+    /**
+     * Returns {@code true} if, both for the real part and for the imaginary
+     * part, there is no double value strictly between the arguments or the
+     * difference between them is within the range of allowed error
+     * (inclusive).
+     *
+     * @param x First value (cannot be {@code null}).
+     * @param y Second value (cannot be {@code null}).
+     * @param eps Amount of allowed absolute error.
+     * @return {@code true} if the values are two adjacent floating point
+     * numbers or they are within range of each other.
+     *
+     * @see Precision#equals(double,double,double)
+     * @since 3.3
+     */
+    public static boolean equals(Complex x, Complex y, double eps) {
+        return Precision.equals(x.real, y.real, eps) &&
+            Precision.equals(x.imaginary, y.imaginary, eps);
+    }
+
+    /**
+     * Returns {@code true} if, both for the real part and for the imaginary
+     * part, there is no double value strictly between the arguments or the
+     * relative difference between them is smaller or equal to the given
+     * tolerance.
+     *
+     * @param x First value (cannot be {@code null}).
+     * @param y Second value (cannot be {@code null}).
+     * @param eps Amount of allowed relative error.
+     * @return {@code true} if the values are two adjacent floating point
+     * numbers or they are within range of each other.
+     *
+     * @see Precision#equalsWithRelativeTolerance(double,double,double)
+     * @since 3.3
+     */
+    public static boolean equalsWithRelativeTolerance(Complex x, Complex y,
+                                                      double eps) {
+        return Precision.equalsWithRelativeTolerance(x.real, y.real, eps) &&
+            Precision.equalsWithRelativeTolerance(x.imaginary, y.imaginary, eps);
+    }
+
+    /**
      * Get a hashCode for the complex number.
      * Any {@code Double.NaN} value in real or imaginary part produces
      * the same hash code {@code 7}.

Modified: commons/proper/math/trunk/src/test/java/org/apache/commons/math3/complex/ComplexTest.java
URL: http://svn.apache.org/viewvc/commons/proper/math/trunk/src/test/java/org/apache/commons/math3/complex/ComplexTest.java?rev=1588500&r1=1588499&r2=1588500&view=diff
==============================================================================
--- commons/proper/math/trunk/src/test/java/org/apache/commons/math3/complex/ComplexTest.java (original)
+++ commons/proper/math/trunk/src/test/java/org/apache/commons/math3/complex/ComplexTest.java Fri Apr 18 15:58:47 2014
@@ -311,7 +311,7 @@ public class ComplexTest {
     @Test
     public void testReciprocalReal() {
         Complex z = new Complex(-2.0, 0.0);
-        Assert.assertEquals(new Complex(-0.5, 0.0), z.reciprocal());
+        Assert.assertTrue(Complex.equals(new Complex(-0.5, 0.0), z.reciprocal()));
     }
 
     @Test
@@ -497,6 +497,15 @@ public class ComplexTest {
         Assert.assertFalse(x.equals(null));
     }
 
+    @Test(expected=NullPointerException.class)
+    public void testFloatingPointEqualsPrecondition1() {
+        Complex.equals(new Complex(3.0, 4.0), null, 3);
+    }
+    @Test(expected=NullPointerException.class)
+    public void testFloatingPointEqualsPrecondition2() {
+        Complex.equals(null, new Complex(3.0, 4.0), 3);
+    }
+
     @Test
     public void testEqualsClass() {
         Complex x = new Complex(3.0, 4.0);
@@ -510,6 +519,65 @@ public class ComplexTest {
     }
 
     @Test
+    public void testFloatingPointEquals() {
+        double re = -3.21;
+        double im = 456789e10;
+
+        final Complex x = new Complex(re, im);
+        Complex y = new Complex(re, im);
+
+        Assert.assertTrue(x.equals(y));
+        Assert.assertTrue(Complex.equals(x, y));
+
+        final int maxUlps = 5;
+        for (int i = 0; i < maxUlps; i++) {
+            re = Math.nextUp(re);
+            im = Math.nextUp(im);
+        }
+        y = new Complex(re, im);
+        Assert.assertTrue(Complex.equals(x, y, maxUlps));
+
+        re = Math.nextUp(re);
+        im = Math.nextUp(im);
+        y = new Complex(re, im);
+        Assert.assertFalse(Complex.equals(x, y, maxUlps));
+    }
+
+    @Test
+    public void testFloatingPointEqualsNaN() {
+        Complex c = new Complex(Double.NaN, 1);
+        Assert.assertFalse(Complex.equals(c, c));
+
+        c = new Complex(1, Double.NaN);
+        Assert.assertFalse(Complex.equals(c, c));
+    }
+
+    @Test
+    public void testFloatingPointEqualsWithAllowedDelta() {
+        final double re = 153.0000;
+        final double im = 152.9375;
+        final double tol1 = 0.0625;
+        final Complex x = new Complex(re, im);
+        final Complex y = new Complex(re + tol1, im + tol1);
+        Assert.assertTrue(Complex.equals(x, y, tol1));
+
+        final double tol2 = 0.0624;
+        Assert.assertFalse(Complex.equals(x, y, tol2));
+    }
+
+    @Test
+    public void testFloatingPointEqualsWithRelativeTolerance() {
+        final double tol = 1e-4;
+        final double re = 1;
+        final double im = 1e10;
+
+        final double f = 1 + tol;
+        final Complex x = new Complex(re, im);
+        final Complex y = new Complex(re * f, im * f);
+        Assert.assertTrue(Complex.equalsWithRelativeTolerance(x, y, tol));
+    }
+
+    @Test
     public void testEqualsTrue() {
         Complex x = new Complex(3.0, 4.0);
         Complex y = new Complex(3.0, 4.0);
@@ -551,6 +619,21 @@ public class ComplexTest {
         Complex imaginaryNaN = new Complex(0.0, Double.NaN);
         Assert.assertEquals(realNaN.hashCode(), imaginaryNaN.hashCode());
         Assert.assertEquals(imaginaryNaN.hashCode(), Complex.NaN.hashCode());
+
+        // MATH-1118
+        // "equals" and "hashCode" must be compatible: if two objects have
+        // different hash codes, "equals" must return false.
+        final String msg = "'equals' not compatible with 'hashCode'";
+
+        x = new Complex(0.0, 0.0);
+        y = new Complex(0.0, -0.0);
+        Assert.assertTrue(x.hashCode() != y.hashCode());
+        Assert.assertFalse(msg, x.equals(y));
+
+        x = new Complex(0.0, 0.0);
+        y = new Complex(-0.0, 0.0);
+        Assert.assertTrue(x.hashCode() != y.hashCode());
+        Assert.assertFalse(msg, x.equals(y));
     }
 
     @Test
@@ -1067,7 +1150,8 @@ public class ComplexTest {
     /** test issue MATH-221 */
     @Test
     public void testMath221() {
-        Assert.assertEquals(new Complex(0,-1), new Complex(0,1).multiply(new Complex(-1,0)));
+        Assert.assertTrue(Complex.equals(new Complex(0,-1),
+                                         new Complex(0,1).multiply(new Complex(-1,0))));
     }
 
     /**