sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Result of arithmetic operations on quantities shall be as if all values were converted to system quantities before the operations is performed. https://issues.apache.org/jira/browse/SIS-429
Date Sun, 29 Jul 2018 15:57:56 GMT
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 bbc68e3  Result of arithmetic operations on quantities shall be as if all values
were converted to system quantities before the operations is performed. https://issues.apache.org/jira/browse/SIS-429
bbc68e3 is described below

commit bbc68e3a2d596834b7b94c317ea7b19505cab1df
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Jul 29 17:57:37 2018 +0200

    Result of arithmetic operations on quantities shall be as if all values were converted
to system quantities before the operations is performed.
    https://issues.apache.org/jira/browse/SIS-429
---
 .../java/org/apache/sis/measure/DerivedScalar.java | 270 +++++++++++++++++++++
 .../java/org/apache/sis/measure/Quantities.java    |  34 ++-
 .../main/java/org/apache/sis/measure/Scalar.java   | 108 ++++++---
 .../java/org/apache/sis/measure/ScalarFactory.java |  23 +-
 .../main/java/org/apache/sis/measure/Units.java    |  20 +-
 .../org/apache/sis/measure/QuantitiesTest.java     |  42 +++-
 6 files changed, 453 insertions(+), 44 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/DerivedScalar.java b/core/sis-utility/src/main/java/org/apache/sis/measure/DerivedScalar.java
new file mode 100644
index 0000000..158be45
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/DerivedScalar.java
@@ -0,0 +1,270 @@
+/*
+ * 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.Unit;
+import javax.measure.Quantity;
+import javax.measure.UnitConverter;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Method;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * A quantity related to a scalar by an arbitrary (not necessarily linear) conversion.
+ * For example a temperature in Celsius degrees is related to a temperature in Kelvin
+ * by applying an offset.
+ *
+ * <p>The {@link Scalar} parent class is restricted to cases where the relationship
with system unit
+ * is a scale factor. This {@code DerivedScalar} subclass allow the relationship to be more
generic.
+ * It is a design similar to {@link org.opengis.referencing.crs.DerivedCRS}</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ *
+ * @param <Q>  the concrete subtype.
+ *
+ * @since 1.0
+ * @module
+ */
+abstract class DerivedScalar<Q extends Quantity<Q>> extends Scalar<Q> {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 3729159568163676568L;
+
+    /**
+     * The value specified by the user, in unit of {@link #derivedUnit}.
+     * Could be computed form super-class value, but nevertheless stored
+     * for avoiding rounding errors.
+     */
+    private final double derivedValue;
+
+    /**
+     * The unit of measurement specified by the user. The relationship between this unit
+     * and its system unit (stored in super-class) is something more complex than a scale
+     * factor, otherwise we would not need this {@code DerivedScalar}.
+     */
+    private final Unit<Q> derivedUnit;
+
+    /**
+     * Converter from the system unit to the unit of this quantity.
+     */
+    private final UnitConverter fromSystem;
+
+    /**
+     * Creates a new scalar for the given value.
+     *
+     * @param toSystem  converter from {@code unit} to the system unit.
+     */
+    DerivedScalar(final double value, final Unit<Q> unit, final Unit<Q> systemUnit,
final UnitConverter toSystem) {
+        super(toSystem.convert(value), systemUnit);
+        derivedValue = value;
+        derivedUnit  = unit;
+        fromSystem   = toSystem.inverse();
+    }
+
+    /**
+     * Creates a new scalar resulting from an arithmetic operation performed on the given
scalar.
+     * The arithmetic operation result is in the same unit than the original scalar.
+     *
+     * @param  value  the arithmetic result in system unit.
+     */
+    DerivedScalar(final DerivedScalar<Q> origin, final double value) {
+        super(value, origin.getSystemUnit());
+        derivedUnit  = origin.derivedUnit;
+        fromSystem   = origin.fromSystem;
+        derivedValue = fromSystem.convert(value);
+    }
+
+    /**
+     * Creates a new quantity of same type than this quantity but with a different value.
+     * The unit of measurement shall be the same than the system unit of this quantity.
+     * Implementation in subclasses should be like below:
+     *
+     * {@preformat java
+     *     assert newUnit == getSystemUnit() : newUnit;
+     *     return new MyDerivedScalar(this, newValue);
+     * }
+     */
+    @Override
+    abstract Quantity<Q> create(double newValue, Unit<Q> newUnit);
+
+    /**
+     * Returns the system unit of measurement.
+     */
+    final Unit<Q> getSystemUnit() {
+        return super.getUnit();
+    }
+
+    /**
+     * Returns the unit of measurement specified at construction time.
+     */
+    @Override
+    public final Unit<Q> getUnit() {
+        return derivedUnit;
+    }
+
+    /**
+     * Returns the value specified at construction time.
+     */
+    @Override
+    public final double doubleValue() {
+        return derivedValue;
+    }
+
+    /**
+     * Returns the value casted to a single-precision floating point number.
+     */
+    @Override
+    public final float floatValue() {
+        return (float) derivedValue;
+    }
+
+    /**
+     * Returns the value rounded to nearest integer. {@link Double#NaN} are casted to 0 and
values out of
+     * {@code long} range are clamped to minimal or maximal representable numbers of {@code
long} type.
+     */
+    @Override
+    public final long longValue() {
+        return Math.round(derivedValue);
+    }
+
+    /**
+     * Converts this quantity to another unit of measurement.
+     */
+    @Override
+    public final Quantity<Q> to(final Unit<Q> newUnit) {
+        if (newUnit == derivedUnit) {
+            return this;
+        }
+        ArgumentChecks.ensureNonNull("unit", newUnit);      // "unit" is the parameter name
used in public API.
+        /*
+         * Do not invoke 'this.create(double, Unit)' because the contract in this subclass
+         * restricts the above method to cases where the given unit is the system unit.
+         * Furthermore we need to let 'Quantities.create(…)' re-evaluate whether we need
+         * a 'DerivedScalar' instance or whether 'Scalar' would be sufficient.
+         */
+        return Quantities.create(derivedUnit.getConverterTo(newUnit).convert(derivedValue),
newUnit);
+    }
+
+
+    /**
+     * A temperature in Celsius degrees or any other units having an offset compared to Kelvin.
+     */
+    static final class TemperatureMeasurement extends DerivedScalar<javax.measure.quantity.Temperature>
+            implements javax.measure.quantity.Temperature
+    {
+        private static final long serialVersionUID = -3901877967613695897L;
+
+        /** Constructor for {@link Quantities} factory only. */
+        TemperatureMeasurement(double value, Unit<javax.measure.quantity.Temperature>
unit,
+                    Unit<javax.measure.quantity.Temperature> systemUnit, UnitConverter
toSystem)
+        {
+            super(value, unit, systemUnit, toSystem);
+        }
+
+        /** Constructor for {@code create(…)} implementation only. */
+        private TemperatureMeasurement(TemperatureMeasurement origin, double value) {
+            super(origin, value);
+        }
+
+        @Override
+        Quantity<javax.measure.quantity.Temperature> create(double newValue, Unit<javax.measure.quantity.Temperature>
newUnit) {
+            assert newUnit == getSystemUnit() : newUnit;
+            return new TemperatureMeasurement(this, newValue);
+        }
+    }
+
+    /**
+     * Fallback used when no {@link DerivedScalar} implementation is available for a given
quantity type.
+     * This is basically a copy of {@link ScalarFallback} implementation adapted to {@code
DerivedScalar}.
+     */
+    @SuppressWarnings("serial")
+    static final class Fallback<Q extends Quantity<Q>> extends DerivedScalar<Q>
implements InvocationHandler {
+        /**
+         * The constructor for new proxy instances.
+         *
+         * @see ScalarFallback#constructor
+         */
+        private final Constructor<? extends Q> constructor;
+
+        /**
+         * Constructor for {@link Quantities} factory only.
+         */
+        private Fallback(final double value, final Unit<Q> unit, final Unit<Q>
systemUnit,
+                         final UnitConverter toSystem, final Constructor<? extends Q>
constructor)
+        {
+            super(value, unit, systemUnit, toSystem);
+            this.constructor = constructor;
+        }
+
+        /**
+         * Constructor for {@code create(…)} implementation only.
+         */
+        private Fallback(final Fallback<Q> origin, final double value) {
+            super(origin, value);
+            constructor = origin.constructor;
+        }
+
+        /**
+         * Creates a new quantity of the same type than this quantity but a different value
and/or unit.
+         *
+         * @see ScalarFallback#create(double, Unit)
+         */
+        @Override
+        Quantity<Q> create(final double newValue, final Unit<Q> newUnit) {
+            assert newUnit == getSystemUnit() : newUnit;
+            try {
+                return constructor.newInstance(new Fallback<>(this, newValue));
+            } catch (ReflectiveOperationException e) {
+                throw new UnsupportedOperationException(e);                 // Should never
happen.
+            }
+        }
+
+        /**
+         * Creates a new {@link Fallback} instance implementing the given quantity type.
+         *
+         * @see ScalarFallback#factory(double, Unit, Class)
+         */
+        @SuppressWarnings("unchecked")
+        static <Q extends Quantity<Q>> Q factory(final double value, final Unit<Q>
unit,
+                final Unit<Q> systemUnit, final UnitConverter toSystem, final Class<Q>
type)
+        {
+            final Class<?> pc = Proxy.getProxyClass(Scalar.class.getClassLoader(),
new Class<?>[] {type});
+            final Constructor<? extends Q> constructor;
+            try {
+                constructor = (Constructor<? extends Q>) pc.getConstructor(InvocationHandler.class);
+                return constructor.newInstance(new Fallback<>(value, unit, systemUnit,
toSystem, constructor));
+            } catch (ReflectiveOperationException e) {
+                throw new UnsupportedOperationException(e);                 // Should never
happen.
+            }
+        }
+
+        /**
+         * Invoked when a method of the {@link Quantity} interface is invoked.
+         *
+         * @see ScalarFallback#invoke(Object, Method, Object[])
+         */
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws ReflectiveOperationException
{
+            return method.invoke(this, args);
+        }
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
index 7fc256b..e534398 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
@@ -19,6 +19,7 @@ package org.apache.sis.measure;
 
 import javax.measure.Unit;
 import javax.measure.Quantity;
+import javax.measure.UnitConverter;
 import javax.measure.quantity.Time;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
@@ -39,7 +40,7 @@ import org.apache.sis.util.resources.Errors;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -79,12 +80,37 @@ public final class Quantities extends Static {
         ArgumentChecks.ensureNonNull("unit", unit);
         final Unit<Q> system = unit.getSystemUnit();
         if (system instanceof SystemUnit<?>) {
+            final UnitConverter c = unit.getConverterTo(system);
             final ScalarFactory<Q> factory = ((SystemUnit<Q>) system).factory;
+            if (c.isLinear()) {
+                /*
+                 * We require arithmetic operations (A + B, A * 2, etc.) to be performed
as if all values were
+                 * converted to system unit before calculation. This is mandatory for preserving
arithmetic laws
+                 * like associativity, commutativity, etc.  But in the special case were
the unit of measurement
+                 * if related to the system unit with only a scale factor (no offset), we
get equivalent results
+                 * even if we skip the conversion to system unit.  Since the vast majority
of units fall in this
+                 * category, it is worth to do this optimization.
+                 *
+                 * (Note: despite its name, above 'isLinear()' method actually has an 'isScale()'
behavior).
+                 */
+                if (factory != null) {
+                    return factory.create(value, unit);
+                } else {
+                    return ScalarFallback.factory(value, unit, ((SystemUnit<Q>) system).quantity);
+                }
+            }
+            /*
+             * If the given unit of measurement is derived from the system unit by a more
complex formula
+             * than a scale factor, then we need to perform arithmetic operations using the
full path
+             * (convert all values to system unit before calculation).
+             */
             if (factory != null) {
-                return factory.create(value, unit);
-            } else {
-                return ScalarFallback.factory(value, unit, ((SystemUnit<Q>) system).quantity);
+                final Q quantity = factory.createDerived(value, unit, system, c);
+                if (quantity != null) {
+                    return quantity;
+                }
             }
+            return DerivedScalar.Fallback.factory(value, unit, system, c, ((SystemUnit<Q>)
system).quantity);
         } else {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedImplementation_1,
unit.getClass()));
         }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Scalar.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Scalar.java
index 9050a1a..e13f0cb 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Scalar.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Scalar.java
@@ -19,6 +19,7 @@ package org.apache.sis.measure;
 import java.lang.reflect.Proxy;
 import javax.measure.Unit;
 import javax.measure.Quantity;
+import javax.measure.UnitConverter;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.StringBuilders;
 
@@ -30,7 +31,7 @@ import org.apache.sis.util.StringBuilders;
  * Instances of this class are unmodifiable.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @param <Q>  the concrete subtype.
  *
@@ -63,8 +64,20 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
 
     /**
      * Creates a new quantity of same type than this quantity but with a different value
and/or unit.
-     * This method performs the same work than {@link Quantities#create(double, Unit)}, but
without the need
-     * to check for the Apache SIS specific {@link SystemUnit} implementation.
+     * This method performs the same work than {@link Quantities#create(double, Unit)}, but
without
+     * the need to check for the Apache SIS specific {@link SystemUnit} implementation.
+     *
+     * <p>This method is invoked (indirectly) in only two situations:</p>
+     * <ul>
+     *   <li>Arithmetic operations that do not change the unit of measurement (addition,
subtraction),
+     *       in which case the given {@code newUnit} is the same that the unit of this quantity.</li>
+     *   <li>Conversion to a new compatible unit by {@link #to(Unit)}, provided that
the conversion
+     *       is only a scale factor.</li>
+     * </ul>
+     *
+     * {@link DerivedScalar} relies on the fact that there is no other situations where this
method
+     * is invoked. If this assumption become not true anymore in a future SIS version, then
we need
+     * to revisit {@code DerivedScalar}.
      *
      * @see Quantities#create(double, Unit)
      */
@@ -96,9 +109,10 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
 
     /**
      * Returns the unit of measurement specified at construction time.
+     * The method shall not return {@code null}.
      */
     @Override
-    public final Unit<Q> getUnit() {
+    public Unit<Q> getUnit() {
         return unit;
     }
 
@@ -114,7 +128,7 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
      * Returns the value specified at construction time.
      */
     @Override
-    public final double doubleValue() {
+    public double doubleValue() {
         return value;
     }
 
@@ -122,7 +136,7 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
      * Returns the value casted to a single-precision floating point number.
      */
     @Override
-    public final float floatValue() {
+    public float floatValue() {
         return (float) value;
     }
 
@@ -131,7 +145,7 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
      * {@code long} range are clamped to minimal or maximal representable numbers of {@code
long} type.
      */
     @Override
-    public final long longValue() {
+    public long longValue() {
         return Math.round(value);
     }
 
@@ -185,14 +199,33 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
 
     /**
      * Converts this quantity to another unit of measurement.
+     * This default implementation is valid only if the unit of this quantity is a system
unit,
+     * or convertible to the system unit with only a scale factor. If this assumption does
not
+     * hold anymore (as in {@link DerivedScalar} subclass), then this method needs to be
overridden.
      */
     @Override
-    public final Quantity<Q> to(final Unit<Q> newUnit) {
+    public Quantity<Q> to(final Unit<Q> newUnit) {
         if (newUnit == unit) {
             return this;
         }
         ArgumentChecks.ensureNonNull("unit", newUnit);      // "unit" is the parameter name
used in public API.
-        return create(unit.getConverterTo(newUnit).convert(value), newUnit);
+        assert unit.getConverterTo(unit.getSystemUnit()).isLinear() : unit;             
// See method javadoc.
+        final UnitConverter c = unit.getConverterTo(newUnit);
+        final double newValue = c.convert(value);
+        if (c.isLinear()) {                                 // Despite method name, this
is actually "is scale".
+            /*
+             * Conversion from this quantity to system unit was a scale factor (see assumption
documented
+             * in this method javadoc) and given conversion is also a scale factor. Consequently
conversion
+             * from the new quantity unit to system unit will still be a scale factor, in
which case this
+             * 'Scalar' class is still appropriate.
+             */
+            return create(newValue, newUnit);
+        } else {
+            /*
+             * Re-evaluate if we need to create a DerivedScalar subclass.
+             */
+            return Quantities.create(newValue, newUnit);
+        }
     }
 
     /**
@@ -280,26 +313,35 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
                 return false;
             }
         }
+        /*
+         * We require the Scalar implementation rather than accepting arbitrary Quantity<?>
instance
+         * for making sure that we obey to Object.equals(Object) contract (e.g. symmetric,
transitive,
+         * etc.). But we invoke the getter methods instead than accessing directly the fields
because
+         * DerivedScalar override them.
+         */
         final Scalar<?> that = (Scalar<?>) other;
-        return Double.doubleToLongBits(value) == Double.doubleToLongBits(that.value) &&
unit.equals(that.unit);
+        return Double.doubleToLongBits(doubleValue()) == Double.doubleToLongBits(that.doubleValue())
+                && getUnit().equals(that.getUnit());
     }
 
     /**
-     * Returns a hash code value for this quantity.
+     * Returns a hash code value for this quantity. This method computes the code from values
returned by
+     * {@link #doubleValue()} and {@link #getUnit()} methods, which may be overridden by
sub-classes.
      */
     @Override
     public final int hashCode() {
-        return Double.hashCode(value) ^ unit.hashCode();
+        return Double.hashCode(doubleValue()) ^ getUnit().hashCode();
     }
 
     /**
-     * Returns the quantity value followed by its units of measurement.
+     * Returns the quantity value followed by its units of measurement. This method uses
the values returned
+     * by {@link #doubleValue()} and {@link #getUnit()} methods, which may be overridden
by sub-classes.
      */
     @Override
     public final String toString() {
-        final StringBuilder buffer = new StringBuilder().append(value);
+        final StringBuilder buffer = new StringBuilder().append(doubleValue());
         StringBuilders.trimFractionalPart(buffer);
-        final String symbol = unit.toString();
+        final String symbol = getUnit().toString();
         if (symbol != null && !symbol.isEmpty()) {
             buffer.append(' ').append(symbol);
         }
@@ -458,18 +500,6 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
         }
     }
 
-    static final class Temperature extends Scalar<javax.measure.quantity.Temperature>
-                                   implements     javax.measure.quantity.Temperature
-    {
-        private static final long serialVersionUID = -6391507887931973739L;
-        Temperature(double value, Unit<javax.measure.quantity.Temperature> unit) {super(value,
unit);}
-
-        @Override
-        Quantity<javax.measure.quantity.Temperature> create(double value, Unit<javax.measure.quantity.Temperature>
unit) {
-            return new Temperature(value, unit);
-        }
-    }
-
     static final class Pressure extends Scalar<javax.measure.quantity.Pressure>
                                 implements     javax.measure.quantity.Pressure
     {
@@ -481,4 +511,28 @@ abstract class Scalar<Q extends Quantity<Q>> extends Number
implements Quantity<
             return new Pressure(value, unit);
         }
     }
+
+    static final class Temperature extends Scalar<javax.measure.quantity.Temperature>
+                                   implements     javax.measure.quantity.Temperature
+    {
+        static final ScalarFactory<javax.measure.quantity.Temperature> FACTORY = new
ScalarFactory<javax.measure.quantity.Temperature>() {
+            @Override public javax.measure.quantity.Temperature create(double value, Unit<javax.measure.quantity.Temperature>
unit) {
+                return new Temperature(value, unit);
+            }
+
+            @Override public javax.measure.quantity.Temperature createDerived(double value,
Unit<javax.measure.quantity.Temperature> unit,
+                    Unit<javax.measure.quantity.Temperature> systemUnit, UnitConverter
toSystem)
+            {
+                return new DerivedScalar.TemperatureMeasurement(value, unit, systemUnit,
toSystem);
+            }
+        };
+
+        private static final long serialVersionUID = -6391507887931973739L;
+        Temperature(double value, Unit<javax.measure.quantity.Temperature> unit) {super(value,
unit);}
+
+        @Override
+        Quantity<javax.measure.quantity.Temperature> create(double value, Unit<javax.measure.quantity.Temperature>
unit) {
+            return new Temperature(value, unit);
+        }
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/ScalarFactory.java b/core/sis-utility/src/main/java/org/apache/sis/measure/ScalarFactory.java
index 8d3516d..9375cea 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/ScalarFactory.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/ScalarFactory.java
@@ -18,13 +18,14 @@ package org.apache.sis.measure;
 
 import javax.measure.Unit;
 import javax.measure.Quantity;
+import javax.measure.UnitConverter;
 
 
 /**
  * Creates quantities for the given value and unit of measurement.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @param <Q>  the type of quantities created by this factory.
  *
@@ -34,10 +35,30 @@ import javax.measure.Quantity;
 interface ScalarFactory<Q extends Quantity<Q>> {
     /**
      * Creates a new quantity for the given value and unit of measurement.
+     * This method is invoked only when the given unit is convertible to
+     * system unit with a scale factor (no offset). This is the case of
+     * most units of measurement.
      *
      * @param  value  the value of the quantity to create.
      * @param  unit   the unit of measurement associated to the given value.
      * @return a quantity with the given value and unit of measurement.
      */
     Q create(double value, Unit<Q> unit);
+
+    /**
+     * Creates a new quantity for the given value and unit of measurement
+     * which is not convertible to system unit by a scale factor. Example
+     * of such quantities is a temperature measurement in Celsius degrees,
+     * since conversion to Kelvin implies an offset.
+     *
+     * @param  value       the value of the quantity to create.
+     * @param  unit        the unit of measurement associated to the given value.
+     * @param  systemUnit  {@link Unit#getSystemUnit()}, opportunistically provided because
already known by the caller.
+     * @param  toSystem    {@code unit.getConverterTo(systemUnit)}, provided because already
known by the caller.
+     * @return a quantity with the given value and unit of measurement, or
+     *         {@code null} if no pre-defined implementation is available.
+     */
+    default Q createDerived(double value, Unit<Q> unit, Unit<Q> systemUnit, UnitConverter
toSystem) {
+        return null;
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
index a29c81e..372f877 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
@@ -1091,16 +1091,16 @@ public final class Units extends Static {
         /*
          * Base, derived or alternate units that we need to reuse more than once in this
static initializer.
          */
-        final SystemUnit<Length>        m   = add(Length.class,        Scalar.Length::new,
       length,        "m",   (byte) (SI | PREFIXABLE), Constants.EPSG_METRE);
-        final SystemUnit<Area>          m2  = add(Area.class,          Scalar.Area::new,
         area,          "m²",  (byte) (SI | PREFIXABLE), (short) 0);
-        final SystemUnit<Volume>        m3  = add(Volume.class,        Scalar.Volume::new,
       length.pow(3), "m³",  (byte) (SI | PREFIXABLE), (short) 0);
-        final SystemUnit<Time>          s   = add(Time.class,          Scalar.Time::new,
         time,          "s",   (byte) (SI | PREFIXABLE), (short) 1040);
-        final SystemUnit<Temperature>   K   = add(Temperature.class,   Scalar.Temperature::new,
  temperature,   "K",   (byte) (SI | PREFIXABLE), (short) 0);
-        final SystemUnit<Speed>         mps = add(Speed.class,         Scalar.Speed::new,
        speed,         "m∕s", (byte) (SI | PREFIXABLE), (short) 1026);
-        final SystemUnit<Pressure>      Pa  = add(Pressure.class,      Scalar.Pressure::new,
     pressure,      "Pa",  (byte) (SI | PREFIXABLE), (short) 0);
-        final SystemUnit<Angle>         rad = add(Angle.class,         Scalar.Angle::new,
        dimensionless, "rad", (byte) (SI | PREFIXABLE), (short) 9101);
-        final SystemUnit<Dimensionless> one = add(Dimensionless.class, Scalar.Dimensionless::new,
dimensionless, "",            SI,               (short) 9201);
-        final SystemUnit<Mass>          kg  = add(Mass.class,          Scalar.Mass::new,
         mass,          "kg",          SI,               (short) 0);
+        final SystemUnit<Length>        m   = add(Length.class,        Scalar.Length::new,
        length,        "m",   (byte) (SI | PREFIXABLE), Constants.EPSG_METRE);
+        final SystemUnit<Area>          m2  = add(Area.class,          Scalar.Area::new,
          area,          "m²",  (byte) (SI | PREFIXABLE), (short) 0);
+        final SystemUnit<Volume>        m3  = add(Volume.class,        Scalar.Volume::new,
        length.pow(3), "m³",  (byte) (SI | PREFIXABLE), (short) 0);
+        final SystemUnit<Time>          s   = add(Time.class,          Scalar.Time::new,
          time,          "s",   (byte) (SI | PREFIXABLE), (short) 1040);
+        final SystemUnit<Temperature>   K   = add(Temperature.class,   Scalar.Temperature.FACTORY,
temperature,   "K",   (byte) (SI | PREFIXABLE), (short) 0);
+        final SystemUnit<Speed>         mps = add(Speed.class,         Scalar.Speed::new,
         speed,         "m∕s", (byte) (SI | PREFIXABLE), (short) 1026);
+        final SystemUnit<Pressure>      Pa  = add(Pressure.class,      Scalar.Pressure::new,
      pressure,      "Pa",  (byte) (SI | PREFIXABLE), (short) 0);
+        final SystemUnit<Angle>         rad = add(Angle.class,         Scalar.Angle::new,
         dimensionless, "rad", (byte) (SI | PREFIXABLE), (short) 9101);
+        final SystemUnit<Dimensionless> one = add(Dimensionless.class, Scalar.Dimensionless::new,
 dimensionless, "",            SI,               (short) 9201);
+        final SystemUnit<Mass>          kg  = add(Mass.class,          Scalar.Mass::new,
          mass,          "kg",          SI,               (short) 0);
         /*
          * All SI prefix to be used below, with additional converters to be used more than
once.
          */
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
index 22e0621..5d726d2 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
@@ -19,18 +19,19 @@ package org.apache.sis.measure;
 import javax.measure.Unit;
 import javax.measure.Quantity;
 import javax.measure.quantity.Length;
+import javax.measure.quantity.Temperature;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.opengis.test.Assert.*;
 
 
 /**
  * Tests {@link Quantities}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -72,4 +73,41 @@ public final strictfp class QuantitiesTest extends TestCase {
         assertEquals("value", 8, c.getValue().doubleValue(), STRICT);
         assertSame  ("unit", Units.CENTIMETRE, c.getUnit());
     }
+
+    /**
+     * Tests operations on temperature. The values shall be converted to Kelvin before any
operation.
+     * This produces counter-intuitive result, but is the only way to get results that are
consistent
+     * with arithmetic rules like commutativity and associativity.
+     *
+     * @since 1.0
+     */
+    @Test
+    public void testTemperature() {
+        final Quantity<Temperature> q1 = Quantities.create( 2, Units.CELSIUS);
+        final Quantity<Temperature> q2 = Quantities.create( 3, Units.KELVIN);
+        final Quantity<Temperature> q3 = Quantities.create(-8, Units.CELSIUS);
+
+        assertInstanceOf( "2°C", DerivedScalar.TemperatureMeasurement.class, q1);
+        assertInstanceOf( "3 K", DerivedScalar.Temperature.class,            q2);
+        assertInstanceOf("-8°C", DerivedScalar.TemperatureMeasurement.class, q3);
+
+        Quantity<Temperature> r = q1.add(q2);
+        assertSame  ("unit",  Units.CELSIUS, r.getUnit());
+        assertEquals("value", 5, r.getValue().doubleValue(), 1E-13);
+
+        r = q2.add(q1);
+        assertSame  ("unit",  Units.KELVIN, r.getUnit());
+        assertEquals("value", 278.15, r.getValue().doubleValue(), 1E-13);
+
+        r = q1.add(q3);
+        assertSame  ("unit",  Units.CELSIUS, r.getUnit());
+        assertEquals("value", 267.15, r.getValue().doubleValue(), 1E-13);
+
+        r = q1.multiply(3);
+        assertSame  ("unit",  Units.CELSIUS, r.getUnit());
+        assertEquals("value", 552.3, r.getValue().doubleValue(), 1E-13);
+
+        r = q1.multiply(1);
+        assertSame(q1, r);
+    }
 }


Mime
View raw message