瀏覽代碼

review: datetime

Ranides Atterwim 10 月之前
父節點
當前提交
b632adc140
共有 17 個文件被更改,包括 409 次插入372 次删除
  1. 53 0
      assira.commons/src/main/java/net/ranides/assira/time/DateComparator.java
  2. 13 9
      assira.commons/src/main/java/net/ranides/assira/time/ZonedRange.java
  3. 12 12
      assira.commons/src/main/java/net/ranides/assira/time/ZonedDateSet.java
  4. 37 59
      assira.commons/src/main/java/net/ranides/assira/time/DateTimeUtils.java
  5. 5 22
      assira.commons/src/main/java/net/ranides/assira/time/ZonedDate.java
  6. 3 2
      assira.commons/src/main/java/net/ranides/assira/time/ZonedTime.java
  7. 3 55
      assira.commons/src/main/java/net/ranides/assira/time/orm/ZonedDateConverter.java
  8. 6 0
      assira.commons/src/main/java/net/ranides/assira/time/orm/ZonedTimeConverter.java
  9. 82 4
      assira.commons/src/test/java/net/ranides/assira/time/DateComparatorTest.java
  10. 16 0
      assira.commons/src/test/java/net/ranides/assira/time/DateTimeUtilsTest.java
  11. 2 2
      assira.commons/src/test/java/net/ranides/assira/time/ZonedDateSetTest1.java
  12. 82 0
      assira.commons/src/test/java/net/ranides/assira/time/ZonedDateTest.java
  13. 0 107
      assira.commons/src/test/java/net/ranides/assira/time/ZonedDateTest1.java
  14. 0 32
      assira.commons/src/test/java/net/ranides/assira/time/ZonedRangeTest1.java
  15. 36 65
      assira.commons/src/test/java/net/ranides/assira/time/ZonedTimeTest1.java
  16. 0 3
      assira.commons/src/test/java/net/ranides/assira/time/format/DateTimeWriterTest.java
  17. 59 0
      assira.commons/src/test/java/net/ranides/assira/time/json/ZonedDateSerializerTest.java

+ 53 - 0
assira.commons/src/main/java/net/ranides/assira/time/DateComparator.java

@@ -2,14 +2,22 @@ package net.ranides.assira.time;
 
 import lombok.experimental.UtilityClass;
 import net.ranides.assira.generic.CompareFunction;
+import net.ranides.assira.math.MathUtils;
 import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.Nullable;
 
+import java.time.DateTimeException;
+import java.time.Duration;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoField;
 import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
 import java.util.Objects;
 
+import static java.time.temporal.ChronoField.NANO_OF_SECOND;
+import static java.time.temporal.ChronoUnit.NANOS;
+import static java.time.temporal.ChronoUnit.SECONDS;
+
 @UtilityClass
 public class DateComparator {
 
@@ -39,6 +47,11 @@ public class DateComparator {
         return cmp(a, b) < 0;
     }
 
+    @ZoneHazard
+    public static boolean isBefore(@Nullable Temporal a, @Nullable Temporal b) {
+        return cmp(a, b) < 0;
+    }
+
     public static boolean isAfter(@Nullable ZonedDateTime a, @Nullable ZonedDateTime b) {
         return cmp(a, b) > 0;
     }
@@ -53,6 +66,11 @@ public class DateComparator {
         return cmp(a, b) > 0;
     }
 
+    @ZoneHazard
+    public static boolean isAfter(@Nullable Temporal a, @Nullable Temporal b) {
+        return cmp(a, b) > 0;
+    }
+
     @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
     public static @Nullable ZonedDateTime min(@Nullable ZonedDateTime a, @Nullable ZonedDateTime b) {
         return cmp(a, b) < 0 ? a : b;
@@ -70,6 +88,11 @@ public class DateComparator {
         return cmp(a, b) < 0 ? a : b;
     }
 
+    @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
+    public static <T extends Temporal> @Nullable T min(@Nullable T a, @Nullable T b) {
+        return cmp(a, b) < 0 ? a : b;
+    }
+
 
     @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
     public static @Nullable ZonedDateTime max(@Nullable ZonedDateTime a, @Nullable ZonedDateTime b) {
@@ -88,6 +111,12 @@ public class DateComparator {
         return cmp(a, b) > 0 ? a : b;
     }
 
+    @ZoneHazard
+    @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
+    public static <T extends Temporal> @Nullable T max(@Nullable T a, @Nullable T b) {
+        return cmp(a, b) > 0 ? a : b;
+    }
+
     @Contract("!null,null -> false; null,!null -> false; null,null -> true")
     public static boolean isSameDate(@Nullable ZonedDateTime a, @Nullable ZonedDateTime b) {
         if(Objects.isNull(a) != Objects.isNull(b)) {
@@ -172,4 +201,28 @@ public class DateComparator {
         return a.compareTo(b);
     }
 
+    @ZoneHazard
+    public static int cmp(@Nullable Temporal a, @Nullable Temporal b) {
+        if(Objects.isNull(a)) {
+            return Objects.isNull(b) ? 0 : -1;
+        }
+        if(Objects.isNull(b)) {
+            return +1;
+        }
+        try {
+            return MathUtils.signum(b.until(a, NANOS));
+        } catch (DateTimeException | ArithmeticException ex) {
+            long secs = b.until(a, SECONDS);
+            if(secs != 0) {
+                return MathUtils.signum(secs);
+            }
+            try {
+                return MathUtils.signum(a.getLong(NANO_OF_SECOND) - b.getLong(NANO_OF_SECOND));
+            } catch (DateTimeException ex2) {
+
+                return 0;
+            }
+        }
+    }
+
 }

+ 13 - 9
assira.commons/src/main/java/net/ranides/assira/time/ZonedRange.java

@@ -9,20 +9,24 @@ import org.jetbrains.annotations.Nullable;
 import java.time.Duration;
 import java.time.ZonedDateTime;
 
-public abstract class ZonedRange extends Range<ZonedDateTime> {
+/**
+ *
+ */
+public abstract class DateTimeRange extends Range<ZonedDateTime> {
+    // @todo test DateTimeRange
 
-    public static ZonedRange of(@Nullable ZonedDateTime begin, @Nullable ZonedDateTime end) {
+    public static DateTimeRange of(@Nullable ZonedDateTime begin, @Nullable ZonedDateTime end) {
         return begin.isBefore(end) ? new SimplePair(begin, end) : new SimplePair(end, begin);
     }
 
     @ZoneHazard
-    public static ZonedRange of(@Nullable ZonedDate begin, @Nullable ZonedDate end) {
+    public static DateTimeRange of(@Nullable ZonedDate begin, @Nullable ZonedDate end) {
         return DateComparator.isBefore(begin, end) ?
             new SimplePair(DateTimeUtils.withMin(begin), DateTimeUtils.withMax(end)) :
             new SimplePair(DateTimeUtils.withMin(end), DateTimeUtils.withMax(begin));
     }
 
-    public static ZonedRange of(@NotNull ZonedDate date) {
+    public static DateTimeRange of(@NotNull ZonedDate date) {
         return new SimpleDate(date.withMin());
     }
 
@@ -40,17 +44,17 @@ public abstract class ZonedRange extends Range<ZonedDateTime> {
     }
 
     @Override
-    protected ZonedRange newInstance(ZonedDateTime begin, ZonedDateTime end) {
+    protected DateTimeRange newInstance(ZonedDateTime begin, ZonedDateTime end) {
         return of(begin, end);
     }
 
     @Override
-    public ZonedRange intersection(Range<ZonedDateTime> range) {
-        return (ZonedRange)super.intersection(range);
+    public DateTimeRange intersection(Range<ZonedDateTime> range) {
+        return (DateTimeRange)super.intersection(range);
     }
 
     @RequiredArgsConstructor
-    private static class SimplePair extends ZonedRange {
+    private static class SimplePair extends DateTimeRange {
 
         private final ZonedDateTime begin;
 
@@ -69,7 +73,7 @@ public abstract class ZonedRange extends Range<ZonedDateTime> {
     }
 
     @RequiredArgsConstructor
-    private static class SimpleDate extends ZonedRange {
+    private static class SimpleDate extends DateTimeRange {
 
         private final ZonedDateTime begin;
 

+ 12 - 12
assira.commons/src/main/java/net/ranides/assira/time/ZonedDateSet.java

@@ -10,22 +10,21 @@ import java.util.SortedSet;
 import static java.time.LocalDateTime.MAX;
 import static java.time.LocalDateTime.MIN;
 
-// @todo ZonedDateSet
-public abstract class ZonedDateSet {
+public abstract class DateTimeSet {
 
-    public boolean containsFully(ZonedRange range) {
+    public boolean containsFully(DateTimeRange range) {
         return intersection(range).equals(Duration.ofDays(1));
     }
 
     public boolean contains(ZonedDate date) {
-        return !intersection(ZonedRange.of(date)).isZero();
+        return !intersection(DateTimeRange.of(date)).isZero();
     }
 
     public abstract boolean contains(ZonedDateTime datetime);
 
-    public abstract Duration intersection(ZonedRange range);
+    public abstract Duration intersection(DateTimeRange range);
 
-    public static class Empty extends ZonedDateSet {
+    public static class Empty extends DateTimeSet {
 
         @Override
         public boolean contains(ZonedDateTime datetime) {
@@ -33,12 +32,12 @@ public abstract class ZonedDateSet {
         }
 
         @Override
-        public Duration intersection(ZonedRange range) {
-            return null;
+        public Duration intersection(DateTimeRange range) {
+            return Duration.ZERO;
         }
     }
 
-    public static class Simple extends ZonedDateSet {
+    public static class Simple extends DateTimeSet {
 
         private final SortedSet<ZonedDateTime> set;
 
@@ -51,13 +50,13 @@ public abstract class ZonedDateSet {
 
         @ZoneHazard
         @Override
-        public Duration intersection(ZonedRange range) {
+        public Duration intersection(DateTimeRange range) {
             ZonedDateTime lower = range.begin();
             ZonedDateTime upper = range.end();
 
             for(ZonedDateTime mid : set.subSet(lower.with(MIN), upper.with(MAX))) {
                 if(mid.isEqual(lower) || (mid.isBefore(upper) && mid.plusDays(1).isAfter(lower))) {
-                    return ZonedRange.of(mid, mid.plusDays(1)).intersection(range).duration();
+                    return DateTimeRange.of(mid, mid.plusDays(1)).intersection(range).duration();
                 }
             }
             return Duration.ZERO;
@@ -65,7 +64,8 @@ public abstract class ZonedDateSet {
 
         @Override
         public boolean contains(ZonedDateTime datetime) {
-            // @todo test, may not work at all
+            // @todo test ZonedDateSet
+
             ZonedDateTime lowerMin = datetime.minusDays(1);
             ZonedDateTime upperMax = datetime.plusDays(1);
             for(ZonedDateTime mid : set.subSet(lowerMin, upperMax)) {

+ 37 - 59
assira.commons/src/main/java/net/ranides/assira/time/DateTimeUtils.java

@@ -5,6 +5,7 @@ import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.sql.Timestamp;
 import java.time.DateTimeException;
 import java.time.DayOfWeek;
 import java.time.Duration;
@@ -20,6 +21,7 @@ import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalAdjusters;
 import java.time.temporal.TemporalQueries;
+import java.time.temporal.TemporalUnit;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Objects;
@@ -107,81 +109,57 @@ public class DateTimeUtils {
         return asDateTime(DateTimeZones.UTC, input);
     }
 
-    public static @NotNull Duration modulo24(@NotNull Duration input) {
-        if(input.isNegative()) {
-            return input.plusDays(1-input.toDays());
-        } else {
-            return input.minusDays(input.toDays());
+    @ZoneExplicit
+    @Contract("_,null -> null; _,!null -> !null")
+    public static @Nullable ZonedDateTime asDateTime(@NotNull ZoneId zone, @Nullable Timestamp source) {
+        if(Objects.isNull(source)) {
+            return null;
         }
+        return source.toLocalDateTime().atZone(zone);
     }
 
-    // @todo support for temporal
-    // @todo do we really want always absolute?
-    public static @NotNull Duration diff(@NotNull ZonedDateTime begin, @NotNull ZonedDateTime end) {
-        Objects.requireNonNull(begin);
-        Objects.requireNonNull(end);
-        if(begin.isAfter(end)) {
-            return Duration.between(end, begin);
-        } else {
-            return Duration.between(begin, end);
-        }
+    /**
+     * This method assumes that timestamp represents instant in default system zone.
+     * Anyway, in reality Timestamp objects can be used to store any form of timestamps, quite often implicitly zoned.
+     * Be very careful when you use this method and verify semantic behind your input from legacy sources.
+     *
+     * @param source  source
+     * @return datetime in system default zone
+     */
+    @ZoneHazard
+    @Contract("null -> null; !null -> !null")
+    public static @Nullable ZonedDateTime asDateTime(@Nullable Timestamp source) {
+        return asDateTime(ZoneId.systemDefault(), source);
     }
 
-    @ZoneHazard
-    public static @NotNull Duration diff(@NotNull ZonedTime begin, @NotNull ZonedTime end) {
-        Objects.requireNonNull(begin);
-        Objects.requireNonNull(end);
-        return modulo24(Duration.between(begin, end));
+    public static @NotNull Duration modulo24(@NotNull Duration input) {
+        return modulo(input, ChronoUnit.DAYS);
     }
 
-    @ZoneHazard
-    public static @NotNull Period diff(@NotNull ZonedDate begin, @NotNull ZonedDate end) {
-        Objects.requireNonNull(begin);
-        Objects.requireNonNull(end);
-        if(begin.isAfter(end)) {
-            return begin.until(end);
+    public static @NotNull Duration modulo(@NotNull Duration input, TemporalUnit unit) {
+        long div = input.getSeconds() / unit.getDuration().getSeconds();
+        if(input.isNegative()) {
+            return input.plus(1 - div, unit);
         } else {
-            return end.until(begin);
+            return input.minus(div, unit);
         }
     }
 
-    @Contract("null -> false")
-    public static boolean hasMinTime(@Nullable ZonedDateTime datetime) {
-        return Objects.nonNull(datetime) && hasSameSeconds(LocalTime.MIN, datetime.toLocalTime());
-    }
-
-    @Contract("null -> false")
-    public static boolean hasMaxTime(@Nullable ZonedDateTime datetime) {
-        return Objects.nonNull(datetime) && hasSameSeconds(LocalTime.MAX, datetime.toLocalTime());
-    }
-
-    private static boolean hasSameSeconds(LocalTime value1, LocalTime value2) {
-        LocalTime seconds1 = value1.truncatedTo(ChronoUnit.SECONDS);
-        LocalTime seconds2 = value2.truncatedTo(ChronoUnit.SECONDS);
-        return seconds1.equals(seconds2);
-    }
-
-    @Deprecated
     @Contract("null -> null; !null -> !null")
     public static @Nullable ZonedDateTime withLastDayOfMonth(@Nullable ZonedDateTime datetime) {
         return Objects.isNull(datetime) ? null : datetime.with(TemporalAdjusters.lastDayOfMonth());
     }
 
-    @Deprecated
-    @Contract("null -> null; !null -> !null")
-    public static @Nullable ZonedDateTime withLastMinuteOfDay(@Nullable ZonedDateTime datetime) {
-        return Objects.isNull(datetime) ? null : datetime.withHour(23).withMinute(59).withSecond(59);
-    }
-
-    @Contract("null -> false")
-    public static boolean isSaturday(@Nullable ZonedDateTime datetime) {
-        return Objects.nonNull(datetime) && datetime.getDayOfWeek() == DayOfWeek.SATURDAY;
-    }
-
-    @Contract("null -> false")
-    public static boolean isSunday(@Nullable ZonedDateTime datetime) {
-        return Objects.nonNull(datetime) && datetime.getDayOfWeek() == DayOfWeek.SATURDAY;
-    }
+    // @todo implement ZonedCalendar
+    // we should use another specialized class for day-of-week, holidays or other events
+    // every calendar is in the end defined in terms of one specific zone
+    // in worst case, we can use CompositeCalendar constructed from ZonedCalendar instances
+    // class ZonedCalendar:
+    //      #isSaturday
+    //      #isSunday
+    //      #isHoliday
+    //      #isWorkingDay
+    //      etc
 
     @Contract("null -> null")
     public static ZoneId getZone(@Nullable TemporalAccessor input) {

+ 5 - 22
assira.commons/src/main/java/net/ranides/assira/time/ZonedDate.java

@@ -9,7 +9,6 @@ import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.sql.Timestamp;
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -56,21 +55,6 @@ public class ZonedDate implements Comparable<ZonedDate>, Temporal {
         return new ZonedDate(zone, date);
     }
 
-    @Contract("null -> null; !null -> !null")
-    public static @Nullable ZonedDate of(@Nullable Timestamp source) {
-        return of(ZoneId.systemDefault(), source);
-    }
-
-    @Deprecated
-    @ZoneExplicit
-    @Contract("_,null -> null; _,!null -> !null")
-    public static @Nullable ZonedDate of(@NotNull ZoneId zone, @Nullable Timestamp source) {
-        if(Objects.isNull(source)) {
-            return null;
-        }
-        return new ZonedDate(zone, source.toLocalDateTime().atZone(zone).toLocalDate());
-    }
-
     @ZoneExplicit
     public static @NotNull ZonedDate ofEpochSecond(@NotNull ZoneId zone, long epoch) {
         return of(zone, Instant.ofEpochSecond(epoch).atZone(zone).toLocalDate());
@@ -116,8 +100,8 @@ public class ZonedDate implements Comparable<ZonedDate>, Temporal {
         return date.toEpochDay();
     }
 
-    public ZonedRange toRange() {
-        return ZonedRange.of(this);
+    public DateTimeRange toRange() {
+        return DateTimeRange.of(this);
     }
 
     @ZoneHazard
@@ -199,13 +183,12 @@ public class ZonedDate implements Comparable<ZonedDate>, Temporal {
     }
 
     @Override
-    public long until(Temporal endExclusive, TemporalUnit unit) {
-        // @todo test zone awareness
-        return date.atTime(LocalTime.MIN).until(endExclusive.with(LocalTime.MIN), unit);
+    public long until(Temporal end, TemporalUnit unit) {
+        DateTimeUtils.requireSameZone(this, end);
+        return date.atTime(LocalTime.MIN).until(end.with(LocalTime.MIN), unit);
     }
 
     public Period until(Temporal end) {
-        // @todo what to do with more precise types than date?
         return Period.ofDays((int)until(end, ChronoUnit.DAYS));
     }
 

+ 3 - 2
assira.commons/src/main/java/net/ranides/assira/time/ZonedTime.java

@@ -260,9 +260,10 @@ public class ZonedTime implements Comparable<ZonedTime>, Temporal {
     }
 
     @Override
-    public long until(Temporal endExclusive, TemporalUnit unit) {
+    public long until(Temporal end, TemporalUnit unit) {
+        DateTimeUtils.requireSameZone(this, end);
         long a = time.getLong(ChronoField.NANO_OF_DAY);
-        long b = endExclusive.getLong(ChronoField.NANO_OF_DAY);
+        long b = end.getLong(ChronoField.NANO_OF_DAY);
         return LocalTime.ofNanoOfDay(a).until(LocalTime.ofNanoOfDay(b), unit);
     }
 

+ 3 - 55
assira.commons/src/main/java/net/ranides/assira/time/orm/ZonedDateConverter.java

@@ -9,62 +9,10 @@ import java.time.ZonedDateTime;
 import java.util.Objects;
 
 /**
- * @todo refine rules for ORM
+ * Trivial converter used to store ZonedDate inside database
  *
- * Rules for time and date storage:
- *
- * datetime:
- *  - prefer TIMESTAMP WITH ZONE, prefer UTC zone
- *  - accept TIMESTAMP WITH LOCAL ZONE, conversion is supported
- *  - avoid TIMESTAMP, assume UTC zone in such cases
- *  - avoid DATE, assume UTC zone in such cases
- *
- * date:
- *  - prefer TIMESTAMP WITH ZONE, prefer PL zone
- *  - avoid TIMESTAMP WITH LOCAL ZONE, stable conversion is impossible
- *  - accept TIMESTAMP, assume PL zone in such cases
- *  - accept DATE, assume PL zone in such cases
- *
- * time:
- *  - prefer TIMESTAMP WITH ZONE, prefer PL zone
- *  - avoid TIMESTAMP WITH LOCAL ZONE, stable conversion is impossible
- *  - accept TIMESTAMP, assume PL zone in such cases
- *  - accept DATE, assume PL zone in such cases
- *
- * Zone conversion is unpredictable for "date" or "time" - there is not enough information to know DST rules.
- * In practice, we almost always take care about POLISH days, so we should store dates in PL zone.
- * The same applies to hours.
- *
- * In long term we should avoid storage of ZonedDate or ZonedTime anyway.
- *
- * Merging date and time into proper datetime is possible only if timezones are identical.
- * In other cases such operation has no sense and reasonable semantic.
- *
- * If we try to merge "pure date" and "pure time" with different timezones, we should throw exception.
- *
- * If we try to merge "pure date" and "pure time" with different timezones, we could try to convert components into "common time zone"
- * but this conversion is not intuitive really. For example:
- *
- *   1990.10.15 UTC + 08:15 PL  = 1990.10.15T10:15 PL
- *   1990.10.15 PL  + 08:15 UTC = 1990.10.15T06:15 UTC
- *   1990.03.15 PL  + 08:15 UTC = 1990.03.15T07:15 UTC
- *
- * Because:
- *
- *   1990.10.15       UTC + 08:15 PL =
- *   1990.10.15T00:00 UTC + 08:15 PL =
- *   1990.10.15T02:00 PL  + 08:15 PL =
- *   1990.10.15T10:15 PL
- *
- *   1990.10.15       PL  + 08:15 UTC =
- *   1990.10.15T00:00 PL  + 08:15 UTC =
- *   1990.10.14T22:00 UTC + 08:15 UTC =
- *   1990.10.15T06:15 UTC
- *
- *   1990.03.15       PL  + 08:15 UTC =
- *   1990.03.15T00:00 PL  + 08:15 UTC =
- *   1990.03.14T23:00 UTC + 08:15 UTC =
- *   1990.03.15T07:15 UTC
+ * We just delegate work to builtin mappers.
+ * Preferred column type should be "TIMESTAMP WITH ZONE"
  */
 @Converter(autoApply = true)
 public class ZonedDateConverter implements AttributeConverter<ZonedDate, ZonedDateTime> {

+ 6 - 0
assira.commons/src/main/java/net/ranides/assira/time/orm/ZonedTimeConverter.java

@@ -10,6 +10,12 @@ import java.time.LocalDate;
 import java.time.ZonedDateTime;
 import java.util.Objects;
 
+/**
+ * Trivial converter used to store ZonedDate inside database
+ *
+ * We just delegate work to builtin mappers.
+ * Preferred column type should be "TIMESTAMP WITH ZONE"
+ */
 @Converter(autoApply = true)
 public class ZonedTimeConverter implements AttributeConverter<ZonedTime, ZonedDateTime> {
 

+ 82 - 4
assira.commons/src/test/java/net/ranides/assira/time/DateComparatorTest.java

@@ -2,22 +2,100 @@ package net.ranides.assira.time;
 
 import org.junit.Test;
 
+import java.time.DateTimeException;
+import java.time.LocalDateTime;
+import java.time.temporal.Temporal;
+
+import static net.ranides.assira.time.DateTimeZones.PL;
+import static net.ranides.assira.time.DateTimeZones.TV;
 import static org.junit.Assert.*;
 
 public class DateComparatorTest {
 
+    // those results are expected but complicate implementation of range
+    // our consistent comparator assumes that NULL is always the smallest value.
+    // such assumption works pretty well as long as we don't want to use NULL as sentinel value for MAX or MIN
     @Test
-    public void testIsBefore() {
+    public void testIsBeforeDate() {
         ZonedDate a = DateTimeLiteral.date(2020, 10, 10);
         ZonedDate b = DateTimeLiteral.date(2021, 10, 10);
 
         assertTrue(DateComparator.isBefore(a, b));
+        assertFalse(DateComparator.isBefore(b, a));
+
+        assertTrue(DateComparator.isBefore(null, b));
+
+        assertFalse(DateComparator.isBefore(a, null));
+    }
+
+    @Test
+    public void testIsBeforeDateZoned() {
+        ZonedDate a = DateTimeLiteral.date(TV, 2020, 10, 10);
+        ZonedDate b = DateTimeLiteral.date(PL, 2021, 10, 10);
+
+        assertThrows(DateTimeException.class, () -> {
+            DateComparator.isBefore(a, b);
+        });
+        assertThrows(DateTimeException.class, () -> {
+            DateComparator.isBefore(b, a);
+        });
+
+        assertTrue(DateComparator.isBefore(null, b));
+
+        assertFalse(DateComparator.isBefore(a, null));
+    }
+
+    @Test
+    public void testIsBeforeTime() {
+        ZonedTime a = DateTimeLiteral.time(14, 30, 10);
+        ZonedTime b = DateTimeLiteral.time(15, 45, 10);
+
+        assertTrue(DateComparator.isBefore(a, b));
+        assertFalse(DateComparator.isBefore(b, a));
 
-        // we have a problem: our consistent comparator assumes that NULL is always smallest value.
-        // such assumption works pretty well as long as we don't want to use NULL as sentinel value inside range
         assertTrue(DateComparator.isBefore(null, b));
 
-        // this result is expected, but it complicates implementation of Range
         assertFalse(DateComparator.isBefore(a, null));
     }
+
+    @Test
+    public void testIsBeforeTimeZoned() {
+        ZonedTime a = DateTimeLiteral.time(TV, 14, 30, 10);
+        ZonedTime b = DateTimeLiteral.time(PL, 15, 45, 10);
+
+        assertThrows(DateTimeException.class, () -> {
+            DateComparator.isBefore(a, b);
+        });
+        assertThrows(DateTimeException.class, () -> {
+            DateComparator.isBefore(b, a);
+        });
+
+        assertTrue(DateComparator.isBefore(null, b));
+
+        assertFalse(DateComparator.isBefore(a, null));
+    }
+
+    @Test
+    public void testCmpTemporal() {
+        Temporal a = DateTimeLiteral.time(TV, 14, 30, 10);
+        Temporal b = DateTimeLiteral.time(PL, 15, 45, 10);
+        Temporal c = DateTimeLiteral.time(PL, 16, 45, 10);
+
+        assertThrows(DateTimeException.class, () -> {
+            DateComparator.isBefore(a, b);
+        });
+
+        assertTrue(DateComparator.isBefore(null, b));
+        assertFalse(DateComparator.isBefore(a, null));
+        assertTrue(DateComparator.isBefore(b, c));
+        assertTrue(DateComparator.isAfter(c, b));
+
+        assertFalse(DateComparator.isBefore(c, c));
+        assertFalse(DateComparator.isAfter(c, c));
+
+        Temporal d = LocalDateTime.of(2020, 10, 10, 14, 30);
+        Temporal e = LocalDateTime.of(2020, 10, 15, 14, 30);
+        assertTrue(DateComparator.isBefore(d, e));
+        assertTrue(DateComparator.isAfter(e, d));
+    }
 }

+ 16 - 0
assira.commons/src/test/java/net/ranides/assira/time/DateTimeUtilsTest.java

@@ -6,6 +6,7 @@ import java.time.DateTimeException;
 import java.time.Duration;
 import java.time.OffsetDateTime;
 import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.Date;
 
 import static net.ranides.assira.time.DateTimeZones.PL;
@@ -87,4 +88,19 @@ public class DateTimeUtilsTest {
         assertEquals("PT10H14M52S", DateTimeUtils.modulo24(b.negated()).toString());
         assertEquals("PT14M35S", DateTimeUtils.modulo24(c.negated()).toString());
     }
+
+    @Test
+    public void testModulo() {
+        Duration a = DateTimeLiteral.duration(5, 13, 45, 8, 0, 450);
+        Duration b = DateTimeLiteral.duration(5, 13, 45, 8);
+        Duration c = DateTimeLiteral.duration(0, 23, 45, 25);
+
+        assertEquals("PT45M8.00000045S", DateTimeUtils.modulo(a, ChronoUnit.HOURS).toString());
+        assertEquals("PT45M8S", DateTimeUtils.modulo(b, ChronoUnit.HOURS).toString());
+        assertEquals("PT45M25S", DateTimeUtils.modulo(c, ChronoUnit.HOURS).toString());
+
+        assertEquals("PT14M51.99999955S", DateTimeUtils.modulo(a.negated(), ChronoUnit.HOURS).toString());
+        assertEquals("PT14M52S", DateTimeUtils.modulo(b.negated(), ChronoUnit.HOURS).toString());
+        assertEquals("PT14M35S", DateTimeUtils.modulo(c.negated(), ChronoUnit.HOURS).toString());
+    }
 }

+ 2 - 2
assira.commons/src/test/java/net/ranides/assira/time/ZonedDateSetTest1.java

@@ -6,11 +6,11 @@ import java.util.Arrays;
 
 import static org.junit.Assert.*;
 
-public class ZonedDateSetTest1 {
+public class ZonedDateSetTest {
 
     @Test
     public void basic() {
-        ZonedDateSet set = new ZonedDateSet.Simple(Arrays.asList(
+        DateTimeSet set = new DateTimeSet.Simple(Arrays.asList(
             DateTimeLiteral.dateUtc(2020, 3, 15),
             DateTimeLiteral.dateUtc(2020, 3, 16),
             DateTimeLiteral.dateUtc(2020, 4, 20),

+ 82 - 0
assira.commons/src/test/java/net/ranides/assira/time/ZonedDateTest.java

@@ -0,0 +1,82 @@
+package net.ranides.assira.time;
+
+import org.junit.Test;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static org.junit.Assert.*;
+
+public class ZonedDateTest {
+
+    @Test
+    public void testConstructor() {
+        ZonedDate date = ZonedDate.of(DateTimeZones.PL, LocalDate.of(2020, 11, 28));
+
+        assertEquals("2020-11-28 [Europe/Warsaw]", date.toString());
+        assertEquals(DateTimeZones.PL, ZoneId.from(date));
+    }
+
+    @Test
+    public void testEpochSeconds() {
+        ZonedDateTime datetime1 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
+        ZonedDateTime datetime2 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
+        ZonedDateTime datetime3 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 23, 45, 0);
+
+        ZonedDate date1 = ZonedDate.of(DateTimeUtils.withZone(datetime1, DateTimeZones.UTC));
+        ZonedDate date2 = ZonedDate.of(DateTimeUtils.withZone(datetime2, DateTimeZones.PL));
+        ZonedDate date3 = ZonedDate.of(DateTimeUtils.withZone(datetime3, DateTimeZones.PL));
+
+        assertEquals("2020-11-20 [UTC]", date1.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date2.toString());
+        assertEquals("2020-11-21 [Europe/Warsaw]", date3.toString());
+
+        assertEquals(1605830400L, date1.withMin().toEpochSecond());
+        assertEquals(1605826800L, date2.withMin().toEpochSecond());
+        assertEquals(1605913200L, date3.withMin().toEpochSecond());
+
+        ZonedDate date4 = ZonedDate.of(DateTimeZones.UTC, datetime1.toLocalDate());
+        ZonedDate date5 = ZonedDate.of(DateTimeZones.PL, datetime2.toLocalDate());
+        ZonedDate date6 = ZonedDate.of(DateTimeZones.PL, datetime3.toLocalDate());
+
+        assertEquals("2020-11-20 [UTC]", date4.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date5.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date6.toString());
+
+        assertEquals(1605830400L, date4.withMin().toEpochSecond());
+        assertEquals(1605826800L, date5.withMin().toEpochSecond());
+        assertEquals(1605826800L, date6.withMin().toEpochSecond());
+    }
+
+    @Test
+    public void testEpochDay() {
+        ZonedDateTime datetime1 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
+        ZonedDateTime datetime2 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
+        ZonedDateTime datetime3 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 23, 45, 0);
+
+        ZonedDate date1 = ZonedDate.of(DateTimeUtils.withZone(datetime1, DateTimeZones.UTC));
+        ZonedDate date2 = ZonedDate.of(DateTimeUtils.withZone(datetime2, DateTimeZones.PL));
+        ZonedDate date3 = ZonedDate.of(DateTimeUtils.withZone(datetime3, DateTimeZones.PL));
+
+        assertEquals("2020-11-20 [UTC]", date1.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date2.toString());
+        assertEquals("2020-11-21 [Europe/Warsaw]", date3.toString());
+
+        assertEquals(18586, date1.toEpochDay());
+        assertEquals(18586, date2.toEpochDay());
+        assertEquals(18587, date3.toEpochDay());
+
+        ZonedDate date4 = ZonedDate.of(DateTimeZones.UTC, datetime1.toLocalDate());
+        ZonedDate date5 = ZonedDate.of(DateTimeZones.PL, datetime2.toLocalDate());
+        ZonedDate date6 = ZonedDate.of(DateTimeZones.PL, datetime3.toLocalDate());
+
+        assertEquals("2020-11-20 [UTC]", date4.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date5.toString());
+        assertEquals("2020-11-20 [Europe/Warsaw]", date6.toString());
+
+        assertEquals(18586, date4.toEpochDay());
+        assertEquals(18586, date5.toEpochDay());
+        assertEquals(18586, date6.toEpochDay());
+    }
+}

+ 0 - 107
assira.commons/src/test/java/net/ranides/assira/time/ZonedDateTest1.java

@@ -1,107 +0,0 @@
-package net.ranides.assira.time;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import lombok.Builder;
-import lombok.Data;
-import org.junit.Test;
-
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-
-import static org.junit.Assert.*;
-
-public class ZonedDateTest1 {
-
-    @Test
-    public void testWithZone() {
-        ZonedDate date = ZonedDate.of(DateTimeZones.PL, LocalDate.of(2020, 11, 28));
-
-        assertEquals("2020-11-28 [Europe/Warsaw]", date.toString());
-
-        assertEquals(DateTimeZones.PL, ZoneId.from(date));
-    }
-
-    @Test
-    public void testSerialization() throws JsonProcessingException {
-        // https://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring
-
-        MyItem item = MyItem.builder()
-            .id(1170)
-            .code("RTTI")
-            .date1(LocalDate.now())
-            .date2(ZonedDate.now(DateTimeZones.GMT))
-            .date3(ZonedDate.now())
-            .date4(ZonedDate.now())
-            .build();
-
-        ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
-
-        System.out.println(mapper.writeValueAsString(item));
-    }
-
-    @Test
-    public void testEpoch() {
-        ZonedDateTime datetime1 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
-        ZonedDateTime datetime2 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 20, 45, 0);
-        ZonedDateTime datetime3 = DateTimeLiteral.datetimeUtc(2020, 11, 20, 23, 45, 0);
-
-        ZonedDate date1 = ZonedDate.of(DateTimeUtils.withZone(datetime1, DateTimeZones.UTC));
-        ZonedDate date2 = ZonedDate.of(DateTimeUtils.withZone(datetime2, DateTimeZones.PL));
-        ZonedDate date3 = ZonedDate.of(DateTimeUtils.withZone(datetime3, DateTimeZones.PL));
-
-        System.out.println(date1);
-        System.out.println(date2);
-        System.out.println(date3);
-
-        long seconds1 = date1.withMin().toEpochSecond();
-        long seconds2 = date2.withMin().toEpochSecond();
-        long seconds3 = date3.withMin().toEpochSecond();
-
-        System.out.println(date1.withMin());
-        System.out.println(date2.withMin());
-        System.out.println(date3.withMin());
-
-        System.out.println(seconds1);
-        System.out.println(seconds2);
-        System.out.println(seconds3);
-
-        ZonedDate out1 = ZonedDate.ofEpochSecond(DateTimeZones.UTC, seconds1);
-        ZonedDate out2 = ZonedDate.ofEpochSecond(DateTimeZones.PL, seconds2);
-        ZonedDate out3 = ZonedDate.ofEpochSecond(DateTimeZones.PL, seconds3);
-
-        System.out.println(out1);
-        System.out.println(out2);
-        System.out.println(out3);
-
-        System.out.println(out1.withMin());
-        System.out.println(out2.withMin());
-        System.out.println(out3.withMin());
-
-        System.out.println(out1.withMin().toEpochSecond());
-        System.out.println(out2.withMin().toEpochSecond());
-        System.out.println(out3.withMin().toEpochSecond());
-    }
-
-    @Data
-    @Builder
-    public static class MyItem {
-        private final int id;
-
-        private String code;
-
-        private LocalDate date1;
-
-        @JsonFormat(timezone = "Etc/GMT0")
-        private ZonedDate date2;
-
-        @JsonFormat(pattern = "yyyy-MM-dd' ['VV']'")
-        private ZonedDate date3;
-
-        @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
-        private ZonedDate date4;
-    }
-}

+ 0 - 32
assira.commons/src/test/java/net/ranides/assira/time/ZonedRangeTest1.java

@@ -1,32 +0,0 @@
-package net.ranides.assira.time;
-
-import org.junit.Test;
-
-import java.time.ZoneId;
-
-public class ZonedRangeTest1 {
-
-    @Test
-    public void intersection2() {
-        ZoneId zone = ZoneId.of("Pacific/Funafuti");
-
-        ZonedRange a = ZonedRange.of(
-                DateTimeLiteral.datetimeUtc(2020, 9, 15, 10, 0, 0),
-                DateTimeLiteral.datetimeUtc(2020, 9, 18, 10, 0, 0)
-        );
-
-        ZonedRange b = ZonedRange.of(
-                DateTimeLiteral.datetimeUtc(2020, 9, 14, 10, 0, 0),
-                DateTimeLiteral.datetimeUtc(2020, 9, 17, 10, 0, 0)
-        );
-
-        ZonedRange c = ZonedRange.of(
-                DateTimeLiteral.datetime(zone, 2020, 9, 15, 10, 0, 0),
-                DateTimeLiteral.datetime(zone, 2020, 9, 18, 10, 0, 0)
-        );
-
-        System.out.println(a.intersection(a));
-        System.out.println(a.intersection(b));
-        System.out.println(a.intersection(c));
-    }
-}

+ 36 - 65
assira.commons/src/test/java/net/ranides/assira/time/ZonedTimeTest1.java

@@ -3,7 +3,6 @@ package net.ranides.assira.time;
 import org.junit.Test;
 
 import java.time.DateTimeException;
-import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.time.ZoneId;
@@ -11,17 +10,24 @@ import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 
 import static net.ranides.assira.time.DateTimeZones.PL;
+import static net.ranides.assira.time.DateTimeZones.TV;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
-public class ZonedTimeTest1 {
+public class ZonedTimeTest {
 
     @Test
     public void testWithDate() {
         ZonedTime time = ZonedTime.of(PL, LocalTime.of(14, 25, 31));
 
-        System.out.println(time);
-        System.out.println(time.withDate(ZonedDate.of(PL, LocalDate.of(0,1,1))));
+        ZonedDate date1 = ZonedDate.of(PL, LocalDate.of(0, 1, 1));
+        assertEquals("0000-01-01T14:25:31+01:24[Europe/Warsaw]", time.withDate(date1).toString());
+
+        ZonedDate date2 = ZonedDate.of(PL, LocalDate.of(1976, 5, 18));
+        assertEquals("1976-05-18T14:25:31+01:00[Europe/Warsaw]", time.withDate(date2).toString());
+
+        ZonedDate date3 = ZonedDate.of(TV, LocalDate.of(1976, 5, 18));
+        assertThrows(DateTimeException.class, () -> time.withDate(date3));
     }
 
     @Test
@@ -31,30 +37,15 @@ public class ZonedTimeTest1 {
 
         assertEquals(65L, time1a.until(time1b, ChronoUnit.MINUTES));
 
-        ZonedTime time2a = ZonedTime.of(PL, LocalTime.of(8, 15, 0));
-        ZonedTime time2b = ZonedTime.of(PL, LocalTime.of(9, 20, 0));
+        ZonedTime time2a = DateTimeLiteral.time(PL, 8, 15, 0);
+        ZonedTime time2b = DateTimeLiteral.time(PL, 9, 20, 0);
 
         assertEquals(65L, time2a.until(time2b, ChronoUnit.MINUTES));
 
-        ZonedTime time3a = ZonedTime.of(ZonedDateTime.of(1980, 1, 1, 8, 15, 0, 0, PL));
-        ZonedTime time3b = ZonedTime.of(ZonedDateTime.of(1980, 1, 1, 9, 20, 0, 0, PL));
-
-        assertEquals(65L, time3a.until(time3b, ChronoUnit.MINUTES));
-
-        // @todo move to another test
-        ZonedDateTime datetime4a = DateTimeLiteral.datetimeUtc(1980, 1, 1, 23, 45, 0);
-        ZonedDateTime datetime4b = DateTimeLiteral.datetime(1980, 1, 1, 23, 45, 0);
-
-        assertEquals(Duration.ofHours(1), DateTimeUtils.diff(datetime4a, datetime4b));
-        assertEquals(Duration.ofHours(1), DateTimeUtils.diff(datetime4b, datetime4a));
-
-
-        ZonedTime time4a = DateTimeLiteral.timeUtc(23, 45, 0);
-        ZonedTime time4b = DateTimeLiteral.time(22, 45, 0);
-
-        assertEquals(Duration.ofHours(23), DateTimeUtils.diff(time4a, time4b));
-        assertEquals(Duration.ofHours(1), DateTimeUtils.diff(time4b, time4a));
+        ZonedTime time3a = DateTimeLiteral.time(PL, 8, 15, 0);
+        ZonedTime time3b = DateTimeLiteral.time(TV, 9, 20, 0);
 
+        assertThrows(DateTimeException.class, () -> time3a.until(time3b, ChronoUnit.MINUTES));
     }
 
     @Test
@@ -70,14 +61,13 @@ public class ZonedTimeTest1 {
         ZonedTime time2 = ZonedTime.of(ZonedDateTime.of(1970, 1, 1, 6, 45, 0, 0, zone));
 
         assertEquals("1980-01-01T06:45Z[UTC]", time1.withDate(base1).toString());
-//        assertEquals("1979-12-31T18:45Z[UTC]", time1.withDate(base2).toString());
-//        assertEquals("2009-12-31T18:45Z[UTC]", time1.withDate(base3).toString());
-//        assertEquals("1899-12-31T19:12:56Z[UTC]", time1.withDate(base4).toString());
+        assertThrows(DateTimeException.class, () -> time1.withDate(base2));
+        assertThrows(DateTimeException.class, () -> time1.withDate(base3));
+        assertThrows(DateTimeException.class, () -> time1.withDate(base4));
 
-//        assertEquals("1980-01-01T18:45+12:00[Pacific/Funafuti]", time2.withDate(base1).toString());
+        assertThrows(DateTimeException.class, () -> time2.withDate(base1).toString());
         assertEquals("1980-01-01T06:45+12:00[Pacific/Funafuti]", time2.withDate(base2).toString());
         assertEquals("2010-01-01T06:45+12:00[Pacific/Funafuti]", time2.withDate(base3).toString());
-        // @todo difference between JDK 8 & 11: 1900-01-01T06:45+11:32:04[Pacific/Funafuti]
         assertEquals("1900-01-01T06:45+11:56:52[Pacific/Funafuti]", time2.withDate(base4).toString());
 
 
@@ -85,20 +75,19 @@ public class ZonedTimeTest1 {
         ZonedDate date1 = ZonedDate.of(ZonedDateTime.of(2001, 2, 2, 0, 0, 0, 0, PL));
         ZonedDate date2 = ZonedDate.of(ZonedDateTime.of(2001, 7, 7, 0, 0, 0, 0, PL));
 
-//        assertEquals("2001-02-02T12:15Z[UTC]", time3.withDate(date1).toString());
-//        assertEquals("2001-02-02T12:15Z[UTC]", date1.withTime(time3).toString());
-//        assertEquals("2001-07-07T11:15Z[UTC]", time3.withDate(date2).toString());
-//        assertEquals("2001-07-07T11:15Z[UTC]", date2.withTime(time3).toString());
+        assertThrows(DateTimeException.class, () -> time3.withDate(date1).toString());
+        assertThrows(DateTimeException.class, () -> date1.withTime(time3).toString());
+        assertThrows(DateTimeException.class, () -> time3.withDate(date2).toString());
+        assertThrows(DateTimeException.class, () -> date2.withTime(time3).toString());
 
         ZonedTime time4 = ZonedTime.of(ZonedDateTime.of(1970, 1, 1, 13, 15, 0, 0, PL));
         ZonedDate date3 = ZonedDate.of(ZonedDateTime.of(2001, 7, 7, 0, 0, 0, 0, DateTimeZones.UTC));
         ZonedDate date4 = ZonedDate.of(ZonedDateTime.of(2001, 7, 7, 0, 0, 0, 0, DateTimeZones.UTC));
 
-//        assertEquals("2001-07-07T15:15+02:00[Poland]", time4.withDate(date3).toString());
-//        assertEquals("2001-07-07T15:15+02:00[Poland]", date3.withTime(time4).toString());
-//        assertEquals("2001-07-07T15:15+02:00[Poland]", time4.withDate(date4).toString());
-//        assertEquals("2001-07-07T15:15+02:00[Poland]", date4.withTime(time4).toString());
-
+        assertThrows(DateTimeException.class, () -> time4.withDate(date3).toString());
+        assertThrows(DateTimeException.class, () -> date3.withTime(time4).toString());
+        assertThrows(DateTimeException.class, () -> time4.withDate(date4).toString());
+        assertThrows(DateTimeException.class, () -> date4.withTime(time4).toString());
     }
 
     @Test
@@ -114,46 +103,28 @@ public class ZonedTimeTest1 {
         ZonedTime time2 = DateTimeLiteral.time(zone, 6, 45, 0);
 
         assertEquals("1980-01-01T06:45Z[UTC]", time1.withDate(base1).toString());
-        assertThrows(DateTimeException.class, () -> {
-            time1.withDate(base2).toString();
-        });
-        assertThrows(DateTimeException.class, () -> {
-            time1.withDate(base3).toString();
-        });
-        assertThrows(DateTimeException.class, () -> {
-            time1.withDate(base4).toString();
-        });
-
-        assertThrows(DateTimeException.class, () -> {
-            time2.withDate(base1).toString();
-        });
+        assertThrows(DateTimeException.class, () -> time1.withDate(base2).toString());
+        assertThrows(DateTimeException.class, () -> time1.withDate(base3).toString());
+        assertThrows(DateTimeException.class, () -> time1.withDate(base4).toString());
+
+        assertThrows(DateTimeException.class, () -> time2.withDate(base1).toString());
         assertEquals("1980-01-01T06:45+12:00[Pacific/Funafuti]", time2.withDate(base2).toString());
         assertEquals("1980-01-01T06:45+12:00[Pacific/Funafuti]", base2.withTime(time2).toString());
         assertEquals("2010-01-01T06:45+12:00[Pacific/Funafuti]", time2.withDate(base3).toString());
         assertEquals("2010-01-01T06:45+12:00[Pacific/Funafuti]", base3.withTime(time2).toString());
 
-        // @todo difference between JDK 8 & 11: 1900-01-01T06:45+11:32:04[Pacific/Funafuti]
         assertEquals("1900-01-01T06:45+11:56:52[Pacific/Funafuti]", time2.withDate(base4).toString());
         assertEquals("1900-01-01T06:45+11:56:52[Pacific/Funafuti]", base4.withTime(time2).toString());
 
-
         ZonedTime time3 = DateTimeLiteral.timeUtc(13, 15, 0);
         ZonedTime time4 = DateTimeLiteral.time(PL, 13, 15, 0);
         ZonedDate date1 = DateTimeLiteral.date(PL, 2001, 2, 2);
         ZonedDate date2 = DateTimeLiteral.date(PL, 2001, 7, 7);
 
-        assertThrows(DateTimeException.class, () -> {
-            time3.withDate(date1).toString();
-        });
-        assertThrows(DateTimeException.class, () -> {
-            time3.withDate(date2).toString();
-        });
-        assertThrows(DateTimeException.class, () -> {
-            date1.withTime(time3).toString();
-        });
-        assertThrows(DateTimeException.class, () -> {
-            date2.withTime(time3).toString();
-        });
+        assertThrows(DateTimeException.class, () -> time3.withDate(date1).toString());
+        assertThrows(DateTimeException.class, () -> time3.withDate(date2).toString());
+        assertThrows(DateTimeException.class, () -> date1.withTime(time3).toString());
+        assertThrows(DateTimeException.class, () -> date2.withTime(time3).toString());
 
         assertEquals("2001-02-02T13:15+01:00[Europe/Warsaw]", date1.withTime(time4).toString());
         assertEquals("2001-07-07T13:15+02:00[Europe/Warsaw]", date2.withTime(time4).toString());

+ 0 - 3
assira.commons/src/test/java/net/ranides/assira/time/format/DateTimeWriterTest.java

@@ -64,9 +64,6 @@ public class DateTimeWriterTest {
         );
     }
 
-    // @todo move into NewAssert
-
-
     @Test
     public void testFormatDateTimeZoned() {
         ZonedDateTime time1 = DateTimeLiteral.datetime(DateTimeZones.TV, 2024, 10, 24, 11, 30, 15);

+ 59 - 0
assira.commons/src/test/java/net/ranides/assira/time/json/ZonedDateSerializerTest.java

@@ -0,0 +1,59 @@
+package net.ranides.assira.time.json;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.Builder;
+import lombok.Data;
+import net.ranides.assira.time.DateTimeZones;
+import net.ranides.assira.time.ZonedDate;
+import org.junit.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.Assert.*;
+
+public class ZonedDateSerializerTest {
+
+    @Test
+    public void testSerialization() throws JsonProcessingException {
+        // https://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring
+
+        MyItem item = MyItem.builder()
+            .id(1170)
+            .code("RTTI")
+            .date1(LocalDate.now())
+            .date2(ZonedDate.now(DateTimeZones.GMT))
+            .date3(ZonedDate.now())
+            .date4(ZonedDate.now())
+            .build();
+
+        ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
+
+        String output = mapper.writeValueAsString(item);
+        assertEquals("{\"id\":1170,\"code\":\"RTTI\",\"date1\":[2025,9,13],\"date2\":\"2025-09-13\",\"date3\":\"2025-09-13 [Europe/Belgrade]\",\"date4\":1757714400}", output);
+    }
+
+
+
+    @Data
+    @Builder
+    public static class MyItem {
+        private final int id;
+
+        private String code;
+
+        private LocalDate date1;
+
+        @JsonFormat(timezone = "Etc/GMT0")
+        private ZonedDate date2;
+
+        @JsonFormat(pattern = "yyyy-MM-dd' ['VV']'")
+        private ZonedDate date3;
+
+        @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
+        private ZonedDate date4;
+    }
+
+}