This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 8b80a97  Identity unit conversion should preserve negative zero. When 
the value is used in a map projection parameter, the sign has implications in 
the concatenated transforms chain. The final result is numerically equivalent, 
but intermediate steps may differ depending on the parameter sign.
8b80a97 is described below

commit 8b80a971cdea37a107ce1c763e758775da669fb2
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Sep 19 15:39:00 2019 +0200

    Identity unit conversion should preserve negative zero.
    When the value is used in a map projection parameter, the sign has 
implications in the concatenated transforms chain.
    The final result is numerically equivalent, but intermediate steps may 
differ depending on the parameter sign.
---
 .../org/apache/sis/measure/AbstractConverter.java  |   2 +-
 .../apache/sis/measure/ConcatenatedConverter.java  |   2 +-
 .../org/apache/sis/measure/ConventionalUnit.java   |   4 +-
 .../org/apache/sis/measure/IdentityConverter.java  | 109 +++++++++++++++++++++
 .../org/apache/sis/measure/LinearConverter.java    |  49 +++++----
 .../java/org/apache/sis/measure/SystemUnit.java    |   4 +-
 .../apache/sis/measure/LinearConverterTest.java    |  14 +--
 7 files changed, 151 insertions(+), 33 deletions(-)

diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
index 4649573..a85e44d 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
@@ -140,7 +140,7 @@ abstract class AbstractConverter implements UnitConverter, 
Serializable {
     public UnitConverter concatenate(final UnitConverter converter) {
         ArgumentChecks.ensureNonNull("converter", converter);
         if (equals(converter.inverse())) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         return new ConcatenatedConverter(converter, this);
     }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/ConcatenatedConverter.java
 
b/core/sis-utility/src/main/java/org/apache/sis/measure/ConcatenatedConverter.java
index 18b4340..1569206 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/measure/ConcatenatedConverter.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/measure/ConcatenatedConverter.java
@@ -126,7 +126,7 @@ final class ConcatenatedConverter extends AbstractConverter 
implements LenientCo
     public UnitConverter concatenate(final UnitConverter converter) {
         ArgumentChecks.ensureNonNull("converter", converter);
         if (equals(converter.inverse())) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         // Delegate to c1 and c2 because they may provide more intelligent 
'concatenate' implementations.
         return c2.concatenate(c1.concatenate(converter));
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
index 603a4ba..20cc956 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
@@ -310,7 +310,7 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends 
AbstractUnit<Q> {
     @Override
     public UnitConverter getConverterTo(final Unit<Q> that) throws 
UnconvertibleException {
         if (that == this) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         ArgumentChecks.ensureNonNull("that", that);
         UnitConverter c = toTarget;
@@ -340,7 +340,7 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends 
AbstractUnit<Q> {
     @Override
     public UnitConverter getConverterToAny(final Unit<?> that) throws 
IncommensurableException {
         if (that == this) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         ArgumentChecks.ensureNonNull("that", that);
         UnitConverter c = toTarget;
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/IdentityConverter.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/IdentityConverter.java
new file mode 100644
index 0000000..721342e
--- /dev/null
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/measure/IdentityConverter.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.measure;
+
+import javax.measure.UnitConverter;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.LenientComparable;
+
+
+/**
+ * Linear converter with a scale factor of 1 and an offset of 0. We define a 
class for this special case
+ * instead than using the more generic {@link LinearConverter} class because 
we want to avoid performing
+ * any arithmetic operation in the {@link #convert(double)} method, in order 
to preserve negative zero:
+ *
+ * {@preformat java
+ *     convert(-0d) ≡ -0d
+ * }
+ *
+ * When the value is used in a map projection parameter, its sign can have 
implications in the chain of
+ * concatenated transforms. The final result is numerically equivalent, but 
intermediate steps may differ
+ * depending on the parameter sign.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class IdentityConverter extends AbstractConverter implements 
LenientComparable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1230536100139464866L;
+
+    /**
+     * The identity linear converter.
+     */
+    static final IdentityConverter INSTANCE = new IdentityConverter();
+
+    /**
+     * For {@link #INSTANCE} only.
+     */
+    private IdentityConverter() {
+    }
+
+    /** Straight forward implementation. */
+    @Override public boolean       isLinear()                   {return true;}
+    @Override public boolean       isIdentity()                 {return true;}
+    @Override public UnitConverter inverse()                    {return this;}
+    @Override public double        convert(double value)        {return value;}
+    @Override public double        derivative(double value)     {return 1;}
+    @Override public UnitConverter concatenate(UnitConverter c) {return c;}
+    @Override public String        toString()                   {return "y = 
x";}
+
+    /**
+     * Returns the value unchanged, with a check against null values
+     * for consistency with {@link LinearConverter#convert(Number)}.
+     */
+    @Override
+    public Number convert(Number value) {
+        ArgumentChecks.ensureNonNull("value", value);
+        return value;
+    }
+
+    /**
+     * Returns a hash code value for this unit converter.
+     */
+    @Override
+    public int hashCode() {
+        return (int) serialVersionUID;
+    }
+
+    /**
+     * Compares this converter with the given object for equality. This method 
may return {@code true}
+     * only if {@code Object} is an instance of {@link IdentityConverter} or 
{@link LinearConverter}.
+     * We apply this restriction in order to be symmetric with those cases,
+     * i.e. {@code A.equals(B)} = {@code B.equals(A)}.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        // See method javadoc for why we restrict to AbstractConverter instead 
of UnitConverter.
+        return (other instanceof AbstractConverter) && ((AbstractConverter) 
other).isIdentity();
+    }
+
+    /**
+     * Compares this converter with the given object for equality, optionally 
ignoring rounding errors.
+     */
+    @Override
+    public boolean equals(final Object other, final ComparisonMode mode) {
+        if (mode.isApproximate() && other instanceof LinearConverter) {
+            return ((LinearConverter) other).almostIdentity();
+        }
+        return (other instanceof UnitConverter) && ((UnitConverter) 
other).isIdentity();
+    }
+}
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
index 75e7acd..dc81ec3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
@@ -53,11 +53,6 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
     private static final long serialVersionUID = -3759983642723729926L;
 
     /**
-     * The identity linear converter.
-     */
-    static final LinearConverter IDENTITY = new LinearConverter(1, 0, 1);
-
-    /**
      * The scale to apply for converting values, before division by {@link 
#divisor}.
      */
     private final double scale;
@@ -101,7 +96,7 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
      * Creates a linear converter from the given scale and offset, which may 
be {@link BigDecimal} instances.
      * This is the implementation of public {@link Units#converter(Number, 
Number)} method.
      */
-    static LinearConverter create(final Number scale, final Number offset) {
+    static AbstractConverter create(final Number scale, final Number offset) {
         final double numerator, divisor;
         double shift = (offset != null) ? doubleValue(offset) : 0;
         if (scale instanceof Fraction) {
@@ -112,21 +107,16 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
             numerator = (scale != null) ? doubleValue(scale) : 1;
             divisor   = 1;
         }
-        final LinearConverter c = create(numerator, shift, divisor);
+        if (shift == 0 && numerator == divisor) {
+            return IdentityConverter.INSTANCE;
+        }
+        final LinearConverter c = new LinearConverter(numerator, shift, 
divisor);
         if (scale  instanceof BigDecimal) c.scale10  = (BigDecimal) scale;
         if (offset instanceof BigDecimal) c.offset10 = (BigDecimal) offset;
         return c;
     }
 
     /**
-     * Returns a linear converter for the given scale and offset.
-     */
-    private static LinearConverter create(final double scale, final double 
offset, final double divisor) {
-        if (offset == 0 && scale == divisor) return IDENTITY;
-        return new LinearConverter(scale, offset, divisor);
-    }
-
-    /**
      * Returns a linear converter for the given ratio. The scale factor is 
specified as a ratio because
      * the unit conversion factors are often defined with a value in base 10.  
That value is considered
      * exact by definition, but IEEE 754 has no exact representation of 
decimal fraction digits.
@@ -171,7 +161,7 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
             denominator = lc.divisor;
         } else {
             // Subtraction by convert(0) is a paranoiac safety.
-            numerator   = converter.convert(1.0) - converter.convert(0.0);
+            numerator   = converter.convert(1d) - converter.convert(0d);
             denominator = 1;
         }
         if (root) {
@@ -221,6 +211,13 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
     }
 
     /**
+     * Returns {@code true} if this converter is close to an identity 
converter.
+     */
+    final boolean almostIdentity() {
+        return epsilonEquals(scale, divisor) && epsilonEquals(offset, 0);
+    }
+
+    /**
      * Returns the inverse of this unit converter.
      * Given that the formula applied by this converter is:
      *
@@ -248,7 +245,7 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
      */
     @Override
     @SuppressWarnings("fallthrough")
-    Number[] coefficients() {
+    final Number[] coefficients() {
         final Number[] c = new Number[(scale != divisor) ? 2 : (offset != 0) ? 
1 : 0];
         switch (c.length) {
             case 2: c[1] = ratio(scale,  divisor);
@@ -385,7 +382,10 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
             otherOffset  /= cf;
             otherDivisor /= cf;
         }
-        return create(otherScale, otherOffset, otherDivisor);
+        if (otherOffset == 0 && otherScale == otherDivisor) {
+            return IdentityConverter.INSTANCE;
+        }
+        return new LinearConverter(otherScale, otherOffset, otherDivisor);
     }
 
     /**
@@ -409,6 +409,9 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
                    Numerics.equals(offset,  o.offset) &&
                    Numerics.equals(divisor, o.divisor);
         }
+        if (other instanceof IdentityConverter) {
+            return isIdentity();
+        }
         return false;
     }
 
@@ -418,7 +421,13 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
     @Override
     public boolean equals(final Object other, final ComparisonMode mode) {
         if (mode.isApproximate()) {
-            return (other instanceof LinearConverter) && 
equivalent((LinearConverter) other);
+            if (other instanceof LinearConverter) {
+                return equivalent((LinearConverter) other);
+            } else if (other instanceof IdentityConverter) {
+                return almostIdentity();
+            } else {
+                return false;
+            }
         } else {
             return equals(other);
         }
@@ -428,7 +437,7 @@ final class LinearConverter extends AbstractConverter 
implements LenientComparab
      * Returns {@code true} if the given converter perform the same conversion 
than this converter,
      * except for rounding errors.
      */
-    boolean equivalent(final LinearConverter other) {
+    final boolean equivalent(final LinearConverter other) {
         return epsilonEquals(scale  * other.divisor, other.scale  * divisor) &&
                epsilonEquals(offset * other.divisor, other.offset * divisor);
     }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
index d7caac0..fdaa50d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
@@ -359,7 +359,7 @@ final class SystemUnit<Q extends Quantity<Q>> extends 
AbstractUnit<Q> implements
             throw new UnconvertibleException(incompatible(unit));
         }
         if (step == unit) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         /*
          * At this point we know that the given units is not a system unit. 
Ask the conversion
@@ -389,7 +389,7 @@ final class SystemUnit<Q extends Quantity<Q>> extends 
AbstractUnit<Q> implements
             throw new IncommensurableException(incompatible(unit));
         }
         if (step == unit) {
-            return LinearConverter.IDENTITY;
+            return IdentityConverter.INSTANCE;
         }
         // Same remark than in getConverterTo(Unit).
         return unit.getConverterToAny(step).inverse();
diff --git 
a/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
 
b/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
index c4b3ca0..d8138ce 100644
--- 
a/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
+++ 
b/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
@@ -43,7 +43,7 @@ public final strictfp class LinearConverterTest extends 
TestCase {
      * @param  denominator  the expected denominator in the conversion factor.
      * @param  converter    the converter to verify.
      */
-    static void assertScale(final int numerator, final int denominator, final 
LinearConverter converter) {
+    static void assertScale(final int numerator, final int denominator, final 
AbstractConverter converter) {
         final double derivative = numerator / (double) denominator;
         final Number[] coefficients = converter.coefficients();
         assertEquals("coefficients.length", 2, coefficients.length);
@@ -88,7 +88,7 @@ public final strictfp class LinearConverterTest extends 
TestCase {
      */
     @Test
     public void testIsIdentityAndLinear() {
-        LinearConverter c = LinearConverter.IDENTITY;
+        AbstractConverter c = IdentityConverter.INSTANCE;
         assertTrue(c.isIdentity());
         assertTrue(c.isLinear());
         assertEquals("coefficients.length", 0, c.coefficients().length);
@@ -185,15 +185,15 @@ public final strictfp class LinearConverterTest extends 
TestCase {
      */
     @Test
     public void testConcatenate() {
-        LinearConverter c = LinearConverter.scale(254, 100);                   
     // inches to centimetres
+        AbstractConverter c = LinearConverter.scale(254, 100);                 
     // inches to centimetres
         assertScale(254, 100, c);
-        c = (LinearConverter) c.concatenate(LinearConverter.scale(10, 1));     
     // centimetres to millimetres
+        c = (AbstractConverter) c.concatenate(LinearConverter.scale(10, 1));   
     // centimetres to millimetres
         assertScale(254, 10, c);
-        c = (LinearConverter) c.concatenate(LinearConverter.scale(1, 1000));   
     // millimetres to metres
+        c = (AbstractConverter) c.concatenate(LinearConverter.scale(1, 1000)); 
     // millimetres to metres
         assertScale(254, 10000, c);
 
         c = LinearConverter.offset(27315, 100);                                
     // Celsius to kelvin
-        c = (LinearConverter) c.concatenate(LinearConverter.offset(-54630, 
200));
+        c = (AbstractConverter) c.concatenate(LinearConverter.offset(-54630, 
200));
         assertTrue(c.isIdentity());
     }
 
@@ -228,7 +228,7 @@ public final strictfp class LinearConverterTest extends 
TestCase {
      */
     @Test
     public void testToString() {
-        assertEquals("y = x",                   LinearConverter.IDENTITY       
   .toString());
+        assertEquals("y = x",                   IdentityConverter.INSTANCE     
   .toString());
         assertEquals("y = 100⋅x",               LinearConverter.scale (  100,  
 1).toString());
         assertEquals("y = x∕100",               LinearConverter.scale (    1, 
100).toString());
         assertEquals("y = 254⋅x∕100",           LinearConverter.scale (  254, 
100).toString());

Reply via email to