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

commit 534ca09f9109ec7377857f67b14fe54b3fefe2a0
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jun 13 15:58:21 2024 +0200

    Move `TimeMethods` to `org.apache.sis.temporal` for allowing its use in the
    implementation of `DefaultInstant.findRelativePosition(TemporalPrimitive)`.
---
 .../main/org/apache/sis/filter/TemporalFilter.java |  21 +++-
 .../org/apache/sis/filter/TemporalOperation.java   |  25 ++--
 .../org/apache/sis/temporal/DefaultInstant.java    | 101 +++++++++++++--
 .../org/apache/sis/temporal/GeneralDuration.java   |  30 ++---
 .../main/org/apache/sis/temporal}/TimeMethods.java | 105 +++++++++-------
 .../apache/sis/temporal/DefaultInstantTest.java    | 136 +++++++++++++++++++++
 .../org/apache/sis/temporal/DefaultPeriodTest.java |  14 ++-
 .../test/org/apache/sis/io/wkt/ElementTest.java    |   2 +-
 .../main/org/apache/sis/util/resources/Errors.java |   2 +-
 .../apache/sis/util/resources/Errors.properties    |   2 +-
 10 files changed, 359 insertions(+), 79 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
index 88ebd77415..38d1984a85 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
@@ -16,7 +16,10 @@
  */
 package org.apache.sis.filter;
 
+import java.time.DateTimeException;
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.temporal.TimeMethods;
 import org.apache.sis.feature.privy.FeatureExpression;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -25,6 +28,7 @@ import org.opengis.filter.Filter;
 import org.opengis.filter.Expression;
 import org.opengis.filter.TemporalOperator;
 import org.opengis.filter.TemporalOperatorName;
+import org.opengis.filter.InvalidFilterValueException;
 
 
 /**
@@ -175,13 +179,15 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
     /**
      * Determines if the test(s) represented by this filter passes with the 
given operands.
      * Values of {@link #expression1} and {@link #expression2} shall be two 
single values.
+     *
+     * @throws InvalidFilterValueException if two temporal objects cannot be 
compared.
      */
     @Override
     public boolean test(final R candidate) {
         final T left = expression1.apply(candidate);
         if (left != null) {
             final T right = expression2.apply(candidate);
-            if (right != null) {
+            if (right != null) try {
                 if (left instanceof Period) {
                     if (right instanceof Period) {
                         return operation.evaluate((Period) left, (Period) 
right);
@@ -193,6 +199,9 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
                 } else {
                     return operation.evaluate(left, right);
                 }
+            } catch (DateTimeException e) {
+                throw new InvalidFilterValueException(Errors.format(
+                        Errors.Keys.CannotCompareInstanceOf_2, 
left.getClass(), right.getClass()));
             }
         }
         return false;
@@ -227,8 +236,11 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
             final T left = expression1.apply(candidate);
             if (left != null) {
                 final T right = expression2.apply(candidate);
-                if (right != null) {
+                if (right != null) try {
                     return operation.evaluate(left, right);
+                } catch (DateTimeException e) {
+                    throw new InvalidFilterValueException(Errors.format(
+                            Errors.Keys.CannotCompareInstanceOf_2, 
left.getClass(), right.getClass()));
                 }
             }
             return false;
@@ -264,8 +276,11 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
             final Period left = expression1.apply(candidate);
             if (left != null) {
                 final Period right = expression2.apply(candidate);
-                if (right != null) {
+                if (right != null) try {
                     return operation.evaluate(left, right);
+                } catch (DateTimeException e) {
+                    throw new InvalidFilterValueException(Errors.format(
+                            Errors.Keys.CannotCompareInstanceOf_2, 
left.getClass(), right.getClass()));
                 }
             }
             return false;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
index 7918cc3f82..1e6ae3ed5d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
@@ -17,13 +17,15 @@
 package org.apache.sis.filter;
 
 import java.io.Serializable;
+import java.time.DateTimeException;
 import java.time.temporal.Temporal;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.collection.WeakHashSet;
-import static org.apache.sis.filter.TimeMethods.BEFORE;
-import static org.apache.sis.filter.TimeMethods.AFTER;
-import static org.apache.sis.filter.TimeMethods.EQUAL;
+import org.apache.sis.temporal.TimeMethods;
+import static org.apache.sis.temporal.TimeMethods.BEFORE;
+import static org.apache.sis.temporal.TimeMethods.AFTER;
+import static org.apache.sis.temporal.TimeMethods.EQUAL;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.temporal.Period;
@@ -142,6 +144,9 @@ abstract class TemporalOperation<T> implements Serializable 
{
      *
      * <p><b>Note:</b> this relationship is not defined by ISO 19108. This 
method should be overridden
      * only when an ISO 19108 extension can be easily defined, for example for 
the "equal" operation.</p>
+     *
+     * @throws DateTimeException if two temporal objects cannot be compared.
+     * @throws ArithmeticException if the comparison exceeds integer capacity.
      */
     protected boolean evaluate(T self, Period other) {
         return false;
@@ -151,6 +156,9 @@ abstract class TemporalOperation<T> implements Serializable 
{
      * Evaluates the filter between a period and a temporal object.
      * Both arguments given to this method shall be non-null, but period begin 
or end instant may be null.
      * Note: the {@code self} and {@code other} argument names are chosen to 
match ISO 19108 tables.
+     *
+     * @throws DateTimeException if two temporal objects cannot be compared.
+     * @throws ArithmeticException if the comparison exceeds integer capacity.
      */
     protected boolean evaluate(Period self, T other) {
         return false;
@@ -160,6 +168,9 @@ abstract class TemporalOperation<T> implements Serializable 
{
      * Evaluates the filter between two periods.
      * Both arguments given to this method shall be non-null, but period begin 
or end instant may be null.
      * Note: the {@code self} and {@code other} argument names are chosen to 
match ISO 19108 tables.
+     *
+     * @throws DateTimeException if two temporal objects cannot be compared.
+     * @throws ArithmeticException if the comparison exceeds integer capacity.
      */
     protected abstract boolean evaluate(Period self, Period other);
 
@@ -197,9 +208,9 @@ abstract class TemporalOperation<T> implements Serializable 
{
      * @param  self   the object on which to invoke the method identified by 
{@code test}.
      * @param  other  the argument to give to the test method call, or {@code 
null} if none.
      * @return the result of performing the comparison identified by {@code 
test}.
-     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     * @throws DateTimeException if the two objects cannot be compared.
      */
-    final boolean compare(final int test, final T self, final Temporal other) {
+    protected final boolean compare(final int test, final T self, final 
Temporal other) {
         return (other != null) && comparators.compare(test, self, other);
     }
 
@@ -211,10 +222,10 @@ abstract class TemporalOperation<T> implements 
Serializable {
      * @param  self   the object on which to invoke the method identified by 
{@code test}, or {@code null} if none.
      * @param  other  the argument to give to the test method call, or {@code 
null} if none.
      * @return the result of performing the comparison identified by {@code 
test}.
-     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     * @throws DateTimeException if the two objects cannot be compared.
      */
     @SuppressWarnings("unchecked")
-    static boolean compare(final int test, final Temporal self, final Temporal 
other) {
+    protected static boolean compare(final int test, final Temporal self, 
final Temporal other) {
         return (self != null) && (other != null) && TimeMethods.compare(test,
                 (Class) Classes.findCommonClass(self.getClass(), 
other.getClass()), self, other);
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
index f8c0a5af14..ea299d910d 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
@@ -21,8 +21,10 @@ import java.util.Optional;
 import java.io.Serializable;
 import java.time.Duration;
 import java.time.DateTimeException;
+import java.time.ZonedDateTime;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAmount;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 
@@ -32,6 +34,7 @@ import org.opengis.temporal.Instant;
 import org.opengis.temporal.TemporalPrimitive;
 import org.opengis.temporal.IndeterminateValue;
 import org.opengis.filter.TemporalOperatorName;
+import org.opengis.temporal.IndeterminatePositionException;
 
 
 /**
@@ -77,11 +80,11 @@ final class DefaultInstant implements Instant, Serializable 
{
      * @return the date, time or position on the time-scale represented by 
this primitive.
      */
     @Override
-    public Temporal getPosition() {
+    public final Temporal getPosition() {
         if (indeterminate != IndeterminateValue.NOW) {
             return position;
         }
-        return java.time.Instant.now();
+        return ZonedDateTime.now();
     }
 
     /**
@@ -90,7 +93,7 @@ final class DefaultInstant implements Instant, Serializable {
      * @return the reason why the temporal position is missing or inaccurate.
      */
     @Override
-    public Optional<IndeterminateValue> getIndeterminatePosition() {
+    public final Optional<IndeterminateValue> getIndeterminatePosition() {
         return Optional.ofNullable(indeterminate);
     }
 
@@ -107,7 +110,8 @@ final class DefaultInstant implements Instant, Serializable 
{
         ArgumentChecks.ensureNonNull("other", other);
         if (other instanceof Instant) {
             return GeneralDuration.distance(this, (Instant) other, false, 
true);
-        } else if (other instanceof Period) {
+        }
+        if (other instanceof Period) {
             final var p = (Period) other;
             TemporalAmount t = GeneralDuration.distance(this, 
p.getBeginning(), false, false);
             if (t == null) {
@@ -117,13 +121,12 @@ final class DefaultInstant implements Instant, 
Serializable {
                 }
             }
             return t;
-        } else {
-            throw new 
DateTimeException(Errors.format(Errors.Keys.UnsupportedType_1, 
other.getClass()));
         }
+        throw new 
DateTimeException(Errors.format(Errors.Keys.UnsupportedType_1, 
other.getClass()));
     }
 
     /**
-     * Determines the position of this primitive relative to another temporal 
primitive.
+     * Determines the position of this instant relative to another temporal 
primitive.
      * The relative position is identified by an operator which evaluates to 
{@code true}
      * when the two operands are {@code this} and {@code other}.
      *
@@ -133,7 +136,89 @@ final class DefaultInstant implements Instant, 
Serializable {
      */
     @Override
     public TemporalOperatorName findRelativePosition(final TemporalPrimitive 
other) {
-        throw new UnsupportedOperationException();
+        ArgumentChecks.ensureNonNull("other", other);
+        if (other instanceof Instant) {
+            return relativeToInstant((Instant) other);
+        }
+        if (other instanceof Period) {
+            final var period = (Period) other;
+            TemporalOperatorName relation = 
relativeToInstant(period.getBeginning());
+            String erroneous;
+            if (relation == TemporalOperatorName.BEFORE) return relation;
+            if (relation == TemporalOperatorName.EQUALS) return 
TemporalOperatorName.BEGINS;
+            if (relation == TemporalOperatorName.AFTER) {
+                relation = relativeToInstant(period.getEnding());
+                if (relation == TemporalOperatorName.AFTER)  return relation;
+                if (relation == TemporalOperatorName.EQUALS) return 
TemporalOperatorName.ENDS;
+                if (relation == TemporalOperatorName.BEFORE) return 
TemporalOperatorName.DURING;
+                erroneous = "ending";
+            } else {
+                erroneous = "beginning";
+            }
+            throw new 
DateTimeException(Errors.format(Errors.Keys.IllegalMapping_2, erroneous, 
relation));
+        }
+        throw new 
DateTimeException(Errors.format(Errors.Keys.UnsupportedType_1, 
other.getClass()));
+    }
+
+    /**
+     * Determines the position of this instant relative to another instant.
+     *
+     * @param  other the other instant for which to determine the relative 
position.
+     * @return a temporal operator which is true when evaluated between this 
primitive and the other primitive.
+     * @throws DateTimeException if the temporal objects cannot be compared.
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})    // See end of method.
+    private TemporalOperatorName relativeToInstant(final Instant other) {
+        boolean canTestBefore = true;
+        boolean canTestAfter  = true;
+        boolean canTestEqual  = true;
+        if (indeterminate != null && indeterminate != IndeterminateValue.NOW) {
+            canTestBefore = (indeterminate == IndeterminateValue.BEFORE);
+            canTestAfter  = (indeterminate == IndeterminateValue.AFTER);
+            canTestEqual  = false;
+        }
+        final IndeterminateValue oip = 
other.getIndeterminatePosition().orElse(null);
+        if (oip != null) {
+            if (oip != IndeterminateValue.NOW) {
+                canTestBefore &= (oip == IndeterminateValue.AFTER);
+                canTestAfter  &= (oip == IndeterminateValue.BEFORE);
+                canTestEqual   = false;
+            } else if (indeterminate == IndeterminateValue.NOW) {
+                return TemporalOperatorName.EQUALS;
+            }
+        }
+cmp:    if (canTestBefore | canTestAfter | canTestEqual) {
+            final Temporal t1;                  // Same as `this.position` 
except if "now".
+            final Temporal t2;                  // Position of the other 
instant.
+            final TimeMethods<?> comparators;   // The "is before", "is after" 
and "is equal" methods to invoke.
+            /*
+             * First, resolve the case when the indeterminate value is "now". 
Do not invoke `getPosition()`
+             * because the results could differ by a few nanoseconds when two 
"now" instants are compared,
+             * and also for getting a temporal object of the same type than 
the other instant.
+             */
+            if (oip == IndeterminateValue.NOW) {
+                t1 = position;
+                if (t1 == null) break cmp;
+                comparators = TimeMethods.find(t1.getClass());
+                t2 = comparators.now();
+            } else {
+                t2 = other.getPosition();
+                if (t2 == null) break cmp;
+                if (indeterminate == IndeterminateValue.NOW) {
+                    comparators = TimeMethods.find(t2.getClass());
+                    t1 = comparators.now();
+                } else {
+                    t1 = position;
+                    if (t1 == null) break cmp;
+                    comparators = 
TimeMethods.find(Classes.findCommonClass(t1.getClass(), t2.getClass()));
+                }
+            }
+            // This is where the @SuppressWarnings(…) apply.
+            if (canTestBefore && ((TimeMethods) comparators).isBefore.test(t1, 
t2)) return TemporalOperatorName.BEFORE;
+            if (canTestAfter  && ((TimeMethods) comparators).isAfter .test(t1, 
t2)) return TemporalOperatorName.AFTER;
+            if (canTestEqual  && ((TimeMethods) comparators).isEqual .test(t1, 
t2)) return TemporalOperatorName.EQUALS;
+        }
+        throw new 
IndeterminatePositionException(Errors.format(Errors.Keys.IndeterminatePosition));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
index 6b9b49ccf7..f7b08ab404 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
@@ -17,7 +17,6 @@
 package org.apache.sis.temporal;
 
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.io.Serializable;
 import java.time.Period;
@@ -133,14 +132,11 @@ final class GeneralDuration implements TemporalAmount, 
Serializable {
          */
         Temporal t1 = getDeterminatePosition(self);
         Temporal t2 = getDeterminatePosition(other);
-        if (Objects.equals(t1, t2)) {
-            return Duration.ZERO;
-        }
         /*
          * Ensures that the given objects both have a date part, or that none 
of them have a date part.
          * Note that the "epoch day" field is supported by `LocalDate` as well 
as the dates with zone ID.
          */
-        boolean hasDate = isSupportedByBoth(ChronoField.EPOCH_DAY, t1, t2);
+        final boolean hasDate = isSupportedByBoth(ChronoField.EPOCH_DAY, t1, 
t2);
         /*
          * If at least one date has a timezone, then we require that both 
dates have a timezone.
          * It allows an unambiguous duration in number of days, without 
time-varying months or years.
@@ -174,7 +170,6 @@ final class GeneralDuration implements TemporalAmount, 
Serializable {
             if (!absolute && (negate ? d1.isBefore(d2) : d1.isAfter(d2))) {
                 return null;        // Stop early if we can.
             }
-            hasDate = !d1.isEqual(d2);
         }
         /*
          * Compute the duration in the time part. If negative (after negation 
if `negate` is true),
@@ -186,14 +181,17 @@ final class GeneralDuration implements TemporalAmount, 
Serializable {
         if (hasTime) {
             time = Duration.between(LocalTime.from(t1), LocalTime.from(t2));
             if (hasDate) {
-                if (negate ? JDK18.isPositive(time) : time.isNegative()) {
-                    long n = time.toDays();                     // Truncated 
toward 0.
-                    if (negate) {
-                        d1 = d1.plus(++n, ChronoUnit.DAYS);     // `n` is 
positive. Reduces period by increasing the beginning.
-                    } else {
-                        d2 = d2.plus(--n, ChronoUnit.DAYS);     // `n` is 
negative. Reduces period by decreasing the ending.
+                final boolean isPositive = d1.isBefore(d2);
+                if (isPositive || d1.isAfter(d2)) {                 // Require 
the period to be non-zero.
+                    if (isPositive ? time.isNegative() : 
JDK18.isPositive(time)) {
+                        long n = time.toDays();                     // 
Truncated toward 0.
+                        if (isPositive) {
+                            d2 = d2.plus (--n, ChronoUnit.DAYS);    // `n` is 
negative. Reduces period by decreasing the ending.
+                        } else {
+                            d1 = d1.minus(++n, ChronoUnit.DAYS);    // `n` is 
positive. Reduces period by increasing the beginning.
+                        }
+                        time = time.minusDays(n);                   // If 
negative, make positive. If positive, make negative.
                     }
-                    time = time.minusDays(n);                   // If 
negative, make positive. If positive, make negative.
                 }
             }
         }
@@ -203,7 +201,11 @@ final class GeneralDuration implements TemporalAmount, 
Serializable {
          */
         if (hasDate) {
             ChronoPeriod period = d1.until(d2);
-            if (!period.isZero()) {
+            if (period.isZero()) {
+                if (time.isZero()) {
+                    return period;
+                }
+            } else {
                 if (period.isNegative()) {
                     if (!(negate | absolute)) {                 // Equivalent 
to (!negate && !absolute).
                         return null;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
similarity index 84%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
rename to 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
index 11cc0ca6b7..a949c3d6cf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
@@ -14,14 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter;
+package org.apache.sis.temporal;
 
 import java.util.Map;
 import java.util.Date;
+import java.util.function.Supplier;
+import java.util.function.BiPredicate;
 import java.time.Instant;
 import java.time.Year;
 import java.time.YearMonth;
 import java.time.MonthDay;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
 import java.time.LocalTime;
 import java.time.OffsetTime;
 import java.time.OffsetDateTime;
@@ -32,23 +37,19 @@ import java.time.chrono.ChronoZonedDateTime;
 import java.time.temporal.ChronoField;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAccessor;
-import java.util.function.BiPredicate;
 import java.lang.reflect.Modifier;
 import java.io.Serializable;
 import java.io.ObjectStreamException;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.resources.Errors;
 
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.filter.InvalidFilterValueException;
-
 
 /**
  * Provides the <i>is before</i> and <i>is after</i> operations for various 
{@code java.time} objects.
  * This class delegates to the {@code isBefore(T)} or {@code isAfter(T)} 
methods of each supported classes.
  *
- * Instances of this classes are immutable and thread-safe.
- * The same instance can be shared by many {@link TemporalOperation} instances.
+ * <p>Instances of this classes are immutable and thread-safe.
+ * The same instance can be shared by many {@link TemporalOperation} 
instances.</p>
  *
  * <h2>Design note about alternative approaches</h2>
  * We do not delegate to {@link Comparable#compareTo(Object)} because the 
latter method compares not only
@@ -61,7 +62,7 @@ import org.opengis.filter.InvalidFilterValueException;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-class TimeMethods<T> implements Serializable {
+public class TimeMethods<T> implements Serializable {
     /**
      * For cross-version compatibility.
      */
@@ -77,7 +78,7 @@ class TimeMethods<T> implements Serializable {
      *
      * @see #compare(int, T, TemporalAccessor)
      */
-    static final int BEFORE=1, AFTER=2, EQUAL=0;
+    public static final int BEFORE=1, AFTER=2, EQUAL=0;
 
     /**
      * Predicate to execute for testing the ordering between temporal objects.
@@ -86,6 +87,14 @@ class TimeMethods<T> implements Serializable {
      */
     public final transient BiPredicate<T,T> isBefore, isAfter, isEqual;
 
+    /**
+     * Supplier of the current time.
+     * May be {@code null} if we do not know how to create an object of the 
expected {@linkplain #type}.
+     *
+     * @see #now()
+     */
+    public final transient Supplier<T> now;
+
     /**
      * Creates a new set of operators. This method is for subclasses only.
      * For getting a {@code TimeMethods} instance, see {@link #find(Class)}.
@@ -93,12 +102,14 @@ class TimeMethods<T> implements Serializable {
     private TimeMethods(final Class<T> type,
             final BiPredicate<T,T> isBefore,
             final BiPredicate<T,T> isAfter,
-            final BiPredicate<T,T> isEqual)
+            final BiPredicate<T,T> isEqual,
+            final Supplier<T> now)
     {
         this.type     = type;
         this.isBefore = isBefore;
         this.isAfter  = isAfter;
         this.isEqual  = isEqual;
+        this.now      = now;
     }
 
     /**
@@ -138,10 +149,10 @@ class TimeMethods<T> implements Serializable {
      * @param  self   the object on which to invoke the method identified by 
{@code test}.
      * @param  other  the argument to give to the test method call.
      * @return the result of performing the comparison identified by {@code 
test}.
-     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     * @throws DateTimeException if the two objects cannot be compared.
      */
     @SuppressWarnings("unchecked")
-    final boolean compare(final int test, final T self, final TemporalAccessor 
other) {
+    public final boolean compare(final int test, final T self, final 
TemporalAccessor other) {
         if (type.isInstance(other)) {
             return delegate(test, self, (T) other);         // Safe because of 
above `isInstance(…)` check.
         }
@@ -159,9 +170,9 @@ class TimeMethods<T> implements Serializable {
      * @param  self   the object on which to invoke the method identified by 
{@code test}.
      * @param  other  the argument to give to the test method call.
      * @return the result of performing the comparison identified by {@code 
test}.
-     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     * @throws DateTimeException if the two objects cannot be compared.
      */
-    static <T> boolean compare(final int test, final Class<T> type, final T 
self, final T other) {
+    public static <T> boolean compare(final int test, final Class<T> type, 
final T self, final T other) {
         /*
          * The following cast is not strictly true, it should be `<? extends 
T>`.
          * However, because of the `isInstance(…)` check and because <T> is 
used
@@ -218,7 +229,7 @@ class TimeMethods<T> implements Serializable {
         } else if (value instanceof Date) {
             return ((Date) value).toInstant();      // Overridden in `Date` 
subclasses.
         } else {
-            throw new InvalidFilterValueException(Errors.format(
+            throw new DateTimeException(Errors.format(
                     Errors.Keys.CannotCompareInstanceOf_2, value.getClass(), 
TemporalAccessor.class));
         }
     }
@@ -231,24 +242,19 @@ class TimeMethods<T> implements Serializable {
      * @param  self   the object on which to invoke the method identified by 
{@code test}.
      * @param  other  the argument to give to the test method call.
      * @return the result of performing the comparison identified by {@code 
test}.
-     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     * @throws DateTimeException if the two objects cannot be compared.
      */
     private static boolean compareAsInstants(final int test, final 
TemporalAccessor self, final TemporalAccessor other) {
-        try {
-            long t1 =  self.getLong(ChronoField.INSTANT_SECONDS);
-            long t2 = other.getLong(ChronoField.INSTANT_SECONDS);
+        long t1 =  self.getLong(ChronoField.INSTANT_SECONDS);
+        long t2 = other.getLong(ChronoField.INSTANT_SECONDS);
+        if (t1 == t2) {
+            t1 =  self.getLong(ChronoField.NANO_OF_SECOND);     // Should be 
present according Javadoc.
+            t2 = other.getLong(ChronoField.NANO_OF_SECOND);
             if (t1 == t2) {
-                t1 =  self.getLong(ChronoField.NANO_OF_SECOND);     // Should 
be present according Javadoc.
-                t2 = other.getLong(ChronoField.NANO_OF_SECOND);
-                if (t1 == t2) {
-                    return test == EQUAL;
-                }
+                return test == EQUAL;
             }
-            return test == ((t1 < t2) ? BEFORE : AFTER);
-        } catch (DateTimeException | ArithmeticException e) {
-            throw new InvalidFilterValueException(Errors.format(
-                    Errors.Keys.CannotCompareInstanceOf_2, self.getClass(), 
other.getClass()), e);
         }
+        return test == ((t1 < t2) ? BEFORE : AFTER);
     }
 
     /**
@@ -295,9 +301,10 @@ class TimeMethods<T> implements Serializable {
                 return new TimeMethods<>(type,
                         (self, other) -> ((Comparable) self).compareTo(other) 
< 0,
                         (self, other) -> ((Comparable) self).compareTo(other) 
> 0,
-                        (self, other) -> ((Comparable) self).compareTo(other) 
== 0);
+                        (self, other) -> ((Comparable) self).compareTo(other) 
== 0,
+                        null);
             } else {
-                throw new 
InvalidFilterValueException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
 type, type));
+                throw new 
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2, type, 
type));
             }
         } else {
             return fallback(type);
@@ -315,7 +322,8 @@ class TimeMethods<T> implements Serializable {
         return new TimeMethods<>(type,
                 (self, other) -> compare(BEFORE, type, self, other),
                 (self, other) -> compare(AFTER,  type, self, other),
-                (self, other) -> compare(EQUAL,  type, self, other))
+                (self, other) -> compare(EQUAL,  type, self, other),
+                null)
         {
             @Override public boolean isDynamic() {
                 return true;
@@ -337,16 +345,26 @@ class TimeMethods<T> implements Serializable {
         return find(type);
     }
 
+    /**
+     * Returns the current time as a temporal object.
+     *
+     * @return the current time.
+     * @throws ClassCastException if the {@linkplain #type} is {@link Date} or 
{@link MonthDay}.
+     */
+    final Temporal now() {
+        return (now != null) ? (Temporal) now.get() : ZonedDateTime.now();
+    }
+
     /**
      * Operators for all supported temporal types that are interfaces or 
non-final classes.
      * Those types need to be checked with {@link 
Class#isAssignableFrom(Class)} in iteration order.
      */
     @SuppressWarnings({"rawtypes", "unchecked"})            // For `Chrono*` 
interfaces, because they are parameterized.
     private static final TimeMethods<?>[] INTERFACES = {
-        new TimeMethods<>(ChronoZonedDateTime.class, 
ChronoZonedDateTime::isBefore, ChronoZonedDateTime::isAfter, 
ChronoZonedDateTime::isEqual),
-        new TimeMethods<>(ChronoLocalDateTime.class, 
ChronoLocalDateTime::isBefore, ChronoLocalDateTime::isAfter, 
ChronoLocalDateTime::isEqual),
-        new TimeMethods<>(    ChronoLocalDate.class,     
ChronoLocalDate::isBefore,     ChronoLocalDate::isAfter,     
ChronoLocalDate::isEqual),
-        new TimeMethods<>(               Date.class,                Date::  
before,                Date::  after,                Date::equals)
+        new TimeMethods<>(ChronoZonedDateTime.class, 
ChronoZonedDateTime::isBefore, ChronoZonedDateTime::isAfter, 
ChronoZonedDateTime::isEqual, ZonedDateTime::now),
+        new TimeMethods<>(ChronoLocalDateTime.class, 
ChronoLocalDateTime::isBefore, ChronoLocalDateTime::isAfter, 
ChronoLocalDateTime::isEqual, LocalDateTime::now),
+        new TimeMethods<>(    ChronoLocalDate.class,     
ChronoLocalDate::isBefore,     ChronoLocalDate::isAfter,     
ChronoLocalDate::isEqual,     LocalDate::now),
+        new TimeMethods<>(               Date.class,                Date::  
before,                Date::  after,                Date::equals,           
Date::new)
     };
 
     /*
@@ -366,13 +384,16 @@ class TimeMethods<T> implements Serializable {
      * the code working on generic {@link Comparable} needs to check for 
special cases again.
      */
     private static final Map<Class<?>, TimeMethods<?>> FINAL_TYPES = 
Map.ofEntries(
-        entry(new TimeMethods<>(OffsetDateTime.class, 
OffsetDateTime::isBefore, OffsetDateTime::isAfter, OffsetDateTime::isEqual)),
-        entry(new TimeMethods<>(    OffsetTime.class,     
OffsetTime::isBefore,     OffsetTime::isAfter,     OffsetTime::isEqual)),
-        entry(new TimeMethods<>(     LocalTime.class,      
LocalTime::isBefore,      LocalTime::isAfter,      LocalTime::equals)),
-        entry(new TimeMethods<>(          Year.class,           
Year::isBefore,           Year::isAfter,           Year::equals)),
-        entry(new TimeMethods<>(     YearMonth.class,      
YearMonth::isBefore,      YearMonth::isAfter,      YearMonth::equals)),
-        entry(new TimeMethods<>(      MonthDay.class,       
MonthDay::isBefore,       MonthDay::isAfter,       MonthDay::equals)),
-        entry(new TimeMethods<>(       Instant.class,        
Instant::isBefore,        Instant::isAfter,        Instant::equals)),
+        entry(new TimeMethods<>(OffsetDateTime.class, 
OffsetDateTime::isBefore, OffsetDateTime::isAfter, OffsetDateTime::isEqual, 
OffsetDateTime::now)),
+        entry(new TimeMethods<>( ZonedDateTime.class,  
ZonedDateTime::isBefore,  ZonedDateTime::isAfter,  ZonedDateTime::isEqual,  
ZonedDateTime::now)),
+        entry(new TimeMethods<>( LocalDateTime.class,  
LocalDateTime::isBefore,  LocalDateTime::isAfter,  LocalDateTime::isEqual,  
LocalDateTime::now)),
+        entry(new TimeMethods<>(     LocalDate.class,      
LocalDate::isBefore,      LocalDate::isAfter,      LocalDate::isEqual,      
LocalDate::now)),
+        entry(new TimeMethods<>(    OffsetTime.class,     
OffsetTime::isBefore,     OffsetTime::isAfter,     OffsetTime::isEqual,     
OffsetTime::now)),
+        entry(new TimeMethods<>(     LocalTime.class,      
LocalTime::isBefore,      LocalTime::isAfter,      LocalTime::equals,       
LocalTime::now)),
+        entry(new TimeMethods<>(          Year.class,           
Year::isBefore,           Year::isAfter,           Year::equals,            
Year::now)),
+        entry(new TimeMethods<>(     YearMonth.class,      
YearMonth::isBefore,      YearMonth::isAfter,      YearMonth::equals,       
YearMonth::now)),
+        entry(new TimeMethods<>(      MonthDay.class,       
MonthDay::isBefore,       MonthDay::isAfter,       MonthDay::equals,        
MonthDay::now)),
+        entry(new TimeMethods<>(       Instant.class,        
Instant::isBefore,        Instant::isAfter,        Instant::equals,         
Instant::now)),
         entry(fallback(Temporal.class)),    // Frequently declared type. 
Intentionally no "instance of" checks.
         entry(fallback(Object.class)));     // Not a final class, but to be 
used when the declared type is Object.
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultInstantTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultInstantTest.java
index d175036219..dd8acb56e8 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultInstantTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultInstantTest.java
@@ -16,13 +16,22 @@
  */
 package org.apache.sis.temporal;
 
+import java.time.Period;
+import java.time.Duration;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.ZoneOffset;
+import org.opengis.temporal.TemporalPrimitive;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.TemporalOperatorName;
 import org.opengis.temporal.IndeterminateValue;
+import org.opengis.temporal.IndeterminatePositionException;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 
@@ -76,4 +85,131 @@ public final class DefaultInstantTest extends TestCase {
         assertEquals("2010-05-01", new DefaultInstant(date, null).toString());
         assertEquals("after 2010-05-01", new DefaultInstant(date, 
IndeterminateValue.AFTER).toString());
     }
+
+    /**
+     * Tests {@link DefaultInstant#findRelativePosition(TemporalPrimitive)} 
between instants.
+     */
+    @Test
+    public void testRelativePositionBetweenInstants() {
+        final var t1981 = new DefaultInstant(LocalDate.of(1981, 6, 5), null);
+        final var t2000 = new DefaultInstant(LocalDate.of(2000, 1, 1), null);
+        assertEquals(TemporalOperatorName.BEFORE, 
t1981.findRelativePosition(t2000));
+        assertEquals(TemporalOperatorName.AFTER,  
t2000.findRelativePosition(t1981));
+        assertEquals(TemporalOperatorName.EQUALS, 
t2000.findRelativePosition(t2000));
+    }
+
+    /**
+     * Tests {@link DefaultInstant#findRelativePosition(TemporalPrimitive)} 
between an instant and a period.
+     */
+    @Test
+    public void testRelativePositionBetweenInstantAndPeriod() {
+        final var before = new DefaultInstant(LocalDate.of(1981, 1, 1), null);
+        final var begins = new DefaultInstant(LocalDate.of(1981, 6, 5), null);
+        final var during = new DefaultInstant(LocalDate.of(1990, 1, 1), null);
+        final var ends   = new DefaultInstant(LocalDate.of(2000, 1, 1), null);
+        final var after  = new DefaultInstant(LocalDate.of(2000, 1, 2), null);
+        final var period = new DefaultPeriod(begins, ends);
+        assertEquals(TemporalOperatorName.BEFORE, 
before.findRelativePosition(period));
+        assertEquals(TemporalOperatorName.BEGINS, 
begins.findRelativePosition(period));
+        assertEquals(TemporalOperatorName.DURING, 
during.findRelativePosition(period));
+        assertEquals(TemporalOperatorName.ENDS,   ends  
.findRelativePosition(period));
+        assertEquals(TemporalOperatorName.AFTER,  after 
.findRelativePosition(period));
+    }
+
+    /**
+     * Tests {@link DefaultInstant#findRelativePosition(TemporalPrimitive)} 
with indeterminate instants.
+     * The position tested are "before" and "after".
+     */
+    @Test
+    public void testIndeterminatePosition() {
+        final var before2000 = new DefaultInstant(LocalDate.of(2000, 1, 1), 
IndeterminateValue.BEFORE);
+        final var  after2010 = new DefaultInstant(LocalDate.of(2010, 1, 1), 
IndeterminateValue.AFTER);
+        final var before2020 = new DefaultInstant(LocalDate.of(2020, 1, 1), 
IndeterminateValue.BEFORE);
+
+        assertEquals(TemporalOperatorName.BEFORE, 
before2000.findRelativePosition( after2010));
+        assertEquals(TemporalOperatorName.AFTER,   
after2010.findRelativePosition(before2000));
+        assertIndeterminate(() ->  after2010.findRelativePosition(before2020));
+        assertIndeterminate(() -> before2000.findRelativePosition(before2020));
+        assertIndeterminate(() -> before2020.findRelativePosition(before2000));
+    }
+
+    /**
+     * Asserts that the result of the given comparison is indeterminate.
+     *
+     * @param  c  the comparison to perform.
+     */
+    private static void assertIndeterminate(final Executable c) {
+        assertNotNull(assertThrows(IndeterminatePositionException.class, 
c).getMessage());
+    }
+
+    /**
+     * Tests {@link DefaultInstant#distance(TemporalPrimitive)} between two 
locale dates.
+     */
+    @Test
+    public void testDistanceBetweenLocalDates() {
+        final var t1981 = new DefaultInstant(LocalDate.of(1981, 6, 5), null);
+        final var t2000 = new DefaultInstant(LocalDate.of(2000, 8, 8), null);
+        final Period expected = Period.of(19, 2, 3);
+        assertEquals(expected,    t1981.distance(t2000));
+        assertEquals(expected,    t2000.distance(t1981));
+        assertEquals(Period.ZERO, t2000.distance(t2000));
+    }
+
+    /**
+     * Tests {@link DefaultInstant#distance(TemporalPrimitive)} between two 
dates with timezone.
+     */
+    @Test
+    public void testDistanceBetweenZonedDates() {
+        final var t2000 = new DefaultInstant(ZonedDateTime.of(2000, 6, 5, 12, 
4, 0, 0, ZoneOffset.UTC), null);
+        final var t2001 = new DefaultInstant(ZonedDateTime.of(2001, 6, 5, 14, 
4, 0, 0, ZoneOffset.UTC), null);
+        final Duration expected = Duration.ofDays(365).plusHours(2);
+        assertEquals(expected,      t2000.distance(t2001));
+        assertEquals(expected,      t2001.distance(t2000));
+        assertEquals(Duration.ZERO, t2000.distance(t2000));
+    }
+
+    /**
+     * Tests {@link DefaultInstant#distance(TemporalPrimitive)} between two 
dates with times.
+     * The period cannot be expressed with standard {@link java.time} objects.
+     */
+    @Test
+    public void testDistanceBetweenLocalDateTimes() {
+        final var t1 = new DefaultInstant(LocalDateTime.of(2000, 6, 5, 12, 4, 
0, 0), null);
+        final var t3 = new DefaultInstant(LocalDateTime.of(2001, 6, 9, 14, 4, 
0, 0), null);
+        final var t2 = new DefaultInstant(LocalDateTime.of(2001, 6, 9, 10, 4, 
0, 0), null);
+
+        Object expected = "P1Y4DT2H";
+        assertEquals(expected,    t1.distance(t3).toString());
+        assertEquals(expected,    t3.distance(t1).toString());
+        assertEquals(Period.ZERO, t1.distance(t1));
+
+        expected = "P1Y3DT22H";
+        assertEquals(expected,    t1.distance(t2).toString());
+        assertEquals(expected,    t2.distance(t1).toString());
+        assertEquals(Period.ZERO, t2.distance(t2));
+
+        expected = Duration.ofHours(4);
+        assertEquals(expected,    t2.distance(t3));
+        assertEquals(expected,    t3.distance(t2));
+        assertEquals(Period.ZERO, t3.distance(t3));
+    }
+
+    /**
+     * Tests {@link DefaultInstant#distance(TemporalPrimitive)} between an 
instant and a period.
+     */
+    @Test
+    public void testDistanceWithPeriod() {
+        final var before = new DefaultInstant(LocalDate.of(1981, 1, 1), null);
+        final var begins = new DefaultInstant(LocalDate.of(1981, 6, 5), null);
+        final var during = new DefaultInstant(LocalDate.of(1990, 1, 1), null);
+        final var ends   = new DefaultInstant(LocalDate.of(2000, 1, 1), null);
+        final var after  = new DefaultInstant(LocalDate.of(2000, 1, 2), null);
+        final var period = new DefaultPeriod(begins, ends);
+
+        assertEquals(Period.of(0, 5, 4), before.distance(period));
+        assertEquals(Period.ZERO,        begins.distance(period));
+        assertEquals(Duration.ZERO,      during.distance(period));      // 
`Duration` considered an implementation details.
+        assertEquals(Period.ZERO,        ends  .distance(period));
+        assertEquals(Period.of(0, 0, 1), after .distance(period));
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultPeriodTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultPeriodTest.java
index 6ea04f0a11..de37051758 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultPeriodTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/DefaultPeriodTest.java
@@ -17,8 +17,7 @@
 package org.apache.sis.temporal;
 
 import java.time.LocalDate;
-
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import java.time.Period;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -76,4 +75,15 @@ public final class DefaultPeriodTest extends TestCase {
         var p1 = TemporalUtilities.createPeriod(LocalDate.of(2000, 1, 1), 
LocalDate.of(2010, 1, 1));
         assertEquals("2000-01-01/2010-01-01", p1.toString());
     }
+
+    /**
+     * Tests {@link DefaultPeriod#length()}.
+     */
+    @Test
+    public void testLength() {
+        var beginning = LocalDate.of(2010, 5, 1);
+        var ending    = LocalDate.of(2015, 8, 6);
+        var period    = TemporalUtilities.createPeriod(beginning, ending);
+        assertEquals(Period.of(5, 3, 5), period.length());
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
index 409e88ca86..d8ab00ab64 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
@@ -237,7 +237,7 @@ public final class ElementTest extends TestCase {
     public void testClose() throws ParseException {
         final Element element = parse("A[\"B\", \"C\"]");
         var e = assertThrows(ParseException.class, () -> element.close(null));
-        assertEquals("Unexpected value “B” in “A” element.", 
e.getLocalizedMessage());
+        assertEquals("Unexpected value “B” in the “A” element.", 
e.getLocalizedMessage());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
index e8cc5e393d..78d747d87c 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
@@ -952,7 +952,7 @@ public class Errors extends IndexedResourceBundle {
         public static final short UnexpectedTypeForReference_3 = 175;
 
         /**
-         * Unexpected value “{1}” in “{0}” element.
+         * Unexpected value “{1}” in the “{0}” element.
          */
         public static final short UnexpectedValueInElement_2 = 176;
 
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
index c348e5bafc..103ad55d34 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
@@ -202,7 +202,7 @@ UnexpectedParameter_1             = Parameter 
\u201c{0}\u201d was not expected.
 UnexpectedProperty_2              = Property \u201c{1}\u201d was not expected 
in \u201c{0}\u201d.
 UnexpectedScaleFactorForUnit_2    = Unexpected scale factor {1,number} for 
unit of measurement \u201c{0}\u201d.
 UnexpectedTypeForReference_3      = Expected \u201c{0}\u201d to reference an 
instance of \u2018{1}\u2019, but found an instance of \u2018{2}\u2019.
-UnexpectedValueInElement_2        = Unexpected value \u201c{1}\u201d in 
\u201c{0}\u201d element.
+UnexpectedValueInElement_2        = Unexpected value \u201c{1}\u201d in the 
\u201c{0}\u201d element.
 Uninitialized_1                   = \u2018{0}\u2019 has not been initialized.
 UnknownCommand_1                  = Command \u201c{0}\u201d is not recognized.
 UnknownEnumValue_2                = \u201c{1}\u201d is not a known or 
supported value for the \u2018{0}\u2019 enumeration.


Reply via email to