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 d6f35024a6e01bb0e11549e1f6ce2ce93d785197 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sun Jan 25 00:27:43 2026 +0100 Simplify `ComparisonFilter` by delegating to `TimeMethods` when the operands are temporal. It will also enable optimization in future commits. --- .../org/apache/sis/filter/ComparisonFilter.java | 342 +++------------ .../main/org/apache/sis/filter/TemporalFilter.java | 2 +- .../org/apache/sis/filter/TemporalOperation.java | 129 +++--- .../org/apache/sis/temporal/DefaultInstant.java | 8 +- .../main/org/apache/sis/temporal/TimeMethods.java | 468 ++++++++++++++++++--- .../apache/sis/storage/base/MetadataBuilder.java | 4 +- 6 files changed, 542 insertions(+), 411 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java index 3c48ac4b32..50da8c794e 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java @@ -18,26 +18,14 @@ package org.apache.sis.filter; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.DateTimeException; import java.util.List; import java.util.Collection; -import java.util.Date; -import java.util.Calendar; import java.util.Objects; -import java.time.Instant; -import java.time.LocalTime; -import java.time.OffsetTime; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.time.ZoneId; -import java.time.chrono.ChronoLocalDate; -import java.time.chrono.ChronoLocalDateTime; -import java.time.chrono.ChronoZonedDateTime; -import java.time.temporal.ChronoField; -import java.time.temporal.Temporal; import org.apache.sis.math.Fraction; import org.apache.sis.filter.base.Node; import org.apache.sis.filter.base.BinaryFunctionWidening; +import org.apache.sis.temporal.TimeMethods; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.filter.Filter; @@ -203,7 +191,9 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj pass = evaluate(left, element); } switch (matchAction) { - default: return false; // Unknown enumeration. + default: { + return false; // Unknown enumeration. + } case ALL: { if (!pass) return false; match = true; // Remember that we have at least 1 value. @@ -230,6 +220,7 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj /** * Compares the given objects. If both values are numerical, then this method delegates to an {@code applyAs…} method. + * If both values are temporal, then this method delegates to {@link TimeMethods} with runtime detection of the type. * For other kind of objects, this method delegates to a {@code compare(…)} method. If the two objects are not of the * same type, then the less accurate one is converted to the most accurate type if possible. * @@ -248,71 +239,12 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj final Number r = apply((Number) left, (Number) right); if (r != null) return r.intValue() != 0; } - /* - * For legacy java.util.Date, the compareTo(…) method is consistent only for dates of the same class. - * Otherwise A.compareTo(B) and B.compareTo(A) are inconsistent if one object is a java.util.Date and - * the other object is a java.sql.Timestamp. In such case, we compare the dates as java.time objects. - */ - if (left instanceof Date && right instanceof Date) { - if (left.getClass() == right.getClass()) { - return fromCompareTo(((Date) left).compareTo((Date) right)); - } - left = fromLegacy((Date) left); - right = fromLegacy((Date) right); - } - /* - * Temporal objects have complex conversion rules. We take Instant as the most accurate and unambiguous type. - * So if at least one value is an Instant, try to unconditionally promote the other value to an Instant too. - * This conversion will fail if the other object has some undefined fields; for example java.sql.Date has no - * time fields (we do not assume that the values of those fields are zero). - * - * OffsetTime and OffsetDateTime are final classes that do not implement a java.time.chrono interface. - * Note that OffsetDateTime is convertible into OffsetTime by dropping the date fields, but we do not - * (for now) perform comparisons that would ignore the date fields of an operand. - */ - if (left instanceof Temporal || right instanceof Temporal) { // Use || because an operand may be Date. - if (left instanceof Instant) { - final Instant t = toInstant(right); - if (t != null) return fromCompareTo(((Instant) left).compareTo(t)); - } else if (right instanceof Instant) { - final Instant t = toInstant(left); - if (t != null) return fromCompareTo(t.compareTo((Instant) right)); - } else if (left instanceof OffsetDateTime) { - final OffsetDateTime t = toOffsetDateTime(right); - if (t != null) return compare((OffsetDateTime) left, t); - } else if (right instanceof OffsetDateTime) { - final OffsetDateTime t = toOffsetDateTime(left); - if (t != null) return compare(t, (OffsetDateTime) right); - } else if (left instanceof OffsetTime && right instanceof OffsetTime) { - return compare((OffsetTime) left, (OffsetTime) right); - } - /* - * Comparisons of temporal objects implementing java.time.chrono interfaces. We need to check the most - * complete types first. If the type are different, we reduce to the type of the less smallest operand. - * For example if an operand is a date+time and the other operand is only a date, then the time fields - * will be ignored and a warning will be reported. - */ - if (left instanceof ChronoLocalDateTime<?>) { - final ChronoLocalDateTime<?> t = toLocalDateTime(right); - if (t != null) return compare((ChronoLocalDateTime<?>) left, t); - } else if (right instanceof ChronoLocalDateTime<?>) { - final ChronoLocalDateTime<?> t = toLocalDateTime(left); - if (t != null) return compare(t, (ChronoLocalDateTime<?>) right); - } - if (left instanceof ChronoLocalDate) { - final ChronoLocalDate t = toLocalDate(right); - if (t != null) return compare((ChronoLocalDate) left, t); - } else if (right instanceof ChronoLocalDate) { - final ChronoLocalDate t = toLocalDate(left); - if (t != null) return compare(t, (ChronoLocalDate) right); - } - if (left instanceof LocalTime) { - final LocalTime t = toLocalTime(right); - if (t != null) return fromCompareTo(((LocalTime) left).compareTo(t)); - } else if (right instanceof LocalTime) { - final LocalTime t = toLocalTime(left); - if (t != null) return fromCompareTo(t.compareTo((LocalTime) right)); - } + try { + Boolean t = TimeMethods.compareIfTemporal(temporalTest(), left, right); + if (t != null) return t; + } catch (DateTimeException e) { + warning(e); + return false; } /* * Test character strings only after all specialized types have been tested. The intent is that if an @@ -345,148 +277,7 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj } /** - * Converts a legacy {@code Date} object to an object from the {@link java.time} package. - * We performs this conversion before to compare to {@code Date} instances that are not of - * the same class, because the {@link Date#compareTo(Date)} method in such case is not well - * defined. - */ - private static Temporal fromLegacy(final Date value) { - if (value instanceof java.sql.Timestamp) { - return ((java.sql.Timestamp) value).toLocalDateTime(); - } else if (value instanceof java.sql.Date) { - return ((java.sql.Date) value).toLocalDate(); - } else if (value instanceof java.sql.Time) { - return ((java.sql.Time) value).toLocalTime(); - } else { - // Implementation of above toFoo() methods use system default time zone. - return LocalDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); - } - } - - /** - * Converts the given object to an {@link Instant}, or returns {@code null} if unconvertible. - * This method handles a few types from the {@link java.time} package and legacy types like - * {@link Date} (with a special case for SQL dates) and {@link Calendar}. - */ - private static Instant toInstant(final Object value) { - if (value instanceof Instant) { - return (Instant) value; - } else if (value instanceof OffsetDateTime) { - return ((OffsetDateTime) value).toInstant(); - } else if (value instanceof ChronoZonedDateTime) { - return ((ChronoZonedDateTime) value).toInstant(); - } else if (value instanceof Date) { - try { - return ((Date) value).toInstant(); - } catch (UnsupportedOperationException e) { - /* - * java.sql.Date and java.sql.Time cannot be converted to Instant because a part - * of their coordinates on the timeline is undefined. For example in the case of - * java.sql.Date the hours, minutes and seconds are unspecified (which is not the - * same thing as assuming that those values are zero). - */ - } - } else if (value instanceof Calendar) { - return ((Calendar) value).toInstant(); - } - return null; - } - - /** - * Converts the given object to an {@link OffsetDateTime}, or returns {@code null} if unconvertible. - */ - private static OffsetDateTime toOffsetDateTime(final Object value) { - if (value instanceof OffsetDateTime) { - return (OffsetDateTime) value; - } else if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toOffsetDateTime(); - } else { - return null; - } - } - - /** - * Converts the given object to a {@link ChronoLocalDateTime}, or returns {@code null} if unconvertible. - * This method handles the case of legacy SQL {@link java.sql.Timestamp} objects. - * Conversion may lost timezone information. - */ - private static ChronoLocalDateTime<?> toLocalDateTime(final Object value) { - if (value instanceof ChronoLocalDateTime<?>) { - return (ChronoLocalDateTime<?>) value; - } else if (value instanceof ChronoZonedDateTime) { - ignoringField(ChronoField.OFFSET_SECONDS); - return ((ChronoZonedDateTime) value).toLocalDateTime(); - } else if (value instanceof OffsetDateTime) { - ignoringField(ChronoField.OFFSET_SECONDS); - return ((OffsetDateTime) value).toLocalDateTime(); - } else if (value instanceof java.sql.Timestamp) { - return ((java.sql.Timestamp) value).toLocalDateTime(); - } else { - return null; - } - } - - /** - * Converts the given object to a {@link ChronoLocalDate}, or returns {@code null} if unconvertible. - * This method handles the case of legacy SQL {@link java.sql.Date} objects. - * Conversion may lost timezone information and time fields. - */ - private static ChronoLocalDate toLocalDate(final Object value) { - if (value instanceof ChronoLocalDate) { - return (ChronoLocalDate) value; - } else if (value instanceof ChronoLocalDateTime) { - ignoringField(ChronoField.SECOND_OF_DAY); - return ((ChronoLocalDateTime) value).toLocalDate(); - } else if (value instanceof ChronoZonedDateTime) { - ignoringField(ChronoField.SECOND_OF_DAY); - return ((ChronoZonedDateTime) value).toLocalDate(); - } else if (value instanceof OffsetDateTime) { - ignoringField(ChronoField.SECOND_OF_DAY); - return ((OffsetDateTime) value).toLocalDate(); - } else if (value instanceof java.sql.Date) { - return ((java.sql.Date) value).toLocalDate(); - } else { - return null; - } - } - - /** - * Converts the given object to a {@link LocalTime}, or returns {@code null} if unconvertible. - * This method handles the case of legacy SQL {@link java.sql.Time} objects. - * Conversion may lost timezone information. - */ - private static LocalTime toLocalTime(final Object value) { - if (value instanceof LocalTime) { - return (LocalTime) value; - } else if (value instanceof OffsetTime) { - ignoringField(ChronoField.OFFSET_SECONDS); - return ((OffsetTime) value).toLocalTime(); - } else if (value instanceof java.sql.Time) { - return ((java.sql.Time) value).toLocalTime(); - } else { - return null; - } - } - - /** - * Invoked when a conversion cause a field to be ignored. For example if a "date+time" object is compared - * with a "date" object, the "time" field is ignored. Expected values are: - * - * <ul> - * <li>{@link ChronoField#OFFSET_SECONDS}: time zone is ignored.</li> - * <li>{@link ChronoField#SECOND_OF_DAY}: time of dat and time zone are ignored.</li> - * </ul> - * - * @param field the field which is ignored. - * - * @see <a href="https://issues.apache.org/jira/browse/SIS-460">SIS-460</a> - */ - private static void ignoringField(final ChronoField field) { - // TODO - } - - /** - * Converts the boolean result as an integer for use as a return value of the {@code applyAs…} methods. + * Converts the Boolean result as an integer for use as a return value of the {@code applyAs…} methods. * This is a helper class for subclasses. */ private static Number number(final boolean result) { @@ -499,37 +290,14 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj protected abstract boolean fromCompareTo(int result); /** - * Compares two times with time-zone information. Implementations shall not use {@code compareTo(…)} because - * that method compares more information than desired in order to ensure consistency with {@code equals(…)}. - */ - protected abstract boolean compare(OffsetTime left, OffsetTime right); - - /** - * Compares two dates with time-zone information. Implementations shall not use {@code compareTo(…)} because - * that method compares more information than desired in order to ensure consistency with {@code equals(…)}. - */ - protected abstract boolean compare(OffsetDateTime left, OffsetDateTime right); - - /** - * Compares two dates without time-of-day and time-zone information. Implementations shall not use - * {@code compareTo(…)} because that method also compares chronology, which is not desired for the - * purpose of "is before" or "is after" comparison functions. - */ - protected abstract boolean compare(ChronoLocalDate left, ChronoLocalDate right); - - /** - * Compares two dates without time-zone information. Implementations shall not use {@code compareTo(…)} - * because that method also compares chronology, which is not desired for the purpose of "is before" or - * "is after" comparison functions. - */ - protected abstract boolean compare(ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right); - - /** - * Compares two dates with time-zone information. Implementations shall not use {@code compareTo(…)} - * because that method also compares chronology, which is not desired for the purpose of "is before" + * Returns an identification of the test to use if the operands are temporal. + * We do not use {@code compareTo(…)} for temporal objects because that method + * also compares chronology, which is not desired for the purpose of "is before" * or "is after" comparison functions. + * + * @return identification of the test to apply on temporal objects. */ - protected abstract boolean compare(ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right); + protected abstract TimeMethods.Test temporalTest(); /** Delegates to {@link BigDecimal#compareTo(BigDecimal)} and interprets the result with {@link #fromCompareTo(int)}. */ @Override protected final Number applyAsDecimal (BigDecimal left, BigDecimal right) {return number(fromCompareTo(left.compareTo(right)));} @@ -569,13 +337,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result < 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left < right);} - @Override protected Number applyAsLong (long left, long right) {return number(left < right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return left.isBefore(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return left.isBefore(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return left.isBefore(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return left.isBefore(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return left.isBefore(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left < right);} + @Override protected Number applyAsLong (long left, long right) {return number(left < right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.BEFORE;} } @@ -611,13 +377,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result <= 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left <= right);} - @Override protected Number applyAsLong (long left, long right) {return number(left <= right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return !left.isAfter(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return !left.isAfter(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return !left.isAfter(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return !left.isAfter(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return !left.isAfter(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left <= right);} + @Override protected Number applyAsLong (long left, long right) {return number(left <= right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.NOT_AFTER;} } @@ -653,13 +417,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result > 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left > right);} - @Override protected Number applyAsLong (long left, long right) {return number(left > right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return left.isAfter(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return left.isAfter(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return left.isAfter(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return left.isAfter(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return left.isAfter(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left > right);} + @Override protected Number applyAsLong (long left, long right) {return number(left > right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.AFTER;} } @@ -695,13 +457,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result >= 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left >= right);} - @Override protected Number applyAsLong (long left, long right) {return number(left >= right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return !left.isBefore(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return !left.isBefore(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return !left.isBefore(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return !left.isBefore(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return !left.isBefore(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left >= right);} + @Override protected Number applyAsLong (long left, long right) {return number(left >= right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.NOT_BEFORE;} } @@ -737,13 +497,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result == 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left == right);} - @Override protected Number applyAsLong (long left, long right) {return number(left == right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return left.isEqual(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return left.isEqual(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return left.isEqual(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return left.isEqual(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return left.isEqual(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left == right);} + @Override protected Number applyAsLong (long left, long right) {return number(left == right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.EQUAL;} } @@ -779,13 +537,11 @@ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, Obj @Override protected boolean fromCompareTo(final int result) {return result != 0;} /** Performs the comparison and returns the result as 0 (false) or 1 (true). */ - @Override protected Number applyAsDouble(double left, double right) {return number(left != right);} - @Override protected Number applyAsLong (long left, long right) {return number(left != right);} - @Override protected boolean compare (OffsetTime left, OffsetTime right) {return !left.isEqual(right);} - @Override protected boolean compare (OffsetDateTime left, OffsetDateTime right) {return !left.isEqual(right);} - @Override protected boolean compare (ChronoLocalDate left, ChronoLocalDate right) {return !left.isEqual(right);} - @Override protected boolean compare (ChronoLocalDateTime<?> left, ChronoLocalDateTime<?> right) {return !left.isEqual(right);} - @Override protected boolean compare (ChronoZonedDateTime<?> left, ChronoZonedDateTime<?> right) {return !left.isEqual(right);} + @Override protected Number applyAsDouble(double left, double right) {return number(left != right);} + @Override protected Number applyAsLong (long left, long right) {return number(left != right);} + + /** For comparisons of temporal objects. */ + @Override protected TimeMethods.Test temporalTest() {return TimeMethods.Test.NOT_EQUAL;} } 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 c32399731f..c696cb2815 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 @@ -35,7 +35,7 @@ import org.opengis.filter.InvalidFilterValueException; /** * Temporal operations between a period and a Java temporal object or between two periods. * The base class represents the general case when we don't know which operands are periods. - * The subclasses represent specializations when the type of temporal values is known in advance. + * The subclasses provide specializations when the types of temporal values are known in advance. * * <p>In the context of this class, "instant" can be understood as <abbr>ISO</abbr> 19108 instant * or as the various {@link java.time} objects, not restricted to {@link java.time.Instant}.</p> 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 55ba782f6b..143560c721 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 @@ -23,9 +23,6 @@ import java.time.temporal.Temporal; import org.apache.sis.util.internal.shared.Strings; import org.apache.sis.util.collection.WeakHashSet; 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; @@ -180,23 +177,26 @@ abstract class TemporalOperation<T> implements Serializable { * Returns {@code true} if {@code other} is non-null and the specified comparison evaluates to {@code true}. * This is a helper function for {@code evaluate(…)} methods implementations. * - * @param test enumeration value such as {@link TimeMethods#BEFORE} or {@link TimeMethods#AFTER}. + * @param test the test to apply (before, after and/or equal). * @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 DateTimeException if the two objects cannot be compared. */ - protected final boolean compare(final int test, final T self, final Instant other) { + protected final boolean compare(final TimeMethods.Test test, final T self, final Instant other) { if (other != null) { final Temporal position; final Optional<IndeterminateValue> p = other.getIndeterminatePosition(); if (p.isPresent()) { - if (p.get() == IndeterminateValue.NOW) { + final IndeterminateValue v = p.get(); + if (v == IndeterminateValue.NOW) { position = comparators.now(); } else { switch (test) { - case BEFORE: if (p.get() != IndeterminateValue.AFTER) return false; else break; - case AFTER: if (p.get() != IndeterminateValue.BEFORE) return false; else break; + case BEFORE: if (v != IndeterminateValue.AFTER) return false; else break; + case AFTER: if (v != IndeterminateValue.BEFORE) return false; else break; + case NOT_BEFORE: if (v == IndeterminateValue.BEFORE) return false; else break; + case NOT_AFTER: if (v == IndeterminateValue.AFTER) return false; else break; default: return false; } position = other.getPosition(); @@ -215,41 +215,42 @@ abstract class TemporalOperation<T> implements Serializable { * Returns {@code true} if both arguments are non-null and the specified comparison evaluates to {@code true}. * This is a helper function for {@code evaluate(…)} methods implementations. * - * @param test enumeration value such as {@link TimeMethods#BEFORE} or {@link TimeMethods#AFTER}. + * @param test the test to apply (before, after and/or equal). * @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 DateTimeException if the two objects cannot be compared. */ - @SuppressWarnings("unchecked") - protected static boolean compare(final int test, final Instant self, final Instant other) { + protected static boolean compare(final TimeMethods.Test test, final Instant self, final Instant other) { if (self == null || other == null) { return false; } - final IndeterminateValue p1 = self.getIndeterminatePosition().orElse(null); + final IndeterminateValue p1 = self .getIndeterminatePosition().orElse(null); final IndeterminateValue p2 = other.getIndeterminatePosition().orElse(null); if (p1 != null || p2 != null) { - if (p1 == p2) { - return (test == EQUAL) && (p1 == IndeterminateValue.NOW); - } + final IndeterminateValue a1, a2; // Values which would create ambiguities. switch (test) { - case BEFORE: if (isAmbiguous(p1, IndeterminateValue.BEFORE) || isAmbiguous(p2, IndeterminateValue.AFTER)) return false; else break; - case AFTER: if (isAmbiguous(p1, IndeterminateValue.AFTER) || isAmbiguous(p2, IndeterminateValue.BEFORE)) return false; else break; - default: return false; + case BEFORE: case NOT_AFTER: a1 = IndeterminateValue.BEFORE; a2 = IndeterminateValue.AFTER; break; + case AFTER: case NOT_BEFORE: a1 = IndeterminateValue.AFTER; a2 = IndeterminateValue.BEFORE; break; + case EQUAL: return (p1 == p2) && (p1 == IndeterminateValue.NOW); + default: return false; + } + if (isAmbiguous(p1, a1) || isAmbiguous(p2, a2)) { + return false; } } - return TimeMethods.compareAny(test, self.getPosition(), other.getPosition()); + return TimeMethods.compareLenient(test, self.getPosition(), other.getPosition()); } /** * Returns {@code true} if using the {@code p} value would be ambiguous. * - * @param p the indeterminate value to test. + * @param p1 the indeterminate value to test. * @param required the required value for a non-ambiguous comparison. * @return whether using the given value would be ambiguous. */ - private static boolean isAmbiguous(final IndeterminateValue p, final IndeterminateValue required) { - return (p != null) && (p != IndeterminateValue.NOW) && (p != required); + private static boolean isAmbiguous(final IndeterminateValue p1, final IndeterminateValue required) { + return (p1 != null) && (p1 != IndeterminateValue.NOW) && (p1 != required); } @@ -306,20 +307,20 @@ abstract class TemporalOperation<T> implements Serializable { /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(T self, Period other) { - return compare(EQUAL, self, other.getBeginning()) && - compare(EQUAL, self, other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self, other.getBeginning()) && + compare(TimeMethods.Test.EQUAL, self, other.getEnding()); } /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(Period self, T other) { - return compare(EQUAL, other, self.getBeginning()) && - compare(EQUAL, other, self.getEnding()); + return compare(TimeMethods.Test.EQUAL, other, self.getBeginning()) && + compare(TimeMethods.Test.EQUAL, other, self.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, Period other) { - return compare(EQUAL, self.getBeginning(), other.getBeginning()) && - compare(EQUAL, self.getEnding(), other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self.getBeginning(), other.getBeginning()) && + compare(TimeMethods.Test.EQUAL, self.getEnding(), other.getEnding()); } } @@ -360,17 +361,17 @@ abstract class TemporalOperation<T> implements Serializable { /** Relationship not defined by ISO 19108:2006. */ @Override public boolean evaluate(T self, Period other) { - return compare(BEFORE, self, other.getBeginning()); + return compare(TimeMethods.Test.BEFORE, self, other.getBeginning()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, T other) { - return compare(AFTER, other, self.getEnding()); + return compare(TimeMethods.Test.AFTER, other, self.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, Period other) { - return compare(BEFORE, self.getEnding(), other.getBeginning()); + return compare(TimeMethods.Test.BEFORE, self.getEnding(), other.getBeginning()); } } @@ -411,17 +412,17 @@ abstract class TemporalOperation<T> implements Serializable { /** Relationship not defined by ISO 19108:2006. */ @Override public boolean evaluate(T self, Period other) { - return compare(AFTER, self, other.getEnding()); + return compare(TimeMethods.Test.AFTER, self, other.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, T other) { - return compare(BEFORE, other, self.getBeginning()); + return compare(TimeMethods.Test.BEFORE, other, self.getBeginning()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, Period other) { - return compare(AFTER, self.getBeginning(), other.getEnding()); + return compare(TimeMethods.Test.AFTER, self.getBeginning(), other.getEnding()); } } @@ -450,8 +451,8 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getBeginning(), other.getBeginning()) && - compare(BEFORE, self.getEnding(), other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self.getBeginning(), other.getBeginning()) && + compare(TimeMethods.Test.BEFORE, self.getEnding(), other.getEnding()); } } @@ -480,8 +481,8 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getEnding(), other.getEnding()) && - compare(AFTER, self.getBeginning(), other.getBeginning()); + return compare(TimeMethods.Test.EQUAL, self.getEnding(), other.getEnding()) && + compare(TimeMethods.Test.AFTER, self.getBeginning(), other.getBeginning()); } } @@ -511,13 +512,13 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(Period self, T other) { - return compare(EQUAL, other, self.getBeginning()); + return compare(TimeMethods.Test.EQUAL, other, self.getBeginning()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getBeginning(), other.getBeginning()) && - compare(AFTER, self.getEnding(), other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self.getBeginning(), other.getBeginning()) && + compare(TimeMethods.Test.AFTER, self.getEnding(), other.getEnding()); } } @@ -547,13 +548,13 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final T other) { - return compare(EQUAL, other, self.getEnding()); + return compare(TimeMethods.Test.EQUAL, other, self.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getEnding(), other.getEnding()) && - compare(BEFORE, self.getBeginning(), other.getBeginning()); + return compare(TimeMethods.Test.EQUAL, self.getEnding(), other.getEnding()) && + compare(TimeMethods.Test.BEFORE, self.getBeginning(), other.getBeginning()); } } @@ -587,17 +588,17 @@ abstract class TemporalOperation<T> implements Serializable { /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(final T self, final Period other) { - return compare(EQUAL, self, other.getBeginning()); + return compare(TimeMethods.Test.EQUAL, self, other.getBeginning()); } /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(final Period self, final T other) { - return compare(EQUAL, other, self.getEnding()); + return compare(TimeMethods.Test.EQUAL, other, self.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getEnding(), other.getBeginning()); + return compare(TimeMethods.Test.EQUAL, self.getEnding(), other.getBeginning()); } } @@ -631,17 +632,17 @@ abstract class TemporalOperation<T> implements Serializable { /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(final T self, final Period other) { - return compare(EQUAL, self, other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self, other.getEnding()); } /** Extension to ISO 19108: handle instant as a tiny period. */ @Override public boolean evaluate(final Period self, final T other) { - return compare(EQUAL, other, self.getBeginning()); + return compare(TimeMethods.Test.EQUAL, other, self.getBeginning()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(EQUAL, self.getBeginning(), other.getEnding()); + return compare(TimeMethods.Test.EQUAL, self.getBeginning(), other.getEnding()); } } @@ -675,8 +676,8 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(AFTER, self.getBeginning(), other.getBeginning()) && - compare(BEFORE, self.getEnding(), other.getEnding()); + return compare(TimeMethods.Test.AFTER, self.getBeginning(), other.getBeginning()) && + compare(TimeMethods.Test.BEFORE, self.getEnding(), other.getEnding()); } } @@ -711,14 +712,14 @@ abstract class TemporalOperation<T> implements Serializable { /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final T other) { - return compare(AFTER, other, self.getBeginning()) && - compare(BEFORE, other, self.getEnding()); + return compare(TimeMethods.Test.AFTER, other, self.getBeginning()) && + compare(TimeMethods.Test.BEFORE, other, self.getEnding()); } /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */ @Override public boolean evaluate(final Period self, final Period other) { - return compare(BEFORE, self.getBeginning(), other.getBeginning()) && - compare(AFTER, self.getEnding(), other.getEnding()); + return compare(TimeMethods.Test.BEFORE, self.getBeginning(), other.getBeginning()) && + compare(TimeMethods.Test.AFTER, self.getEnding(), other.getEnding()); } } @@ -749,9 +750,9 @@ abstract class TemporalOperation<T> implements Serializable { @Override public boolean evaluate(final Period self, final Period other) { final Instant selfBegin, selfEnd, otherBegin, otherEnd; return ((otherBegin = other.getBeginning()) != null) && - ((selfBegin = self.getBeginning()) != null) && compare(BEFORE, selfBegin, otherBegin) && - ((selfEnd = self. getEnding()) != null) && compare(AFTER, selfEnd, otherBegin) && - ((otherEnd = other. getEnding()) != null) && compare(BEFORE, selfEnd, otherEnd); + ((selfBegin = self.getBeginning()) != null) && compare(TimeMethods.Test.BEFORE, selfBegin, otherBegin) && + ((selfEnd = self. getEnding()) != null) && compare(TimeMethods.Test.AFTER, selfEnd, otherBegin) && + ((otherEnd = other. getEnding()) != null) && compare(TimeMethods.Test.BEFORE, selfEnd, otherEnd); } } @@ -782,9 +783,9 @@ abstract class TemporalOperation<T> implements Serializable { @Override public boolean evaluate(final Period self, final Period other) { final Instant selfBegin, selfEnd, otherBegin, otherEnd; return ((selfBegin = self.getBeginning()) != null) && - ((otherBegin = other.getBeginning()) != null) && compare(AFTER, selfBegin, otherBegin) && - ((otherEnd = other. getEnding()) != null) && compare(BEFORE, selfBegin, otherEnd) && - ((selfEnd = self. getEnding()) != null) && compare(AFTER, selfEnd, otherEnd); + ((otherBegin = other.getBeginning()) != null) && compare(TimeMethods.Test.AFTER, selfBegin, otherBegin) && + ((otherEnd = other. getEnding()) != null) && compare(TimeMethods.Test.BEFORE, selfBegin, otherEnd) && + ((selfEnd = self. getEnding()) != null) && compare(TimeMethods.Test.AFTER, selfEnd, otherEnd); } } @@ -813,9 +814,9 @@ abstract class TemporalOperation<T> implements Serializable { @Override public boolean evaluate(final Period self, final Period other) { final Instant selfBegin, selfEnd, otherBegin, otherEnd; return ((selfBegin = self.getBeginning()) != null) && - ((otherEnd = other. getEnding()) != null) && compare(BEFORE, selfBegin, otherEnd) && + ((otherEnd = other. getEnding()) != null) && compare(TimeMethods.Test.BEFORE, selfBegin, otherEnd) && ((selfEnd = self. getEnding()) != null) && - ((otherBegin = other.getBeginning()) != null) && compare(AFTER, selfEnd, otherBegin); + ((otherBegin = other.getBeginning()) != null) && compare(TimeMethods.Test.AFTER, selfEnd, otherBegin); } } } 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 80613993e9..33415a75cd 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 @@ -283,8 +283,12 @@ cmp: if (canTestBefore | canTestAfter | canTestEqual) { return true; } final Temporal other = that.getPosition(); - return Objects.equals(position, other) || // Needed in all cases for testing null values. - (mode.isIgnoringMetadata() && TimeMethods.compareAny(TimeMethods.EQUAL, position, other)); + if (Objects.equals(position, other)) { // Needed anyway for testing null values. + return true; + } + if (mode.isIgnoringMetadata()) { + return TimeMethods.compareLenient(TimeMethods.Test.EQUAL, position, other); + } } } return false; diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java index 45af8e82b2..935762c40f 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java @@ -18,6 +18,7 @@ package org.apache.sis.temporal; import java.util.Map; import java.util.Date; +import java.util.Calendar; import java.util.Optional; import java.util.function.Supplier; import java.util.function.BiFunction; @@ -79,11 +80,81 @@ public class TimeMethods<T> implements Serializable { public final Class<T> type; /** - * Enumeration values for a test to apply. + * The test to apply: equal, before or after. * - * @see #compare(int, T, TemporalAccessor) + * @see #compare(Test, T, TemporalAccessor) */ - public static final int BEFORE=1, AFTER=2, EQUAL=0; + public enum Test { + /** Identifies the <var>A</var> = <var>B</var> test. */ + EQUAL() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isEqual;} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return m.isEqual.test(a, b);} + @Override boolean fromCompareTo(int result) {return result == 0;} + }, + + /** Identifies the <var>A</var> {@literal <} <var>B</var> test. */ + BEFORE() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isBefore;} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return m.isBefore.test(a, b);} + @Override boolean fromCompareTo(int result) {return result < 0;} + }, + + /** Identifies the <var>A</var> {@literal >} <var>B</var> test. */ + AFTER() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isAfter;} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return m.isAfter.test(a, b);} + @Override boolean fromCompareTo(int result) {return result > 0;} + }, + + /** Identifies the <var>A</var> ≠ <var>B</var> test. */ + NOT_EQUAL() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isEqual.negate();} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return !m.isEqual.test(a, b);} + @Override boolean fromCompareTo(int result) {return result != 0;} + }, + + /** Identifies the <var>A</var> ≥ <var>B</var> test. */ + NOT_BEFORE() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isBefore.negate();} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return !m.isBefore.test(a, b);} + @Override boolean fromCompareTo(int result) {return result >= 0;} + }, + + /** Identifies the <var>A</var> ≤ <var>B</var> test. */ + NOT_AFTER() { + @Override <T> BiPredicate<T,T> predicate(TimeMethods<T> m) {return m.isAfter.negate();} + @Override <T> boolean compare(TimeMethods<T> m, T a, T b) {return !m.isAfter.test(a, b);} + @Override boolean fromCompareTo(int result) {return result <= 0;} + }; + + /** + * Returns the predicate to use for this test. + * + * @param <T> the type of temporal objects expected by the predicate. + * @param m the collection of predicate for the type of temporal objects. + * @return the predicate for this test. + */ + abstract <T> BiPredicate<T,T> predicate(TimeMethods<T> m); + + /** + * Executes the test between the given temporal objects. + * + * @param <T> the type of temporal objects expected by the predicate. + * @param m the collection of predicate for the type of temporal objects. + * @param self the object on which to invoke the method identified by this test. + * @param other the argument to give to the test method call. + * @return the result of performing the comparison identified by this test. + */ + abstract <T> boolean compare(TimeMethods<T> m, T self, T other); + + /** + * Returns whether the test pass according the result of a {@code compareTo(…)} method. + * + * @param result the {@code compareTo(…)} result. + * @return whether the test pass. + */ + abstract boolean fromCompareTo(int result); + } /** * Predicate to execute for testing the ordering between temporal objects. @@ -147,59 +218,233 @@ public class TimeMethods<T> implements Serializable { return false; } + /** + * Returns the predicate to use for this test. + * + * @param test the test to apply (before, after and/or equal). + * @return the predicate for the requested test. + */ + public final BiPredicate<T,T> predicate(final Test test) { + return test.predicate(this); + } + /** * Delegates the comparison to the method identified by the {@code test} argument. * This method is overridden in subclasses where the delegation can be more direct. * - * @param test {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}. + * @param test the test to apply (before, after and/or equal). * @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}. */ - boolean delegate(final int test, final T self, final T other) { - final BiPredicate<T,T> p; - switch (test) { - case BEFORE: p = isBefore; break; - case AFTER: p = isAfter; break; - case EQUAL: p = isEqual; break; - default: throw new AssertionError(test); - } - return p.test(self, other); + boolean delegate(final Test test, final T self, final T other) { + return test.compare(this, self, other); } /** * Compares an object of class {@code <T>} with a temporal object of unknown class. * The other object is typically the beginning or ending of a period. * - * @param test {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}. + * @param test the test to apply (before, after and/or equal). * @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 DateTimeException if the two objects cannot be compared. */ @SuppressWarnings("unchecked") - public final boolean compare(final int test, final T self, final TemporalAccessor other) { + public final boolean compare(final Test test, final T self, final TemporalAccessor other) { if (type.isInstance(other)) { return delegate(test, self, (T) other); // Safe because of above `isInstance(…)` check. } return compareAsInstants(test, accessor(self), other); } + /** + * Returns {@code TRUE} if both arguments are non-null and the specified comparison evaluates to {@code true}. + * If the two objects are not of compatible type, they are converted. If at least one object is not temporal, + * then this method returns {@code null} rather than throwing {@link DateTimeException}. + * + * <p>This method should be used in last resort because it may be expensive.</p> + * + * @param test the test to apply (before, after and/or equal). + * @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 comparison result, or {@code null} if the given objects were not recognized as temporal. + * @throws DateTimeException if the two objects are temporal objects but cannot be compared. + */ + public static Boolean compareIfTemporal(final Test test, Object self, Object other) { + if (self == null || other == null) { + return Boolean.FALSE; + } + boolean isTemporal = false; + if (self instanceof TemporalDate) {self = ((TemporalDate) self).temporal; isTemporal = true;} + if (other instanceof TemporalDate) {other = ((TemporalDate) other).temporal; isTemporal = true;} + /* + * For legacy java.util.Date, the compareTo(…) method is consistent only for dates of the same class. + * Otherwise A.compareTo(B) and B.compareTo(A) are inconsistent if one object is a java.util.Date and + * the other object is a java.sql.Timestamp. In such case, we compare the dates as java.time objects. + */ + if (self instanceof Date && other instanceof Date) { + if (self.getClass() == other.getClass()) { + return test.fromCompareTo(((Date) self).compareTo((Date) other)); + } + self = fromLegacy((Date) self); + other = fromLegacy((Date) other); + isTemporal = true; // For skipping unecessary `if (x instanceof Temporal)` checks. + } + // Use `||` because an operand by still be a `java.utl.Date`. + if (isTemporal || self instanceof Temporal || other instanceof Temporal) { + return compareAny(test, self, other); + } + return null; + } + /** * Returns {@code true} if both arguments are non-null and the specified comparison evaluates to {@code true}. * The type of the objects being compared is determined dynamically, which has a performance cost. * The {@code compare(…)} methods should be preferred when the type is known in advance. * - * @param test {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}. + * @param test the test to apply (before, after and/or equal). * @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 DateTimeException if the two objects cannot be compared. */ - @SuppressWarnings("unchecked") - public static boolean compareAny(final int test, final Temporal self, final Temporal other) { - return (self != null) && (other != null) - && compare(test, (Class) Classes.findCommonClass(self.getClass(), other.getClass()), self, other); + public static boolean compareLenient(final Test test, final Temporal self, final Temporal other) { + if (self != null && other != null) { + Boolean c = compareAny(test, self, other); + if (c != null) return c; + } + return false; + } + + /** + * Implementation of lenient comparisons. + * Temporal objects have complex conversion rules. We take Instant as the most accurate and unambiguous type. + * So if at least one value is an Instant, try to unconditionally promote the other value to an Instant too. + * This conversion will fail if the other object has some undefined fields. For example {@link java.sql.Date} + * has no time fields (we do not assume that the values of those fields are zero). + * + * @param test the test to apply (before, after and/or equal). + * @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 comparison result, or {@code null} if the given objects were not recognized as temporal. + * @throws DateTimeException if the two objects cannot be compared. + */ + private static Boolean compareAny(final Test test, Object self, Object other) { + Class<?> type = self.getClass(); +adapt: if (self != other.getClass()) { + Temporal converted; + /* + * OffsetTime and OffsetDateTime are final classes that do not implement a java.time.chrono interface. + * Note that OffsetDateTime is convertible into OffsetTime by dropping the date fields, but we do not + * (for now) perform comparisons that would ignore the date fields of an operand. + */ + if (self instanceof Instant) { + converted = toInstant(other); + if (converted != null) { + other = converted; + type = Instant.class; + break adapt; + } + } else if (other instanceof Instant) { + converted = toInstant(self); + if (converted != null) { + self = converted; + type = Instant.class; + break adapt; + } + } else if (self instanceof OffsetDateTime) { + converted = toOffsetDateTime(other); + if (converted != null) { + other = converted; + type = OffsetDateTime.class; + break adapt; + } + } else if (other instanceof OffsetDateTime) { + converted = toOffsetDateTime(self); + if (converted != null) { + self = converted; + type = OffsetDateTime.class; + break adapt; + } + } + /* + * Comparisons of temporal objects implementing java.time.chrono interfaces. We need to check the most + * complete types first. If the type are different, we reduce to the type of the less smallest operand. + * For example if an operand is a date+time and the other operand is only a date, then the time fields + * will be ignored and a warning will be reported. + */ + if (self instanceof ChronoLocalDateTime<?>) { + converted = toLocalDateTime(other); + if (converted != null) { + other = converted; + type = ChronoLocalDateTime.class; + break adapt; + } + } else if (other instanceof ChronoLocalDateTime<?>) { + converted = toLocalDateTime(self); + if (converted != null) { + self = converted; + type = ChronoLocalDateTime.class; + break adapt; + } + } + // No else, we want this as a fallback. + if (self instanceof ChronoLocalDate) { + converted = toLocalDate(other); + if (converted != null) { + other = converted; + type = ChronoLocalDate.class; + break adapt; + } + } else if (other instanceof ChronoLocalDate) { + converted = toLocalDate(self); + if (converted != null) { + self = converted; + type = ChronoLocalDate.class; + break adapt; + } + } + // No else, we want this as a fallback. + if (self instanceof LocalTime) { + converted = toLocalTime(other); + if (converted != null) { + other = converted; + type = LocalTime.class; + break adapt; + } + } else if (other instanceof LocalTime) { + converted = toLocalTime(self); + if (converted != null) { + self = converted; + type = LocalTime.class; + break adapt; + } + } + // No else, we want this as a fallback. + if (self instanceof Temporal && other instanceof Temporal) { + type = Classes.findCommonClass(self.getClass(), other.getClass()); + } else { + return null; + } + } + return castAndCompare(test, type, self, other); + } + + /** + * Delegates to {@link #compare(int, Class, Object, Object)} after verification of the type. + * + * @param test the test to apply (before, after and/or equal). + * @param type base class of the {@code self} and {@code other} arguments. + * @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 ClassCastException if {@code self} or {@code other} is not an instance of {@code type}. + * @throws DateTimeException if the two objects cannot be compared. + */ + private static <T> boolean castAndCompare(Test test, Class<T> type, Object self, Object other) { + return compare(test, type, type.cast(self), type.cast(other)); } /** @@ -208,14 +453,14 @@ public class TimeMethods<T> implements Serializable { * are not always the same as {@code compareTo(…)}. * * @param <T> base class of the objects to compare. - * @param test {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}. + * @param test the test to apply (before, after and/or equal). * @param type base class of the {@code self} and {@code other} arguments. * @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 DateTimeException if the two objects cannot be compared. */ - public static <T> boolean compare(final int test, final Class<T> type, final T self, final T other) { + public static <T> boolean compare(final Test 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 @@ -230,19 +475,10 @@ public class TimeMethods<T> implements Serializable { /* * Found one of the special cases listed in `INTERFACES` or `FINAL_TYPE`. * If the other type is compatible, the comparison is executed directly. - * Note: the `switch` statement is equivalent to `tc.compare(test, …)`, - * but is inlined because that method is never overridden in this context. */ if (tc.type.isInstance(other)) { assert tc.type.isAssignableFrom(type) : tc; // Those types are not necessarily equal. - final BiPredicate<? super T, ? super T> p; - switch (test) { - case BEFORE: p = tc.isBefore; break; - case AFTER: p = tc.isAfter; break; - case EQUAL: p = tc.isEqual; break; - default: throw new AssertionError(test); - } - return p.test(self, other); + return test.compare(tc, self, other); } } else if (self instanceof Comparable<?> && self.getClass().isInstance(other)) { /* @@ -253,12 +489,7 @@ public class TimeMethods<T> implements Serializable { */ @SuppressWarnings("unchecked") // Safe because verification done by `isInstance(…)`. final int c = ((Comparable) self).compareTo(other); - switch (test) { - case BEFORE: return c < 0; - case AFTER: return c > 0; - case EQUAL: return c == 0; - default: throw new AssertionError(test); - } + return test.fromCompareTo(c); } /* * If we reach this point, the two operands are of different classes and we cannot compare them directly. @@ -285,23 +516,20 @@ public class TimeMethods<T> implements Serializable { * Compares two temporal objects as instants. * This is a last-resort fallback, when objects cannot be compared by their own methods. * - * @param test {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}. + * @param test the test to apply (before, after and/or equal). * @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 DateTimeException if the two objects cannot be compared. */ - private static boolean compareAsInstants(final int test, final TemporalAccessor self, final TemporalAccessor other) { + private static boolean compareAsInstants(final Test test, final TemporalAccessor self, final TemporalAccessor other) { 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) { - return test == EQUAL; - } } - return test == ((t1 < t2) ? BEFORE : AFTER); + return test.fromCompareTo(Long.compare(t1, t2)); } /** @@ -367,15 +595,15 @@ public class TimeMethods<T> implements Serializable { */ private static <T> TimeMethods<? super T> fallback(final Class<T> type) { 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(Test.BEFORE, type, self, other), + (self, other) -> compare(Test.AFTER, type, self, other), + (self, other) -> compare(Test.EQUAL, type, self, other), null, null, false) { @Override public boolean isDynamic() { return true; } - @Override boolean delegate(final int test, final T self, final T other) { + @Override boolean delegate(final Test test, final T self, final T other) { return compare(test, type, self, other); } }; @@ -429,9 +657,10 @@ public class TimeMethods<T> implements Serializable { * </li> * </ul> * + * @param <T> type of the {@code time} argument. * @param time the temporal object to return with the specified timezone, or {@code null} if none. * @param timezone the desired timezone. Cannot be {@code null}. - * @param allowAdd + * @param allowAdd whether to allow the addition of a time zone in an object that initially had none. * @return a temporal object with the specified timezone, if it was possible to apply a timezone. */ public static <T> Optional<Temporal> withZone(final T time, final ZoneId timezone, final boolean allowAdd) { @@ -541,6 +770,147 @@ public class TimeMethods<T> implements Serializable { } } + /** + * Converts a legacy {@code Date} object to an object from the {@link java.time} package. + * We performs this conversion before to compare to {@code Date} instances that are not of + * the same class, because the {@link Date#compareTo(Date)} method in such case is not well + * defined. + */ + private static Temporal fromLegacy(final Date value) { + if (value instanceof java.sql.Timestamp) { + return ((java.sql.Timestamp) value).toLocalDateTime(); + } else if (value instanceof java.sql.Date) { + return ((java.sql.Date) value).toLocalDate(); + } else if (value instanceof java.sql.Time) { + return ((java.sql.Time) value).toLocalTime(); + } else { + // Implementation of above toFoo() methods use system default time zone. + return LocalDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); + } + } + + /** + * Converts the given object to an {@link Instant}, or returns {@code null} if unconvertible. + * This method handles a few types from the {@link java.time} package and legacy types like + * {@link Date} (with a special case for SQL dates) and {@link Calendar}. + */ + private static Instant toInstant(final Object value) { + if (value instanceof Instant) { + return (Instant) value; + } else if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).toInstant(); + } else if (value instanceof ChronoZonedDateTime) { + return ((ChronoZonedDateTime) value).toInstant(); + } else if (value instanceof Date) { + try { + return ((Date) value).toInstant(); + } catch (UnsupportedOperationException e) { + /* + * java.sql.Date and java.sql.Time cannot be converted to Instant because a part + * of their coordinates on the timeline is undefined. For example in the case of + * java.sql.Date the hours, minutes and seconds are unspecified (which is not the + * same thing as assuming that those values are zero). + */ + } + } else if (value instanceof Calendar) { + return ((Calendar) value).toInstant(); + } + return null; + } + + /** + * Converts the given object to an {@link OffsetDateTime}, or returns {@code null} if unconvertible. + */ + private static OffsetDateTime toOffsetDateTime(final Object value) { + if (value instanceof OffsetDateTime) { + return (OffsetDateTime) value; + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).toOffsetDateTime(); + } else { + return null; + } + } + + /** + * Converts the given object to a {@link ChronoLocalDateTime}, or returns {@code null} if unconvertible. + * This method handles the case of legacy SQL {@link java.sql.Timestamp} objects. + * Conversion may lost timezone information. + */ + private static ChronoLocalDateTime<?> toLocalDateTime(final Object value) { + if (value instanceof ChronoLocalDateTime<?>) { + return (ChronoLocalDateTime<?>) value; + } else if (value instanceof ChronoZonedDateTime) { + ignoringField(ChronoField.OFFSET_SECONDS); + return ((ChronoZonedDateTime) value).toLocalDateTime(); + } else if (value instanceof OffsetDateTime) { + ignoringField(ChronoField.OFFSET_SECONDS); + return ((OffsetDateTime) value).toLocalDateTime(); + } else if (value instanceof java.sql.Timestamp) { + return ((java.sql.Timestamp) value).toLocalDateTime(); + } else { + return null; + } + } + + /** + * Converts the given object to a {@link ChronoLocalDate}, or returns {@code null} if unconvertible. + * This method handles the case of legacy SQL {@link java.sql.Date} objects. + * Conversion may lost timezone information and time fields. + */ + private static ChronoLocalDate toLocalDate(final Object value) { + if (value instanceof ChronoLocalDate) { + return (ChronoLocalDate) value; + } else if (value instanceof ChronoLocalDateTime) { + ignoringField(ChronoField.SECOND_OF_DAY); + return ((ChronoLocalDateTime) value).toLocalDate(); + } else if (value instanceof ChronoZonedDateTime) { + ignoringField(ChronoField.SECOND_OF_DAY); + return ((ChronoZonedDateTime) value).toLocalDate(); + } else if (value instanceof OffsetDateTime) { + ignoringField(ChronoField.SECOND_OF_DAY); + return ((OffsetDateTime) value).toLocalDate(); + } else if (value instanceof java.sql.Date) { + return ((java.sql.Date) value).toLocalDate(); + } else { + return null; + } + } + + /** + * Converts the given object to a {@link LocalTime}, or returns {@code null} if unconvertible. + * This method handles the case of legacy SQL {@link java.sql.Time} objects. + * Conversion may lost timezone information. + */ + private static LocalTime toLocalTime(final Object value) { + if (value instanceof LocalTime) { + return (LocalTime) value; + } else if (value instanceof OffsetTime) { + ignoringField(ChronoField.OFFSET_SECONDS); + return ((OffsetTime) value).toLocalTime(); + } else if (value instanceof java.sql.Time) { + return ((java.sql.Time) value).toLocalTime(); + } else { + return null; + } + } + + /** + * Invoked when a conversion cause a field to be ignored. For example if a "date+time" object is compared + * with a "date" object, the "time" field is ignored. Expected values are: + * + * <ul> + * <li>{@link ChronoField#OFFSET_SECONDS}: time zone is ignored.</li> + * <li>{@link ChronoField#SECOND_OF_DAY}: time of dat and time zone are ignored.</li> + * </ul> + * + * @param field the field which is ignored. + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-460">SIS-460</a> + */ + private static void ignoringField(final ChronoField field) { + // TODO + } + /** * Returns a string representation of this set of operations for debugging purposes. * diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java index 705f54d29c..177724488b 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java @@ -1145,8 +1145,8 @@ public class MetadataBuilder { for (final Iterator<CitationDate> it = dates.iterator(); it.hasNext();) { final CitationDate existing = it.next(); if (type.equals(existing.getDateType())) { - final int method = type.name().startsWith("LATE_") ? TimeMethods.BEFORE : TimeMethods.AFTER; - if (TimeMethods.compareAny(method, existing.getReferenceDate(), date.getReferenceDate())) { + TimeMethods.Test method = type.name().startsWith("LATE_") ? TimeMethods.Test.BEFORE : TimeMethods.Test.AFTER; + if (TimeMethods.compareLenient(method, existing.getReferenceDate(), date.getReferenceDate())) { it.remove(); break; }
