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.
