Selaa lähdekoodia

draft: datetime

Ranides Atterwim 11 kuukautta sitten
vanhempi
commit
8e347f4467

+ 143 - 4
assira.commons/src/main/java/net/ranides/assira/time/DateTimeFormat.java

@@ -3,6 +3,7 @@ package net.ranides.assira.time;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import org.jetbrains.annotations.NotNull;
 
 import java.time.DateTimeException;
 import java.time.Instant;
@@ -12,13 +13,13 @@ import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.time.ZoneId;
-import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.DateTimeParseException;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalQueries;
 import java.util.Locale;
 import java.util.Objects;
 
@@ -47,6 +48,7 @@ public class DateTimeFormat {
         .optionalStart() // time is optional
         .appendLiteral('T')
         .append(DateTimeFormatter.ISO_LOCAL_TIME)
+        .optionalEnd()
         .optionalStart()  // zone and offset made optional
         .appendOffsetId()
         .optionalStart()
@@ -56,7 +58,6 @@ public class DateTimeFormat {
         .appendLiteral(']')
         .optionalEnd()
         .optionalEnd()
-        .optionalEnd()
         .toFormatter();
 
     public static final DateTimeFormatter TIME_PARSER = new DateTimeFormatterBuilder()
@@ -226,8 +227,27 @@ public class DateTimeFormat {
         TemporalAccessor output = parseFormatter().parseBest(
             input,
             ZonedDateTime::from,
+            OffsetDateTime::from,
             LocalDateTime::from
         );
+        // it is workaround for JDK8 which incorrectly parses timestamp with offset if FORMATTER has defined override zone
+        // 2020-02-02+10:00 is detected as "plain timestamp" (offset is ignored) and zone is loaded from formatter
+
+        // if we detected ZonedDateTime, there is a risk that zone is loaded from formatter
+        // try to parse again, without "override zone"
+        if(output instanceof ZonedDateTime && output.query(TemporalQueries.zoneId()).equals(sourceZone)) {
+            try {
+                output = ZonedDateTime.parse(input, parseFormatter().withZone(null));
+            } catch (DateTimeException e) {
+                // sorry, zone was incorrectly loaded from formatter
+                output = parseFormatter().parseBest(
+                    input,
+                    OffsetDateTime::from,
+                    LocalDateTime::from
+                );
+            }
+        }
+
         if(output instanceof ZonedDateTime) {
             return adjustZone((ZonedDateTime)output);
         }
@@ -250,12 +270,70 @@ public class DateTimeFormat {
             input,
             ZonedDateTime::from,
             LocalDateTime::from,
+            ZonedDate::from,
+            DateTimeFormat::make_OffsetDate,
             LocalDate::from
         );
 
+        // it is workaround for JDK8 which incorrectly parses timestamp with offset if FORMATTER has defined override zone
+        // 2020-02-02+10:00 is detected as "plain timestamp" (offset is ignored) and zone is loaded from formatter
+
+        // if we detected ZonedDateTime, there is a risk that zone is loaded from formatter
+        // try to parse again, without "override zone"
+        if(output instanceof ZonedDateTime && output.query(TemporalQueries.zoneId()).equals(sourceZone)) {
+            try {
+                output = ZonedDateTime.parse(input, parseFormatter().withZone(null));
+            } catch (DateTimeException e) {
+                // sorry, zone was incorrectly loaded from formatter
+                output = parseFormatter().parseBest(
+                    input,
+                    OffsetDateTime::from,
+                    LocalDateTime::from,
+                    ZonedDate::from,
+                    LocalDate::from
+                );
+            }
+        }
+
+        // if we detected ZonedDate, there is a risk that zone is loaded from formatter
+        // try to parse again, without "override zone"
+        if(output instanceof ZonedDate && ((ZonedDate)output).getZone().equals(sourceZone)) {
+            try {
+                output = ZonedDate.parse(input, parseFormatter().withZone(null));
+            } catch (DateTimeException e) {
+                // sorry, zone was incorrectly loaded from formatter
+                output = parseFormatter().parseBest(
+                    input,
+                    ZonedDateTime::from,
+                    LocalDateTime::from,
+                    DateTimeFormat::make_OffsetDate,
+                    LocalDate::from
+                );
+            }
+        }
+
+        if(output instanceof ZonedDate) {
+            ZonedDate dt = (ZonedDate) output;
+            if(targetZone != null && !targetZone.equals(dt.getZone())) {
+                throw new DateTimeParseException("Can't convert zone " + dt.getZone() + " into " + targetZone, input, input.length());
+            }
+            return dt;
+        }
         if(output instanceof ZonedDateTime) {
             return ZonedDate.of(adjustZone((ZonedDateTime)output));
         }
+        if(output instanceof OffsetDateTime) {
+            OffsetDateTime dt = (OffsetDateTime) output;
+            if(targetZone != null && !targetZone.equals(dt.getOffset())) {
+                if(!targetZone.getRules().isFixedOffset()) {
+                    throw new DateTimeParseException("Can't convert zone " + dt.getOffset() + " into " + targetZone, input, input.length());
+                }
+                int targetOffset = targetZone.getRules().getOffset(Instant.EPOCH).getTotalSeconds();
+                int sourceOffset = dt.getOffset().getTotalSeconds();
+                return ZonedDate.of(targetZone, dt.minusSeconds(sourceOffset).plusSeconds(targetOffset).toLocalDate());
+            }
+            return ZonedDate.of(dt.getOffset(), dt.toLocalDate());
+        }
         if(output instanceof LocalDateTime) {
             if(!hasInferredZone()) {
                 throw new DateTimeParseException("Unknown timezone", input, input.length());
@@ -272,6 +350,19 @@ public class DateTimeFormat {
         throw new DateTimeParseException("Invalid date", input, 0);
     }
 
+    private static @NotNull ZonedDate make_OffsetDate(@NotNull TemporalAccessor temporal) {
+        Objects.requireNonNull(temporal, "temporal");
+        ZoneId offset = temporal.query(TemporalQueries.offset());
+        LocalDate date = temporal.query(TemporalQueries.localDate());
+        if (offset == null) {
+            throw new DateTimeException("Unable to obtain offset from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        if (date == null) {
+            throw new DateTimeException("Unable to obtain LocalDate from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        return ZonedDate.of(offset, date);
+    }
+
     public ZonedTime parseTime(String input) {
         if(Objects.isNull(input)) {
             return null;
@@ -280,11 +371,58 @@ public class DateTimeFormat {
         TemporalAccessor output = parseFormatter().parseBest(
             input,
             ZonedDateTime::from,
+            OffsetDateTime::from,
             LocalDateTime::from,
+            ZonedTime::from,
             OffsetTime::from,
             LocalTime::from
         );
 
+        // it is workaround for JDK8 which incorrectly parses timestamp with offset if FORMATTER has defined override zone
+        // 2020-02-02T10:00+10:00 is detected as "plain timestamp" (offset is ignored) and zone is loaded from formatter
+
+        // if we detected ZonedDateTime, there is a risk that zone is loaded from formatter
+        // try to parse again, without "override zone"
+        if(output instanceof ZonedDateTime && output.query(TemporalQueries.zoneId()).equals(sourceZone)) {
+            try {
+                output = ZonedDateTime.parse(input, parseFormatter().withZone(null));
+            } catch (DateTimeException e) {
+                // sorry, zone was incorrectly loaded from formatter
+                output = parseFormatter().parseBest(
+                    input,
+                    OffsetDateTime::from,
+                    LocalDateTime::from,
+                    ZonedTime::from,
+                    OffsetTime::from,
+                    LocalTime::from
+                );
+            }
+        }
+
+        // if we detected ZonedTime, there is a risk that zone is loaded from formatter
+        // try to parse again, without "override zone"
+        if(output instanceof ZonedTime && output.query(TemporalQueries.zoneId()).equals(sourceZone)) {
+            try {
+                output = parseFormatter().withZone(null).parse(input, ZonedTime::from);
+            } catch (DateTimeException e) {
+                // sorry, zone was incorrectly loaded from formatter
+                output = parseFormatter().parseBest(
+                    input,
+                    OffsetDateTime::from,
+                    LocalDateTime::from,
+                    OffsetTime::from,
+                    LocalTime::from
+                );
+            }
+        }
+
+        if(output instanceof ZonedTime) {
+            ZonedTime dt = (ZonedTime) output;
+            if(targetZone != null && !targetZone.equals(dt.getZone())) {
+                throw new DateTimeParseException("Can't convert zone " + dt.getZone() + " into " + targetZone, input, input.length());
+            }
+            return dt;
+        }
         if(output instanceof ZonedDateTime) {
             return ZonedTime.of(adjustZone((ZonedDateTime)output));
         }
@@ -295,10 +433,11 @@ public class DateTimeFormat {
             ZonedDateTime dt = ZonedDateTime.of((LocalDateTime) output, inferredZone());
             return ZonedTime.of(adjustZone(dt));
         }
+        if(output instanceof OffsetDateTime) {
+            output = ((OffsetDateTime) output).toOffsetTime();
+        }
         if(output instanceof OffsetTime) {
             OffsetTime dt = (OffsetTime) output;
-            // @todo offsettime may skip zoneid
-            // OffsetTime may incorrectly interpret 12:00+02:00[Poland] as 12:00+02:00[fixed]
             if(targetZone != null && !targetZone.equals(dt.getOffset())) {
                 if(!targetZone.getRules().isFixedOffset()) {
                     throw new DateTimeParseException("Can't convert zone " + dt.getOffset() + " into " + targetZone, input, input.length());

+ 4 - 0
assira.commons/src/main/java/net/ranides/assira/time/DateTimeUtils.java

@@ -8,11 +8,15 @@ import org.jetbrains.annotations.Nullable;
 import java.sql.Timestamp;
 import java.time.DayOfWeek;
 import java.time.Duration;
+import java.time.Instant;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
+import java.time.OffsetTime;
 import java.time.Period;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAdjusters;

+ 21 - 0
assira.commons/src/main/java/net/ranides/assira/time/ZonedDate.java

@@ -16,9 +16,11 @@ import java.time.LocalTime;
 import java.time.Period;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoField;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalField;
 import java.time.temporal.TemporalQueries;
 import java.time.temporal.TemporalQuery;
@@ -72,6 +74,24 @@ public class ZonedDate implements Comparable<ZonedDate>, Temporal, ZonedRange {
         return of(zone, Instant.ofEpochSecond(epoch).atZone(zone).toLocalDate());
     }
 
+    public static @NotNull ZonedDate from(@NotNull TemporalAccessor temporal) {
+        Objects.requireNonNull(temporal, "temporal");
+        ZoneId zone = temporal.query(TemporalQueries.zoneId());
+        LocalDate date = temporal.query(TemporalQueries.localDate());
+        if (zone == null) {
+            throw new DateTimeException("Unable to obtain ZoneId from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        if (date == null) {
+            throw new DateTimeException("Unable to obtain LocalDate from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        return of(zone, date);
+    }
+
+    public static @NotNull ZonedDate parse(@NotNull CharSequence text, @NotNull DateTimeFormatter formatter) {
+        Objects.requireNonNull(formatter, "formatter");
+        return formatter.parse(text, ZonedDate::from);
+    }
+
     public static @NotNull ZonedDate now() {
         return of(ZonedDateTime.now());
     }
@@ -125,6 +145,7 @@ public class ZonedDate implements Comparable<ZonedDate>, Temporal, ZonedRange {
     }
 
     // @todo nullabity
+    // @todo from(TemporalAccessor) - we could use this inside DateFormat.parseBest
 
     public ZonedDate with(TemporalField field, long amount) {
         if(field.isTimeBased()) {

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

@@ -13,12 +13,16 @@ import java.time.DateTimeException;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoField;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalField;
 import java.time.temporal.TemporalQueries;
 import java.time.temporal.TemporalQuery;
@@ -31,6 +35,8 @@ public class ZonedTime implements Comparable<ZonedTime>, Temporal {
 
     // @todo SQL timestamp test
 
+    // @todo from(TemporalAccessor) - we could use this inside DateFormat.parseBest
+
     private final ZoneId zone;
 
     private final LocalTime time;
@@ -74,6 +80,24 @@ public class ZonedTime implements Comparable<ZonedTime>, Temporal {
         return of(zone, date);
     }
 
+    public static @NotNull ZonedTime from(@NotNull TemporalAccessor temporal) {
+        Objects.requireNonNull(temporal, "temporal");
+        ZoneId zone = temporal.query(TemporalQueries.zoneId());
+        LocalTime time = temporal.query(TemporalQueries.localTime());
+        if (zone == null) {
+            throw new DateTimeException("Unable to obtain ZoneId from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        if (time == null) {
+            throw new DateTimeException("Unable to obtain LocalTime from TemporalAccessor: " + temporal + " of type " + temporal.getClass().getName());
+        }
+        return of(zone, time);
+    }
+
+    public static @NotNull ZonedTime parse(@NotNull CharSequence text, @NotNull DateTimeFormatter formatter) {
+        Objects.requireNonNull(formatter, "formatter");
+        return formatter.parse(text, ZonedTime::from);
+    }
+
     public static @NotNull ZonedTime now() {
         return of(ZonedDateTime.now());
     }
@@ -262,8 +286,9 @@ public class ZonedTime implements Comparable<ZonedTime>, Temporal {
 
     @Override
     public long until(Temporal endExclusive, TemporalUnit unit) {
-        // @todo reimplement
-        return time.atDate(LocalDate.MIN).until(endExclusive.with(LocalDate.MIN), unit);
+        long a = time.getLong(ChronoField.NANO_OF_DAY);
+        long b = endExclusive.getLong(ChronoField.NANO_OF_DAY);
+        return LocalTime.ofNanoOfDay(a).until(LocalTime.ofNanoOfDay(b), unit);
     }
 
     public final Duration until(Temporal end) {

+ 118 - 91
assira.commons/src/test/java/net/ranides/assira/time/DateTimeParsersTest.java

@@ -4,140 +4,167 @@ import net.ranides.assira.junit.NewAssert;
 import org.junit.Test;
 
 import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
 import java.time.format.DateTimeParseException;
 
-import static net.ranides.assira.time.DateTimeZones.PL;
+import static net.ranides.assira.time.DateTimeFormat.DATE;
+import static net.ranides.assira.time.DateTimeFormat.DATETIME;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
 public class DateTimeParsersTest {
 
     @Test
-    public void testParsePolishDateTime() {
-        ZonedDate date1 = ZonedDate.now(DateTimeZones.UTC);
-        ZonedDate date2 = ZonedDate.now().plusDays(25);
-        ZonedDateTime date3 = ZonedDateTime.now().plusDays(40);
-        ZonedDate date4 = ZonedDate.ofEpochSecond(PL, 11_800_150L);
-
-        date1.isAfter(date2);
-        date1.isAfter(date3);
-
-        ZonedTime time1 = ZonedTime.now();
-        ZonedTime time2 = ZonedTime.now().plusMinutes(15);
-        ZonedDateTime time3 = ZonedDateTime.now().plusMinutes(30);
-
-        time1.isAfter(time2);
-        time1.isAfter(time3);
-
-        ZonedDateTime a = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00+02:00[Poland]");
-        ZonedDateTime b = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00Z");
-        ZonedDateTime c = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]");
-        ZonedDateTime d = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]");
-        ZonedDateTime e = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00+06:00");
-        ZonedDateTime f = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00+17:00");
-        ZonedDateTime g = DateTimeFormat.DATETIME.withZone(PL).parse("2020-08-24T08:45:00");
-        System.out.println(a);
-        System.out.println(b);
-        System.out.println(c);
-        System.out.println(d);
-        System.out.println(e);
-        System.out.println(f);
-        System.out.println(g);
+    public void testParseDateTimeDefault() {
+        NewAssert.assertThrows(DateTimeParseException.class, () -> {
+            DATETIME.parse("2020-08-24T08:45:00");
+        });
+
+        assertEquals("2020-08-24T08:45+02:00[Poland]", DATETIME.parse("2020-08-24T08:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-24T08:45Z", DATETIME.parse("2020-08-24T08:45:00Z").toString());
+        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", DATETIME.parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", DATETIME.parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T08:45+06:00", DATETIME.parse("2020-08-24T08:45:00+06:00").toString());
+        assertEquals("2020-08-24T08:45+17:00", DATETIME.parse("2020-08-24T08:45:00+17:00").toString());
     }
 
     @Test
-    public void testParseDateTime() {
+    public void testParseDateTimeSourced() {
+        ZoneId zone = ZoneId.of("Pacific/Funafuti");
 
-        ZonedDateTime a = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00+02:00[Poland]");
-        ZonedDateTime b = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00Z");
-        ZonedDateTime c = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]");
-        ZonedDateTime d = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]");
-        ZonedDateTime e = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00+06:00");
-        ZonedDateTime f = DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00+17:00");
+        assertEquals("2020-08-24T08:45+12:00[Pacific/Funafuti]", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00").toString());
+        assertEquals("2020-08-24T08:45+02:00[Poland]", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-24T08:45Z", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00Z").toString());
+        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T08:45+06:00", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00+06:00").toString());
+        assertEquals("2020-08-24T08:45+17:00", DATETIME.withSourceZone(zone).parse("2020-08-24T08:45:00+17:00").toString());
+    }
 
-        NewAssert.assertThrows(DateTimeParseException.class, () -> {
-            DateTimeFormat.DATETIME.parse("2020-08-24T08:45:00");
+    @Test
+    public void testParseDateTimeTargeted() {
+        ZoneId zone = ZoneId.of("Pacific/Funafuti");
+
+        assertEquals("2020-08-24T08:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-24T20:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00Z").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T14:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00+06:00").toString());
+        assertEquals("2020-08-24T03:45+12:00[Pacific/Funafuti]", DATETIME.withTargetZone(zone).parse("2020-08-24T08:45:00+17:00").toString());
+    }
+
+    @Test
+    public void testParseDateTimeZoned() {
+        ZoneId zone = ZoneId.of("Pacific/Funafuti");
+
+        assertEquals("2020-08-24T08:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-24T20:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00Z").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00+06:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T18:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00+17:00[Europe/Warsaw]").toString());
+        assertEquals("2020-08-24T14:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00+06:00").toString());
+        assertEquals("2020-08-24T03:45+12:00[Pacific/Funafuti]", DATETIME.withZone(zone).parse("2020-08-24T08:45:00+17:00").toString());
+    }
+
+    @Test
+    public void testParseDateDefault() {
+        assertThrows(DateTimeParseException.class, () -> {
+            DATE.parseDate("2024-10-16");
         });
+        assertEquals("2020-08-16 [Poland]", DATE.parseDate("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-16 [Z]", DATE.parseDate("2020-08-16T20:45:00Z").toString());
+        assertEquals("2020-08-16 [+06:00]", DATE.parseDate("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("2020-08-16 [+18:00]", DATE.parseDate("2020-08-16T20:45:00+18:00").toString());
+        assertThrows(DateTimeParseException.class, () -> {
+            DATE.parseDate("2020-08-16T20:45:00");
+        });
+        assertEquals("2020-08-16 [Poland]", DATE.parseDate("2020-08-16+02:00[Poland]").toString());
+        assertEquals("2020-08-16 [+07:00]", DATE.parseDate("2020-08-16+07:00").toString());
+    }
 
-        assertEquals("2020-08-24T08:45+02:00[Poland]", a.toString());
-        assertEquals("2020-08-24T08:45Z", b.toString());
-        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", c.toString());
-        assertEquals("2020-08-24T08:45+02:00[Europe/Warsaw]", d.toString());
-        assertEquals("2020-08-24T08:45+06:00", e.toString());
-        assertEquals("2020-08-24T08:45+17:00", f.toString());
+    @Test
+    public void testParseDateSourced() {
+        ZoneId zone = ZoneId.of("Pacific/Funafuti");
+
+        assertEquals("2024-10-16 [Pacific/Funafuti]", DATE.withSourceZone(zone).parseDate("2024-10-16").toString());
+        assertEquals("2020-08-16 [Poland]", DATE.withSourceZone(zone).parseDate("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-16 [Z]", DATE.withSourceZone(zone).parseDate("2020-08-16T20:45:00Z").toString());
+        assertEquals("2020-08-16 [+06:00]", DATE.withSourceZone(zone).parseDate("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("2020-08-16 [+18:00]", DATE.withSourceZone(zone).parseDate("2020-08-16T20:45:00+18:00").toString());
+        assertEquals("2020-08-16 [Pacific/Funafuti]", DATE.withSourceZone(zone).parseDate("2020-08-16T20:45:00").toString());
+        assertEquals("2020-08-16 [Poland]", DATE.withSourceZone(zone).parseDate("2020-08-16+02:00[Poland]").toString());
+        assertEquals("2020-08-16 [+07:00]", DATE.withSourceZone(zone).parseDate("2020-08-16+07:00").toString());
     }
 
     @Test
-    public void testParseDate() {
+    public void testParseDateTargeted() {
         ZoneId zone = ZoneId.of("Pacific/Funafuti");
 
-        ZonedDate a = DateTimeFormat.DATE.withZone(zone).parseDate("2024-10-16");
-        ZonedDate b = DateTimeFormat.DATE.withZone(zone).parseDate("2020-08-16T20:45:00+02:00[Poland]");
-        ZonedDate c = DateTimeFormat.DATE.withZone(zone).parseDate("2020-08-16T20:45:00Z");
-        ZonedDate d = DateTimeFormat.DATE.withZone(zone).parseDate("2020-08-16T20:45:00+06:00");
-        ZonedDate e = DateTimeFormat.DATE.withZone(zone).parseDate("2020-08-16T20:45:00+18:00");
-        ZonedDate f = DateTimeFormat.DATE.withZone(zone).parseDate("2020-08-16T20:45:00");
-
-        System.out.println(a);
-        System.out.println(b);
-        System.out.println(c);
-        System.out.println(d);
-        System.out.println(e);
-        System.out.println(f);
+        assertEquals("2024-10-16 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2024-10-16").toString());
+        assertEquals("2020-08-17 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("2020-08-17 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2020-08-16T20:45:00Z").toString());
+        assertEquals("2020-08-17 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("2020-08-16 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2020-08-16T20:45:00+18:00").toString());
+        assertEquals("2020-08-16 [Pacific/Funafuti]", DATE.withTargetZone(zone).parseDate("2020-08-16T20:45:00").toString());
+
+        assertThrows(DateTimeParseException.class, () -> {
+            DATE.withTargetZone(zone).parseDate("2020-08-16+02:00[Poland]");
+        });
+
+        assertThrows(DateTimeParseException.class, () -> {
+            DATE.withTargetZone(zone).parseDate("2020-08-16+07:00");
+        });
     }
 
     @Test
-    public void testParseTime1() {
+    public void testParseTimeDefault() {
+
         assertThrows(DateTimeParseException.class, () -> {
             DateTimeFormat.TIME.parseTime("10:45");
         });
-        ZonedTime b = DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+02:00[Poland]");
-        ZonedTime c = DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00Z");
-        ZonedTime d = DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+06:00");
-        ZonedTime e = DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+18:00");
         assertThrows(DateTimeParseException.class, () -> {
             DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00");
         });
 
-        ZonedTime g = DateTimeFormat.TIME.parseTime("20:45:00+06:00");
-        ZonedTime h = DateTimeFormat.TIME.parseTime("20:45:00+04:00[Europe/Astrakhan]");
+        assertEquals("20:45 [Poland]", DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("20:45 [Z]",      DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00Z").toString());
+        assertEquals("20:45 [+06:00]", DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("20:45 [+18:00]", DateTimeFormat.TIME.parseTime("2020-08-16T20:45:00+18:00").toString());
+        assertEquals("20:45 [+06:00]", DateTimeFormat.TIME.parseTime("20:45:00+06:00").toString());
+        assertEquals("20:45 [Europe/Astrakhan]", DateTimeFormat.TIME.parseTime("20:45:00+04:00[Europe/Astrakhan]").toString());
+    }
+
+    @Test
+    public void testParseTimeSourced() {
+        ZoneId zone = ZoneId.of("Pacific/Funafuti");
 
-        assertEquals("20:45 [Poland]", b.toString());
-        assertEquals("20:45 [Z]", c.toString());
-        assertEquals("20:45 [+06:00]", d.toString());
-        assertEquals("20:45 [+18:00]", e.toString());
-        assertEquals("20:45 [+06:00]", g.toString());
-        assertEquals("20:45 [+04:00]", h.toString());
+        assertEquals("10:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("10:45").toString());
+        assertEquals("20:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("2020-08-16T20:45:00").toString());
+        assertEquals("20:45 [Poland]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("20:45 [Z]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("2020-08-16T20:45:00Z").toString());
+        assertEquals("20:45 [+06:00]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("20:45 [+18:00]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("2020-08-16T20:45:00+18:00").toString());
+        assertEquals("20:45 [+06:00]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("20:45:00+06:00").toString());
+        assertEquals("20:45 [Europe/Astrakhan]", DateTimeFormat.TIME.withSourceZone(zone).parseTime("20:45:00+04:00[Europe/Astrakhan]").toString());
     }
 
     @Test
-    public void testParseTime2() {
+    public void testParseTimeTargeted() {
         ZoneId zone = ZoneId.of("Pacific/Funafuti");
 
-        ZonedTime a = DateTimeFormat.TIME.withTargetZone(zone).parseTime("10:45");
-        ZonedTime b = DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+02:00[Poland]");
-        ZonedTime c = DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00Z");
-        ZonedTime d = DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+06:00");
-        ZonedTime e = DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+18:00");
-        ZonedTime f = DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00");
         assertThrows(DateTimeParseException.class, () -> {
             DateTimeFormat.TIME.withTargetZone(zone).parseTime("20:45:00+06:00");
         });
         assertThrows(DateTimeParseException.class, () -> {
-            // @todo wrong, zoneid is ignored
-            // @todo use case for withTargetZone(fixed offset) "20:45:00+04:00[Europe/Astrakhan]"
-            // @todo use case for withTargetZone(fixed offset) "20:45:00+04:00"
             DateTimeFormat.TIME.withTargetZone(zone).parseTime("20:45:00+04:00[Europe/Astrakhan]");
         });
 
-        assertEquals("10:45 [Pacific/Funafuti]", a.toString());
-        assertEquals("06:45 [Pacific/Funafuti]", b.toString());
-        assertEquals("08:45 [Pacific/Funafuti]", c.toString());
-        assertEquals("02:45 [Pacific/Funafuti]", d.toString());
-        assertEquals("14:45 [Pacific/Funafuti]", e.toString());
-        assertEquals("20:45 [Pacific/Funafuti]", f.toString());
+        assertEquals("10:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("10:45").toString());
+        assertEquals("06:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+02:00[Poland]").toString());
+        assertEquals("08:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00Z").toString());
+        assertEquals("02:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+06:00").toString());
+        assertEquals("14:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00+18:00").toString());
+        assertEquals("20:45 [Pacific/Funafuti]", DateTimeFormat.TIME.withTargetZone(zone).parseTime("2020-08-16T20:45:00").toString());
     }
 
 }

+ 15 - 14
assira.commons/src/test/java/net/ranides/assira/time/ZonedTimeTest.java

@@ -3,6 +3,7 @@ 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;
@@ -25,27 +26,27 @@ public class ZonedTimeTest {
 
     @Test
     public void testUntil() {
-        LocalTime timeL1 = LocalTime.of(8, 15);
-        LocalTime timeL2 = LocalTime.of(9, 20);
+        LocalTime time1a = LocalTime.of(8, 15);
+        LocalTime time1b = LocalTime.of(9, 20);
 
-        System.out.println(timeL1.until(timeL2, ChronoUnit.MINUTES));
+        assertEquals(65L, time1a.until(time1b, ChronoUnit.MINUTES));
 
-        ZonedTime timeA1 = ZonedTime.of(PL, LocalTime.of(8, 15, 0));
-        ZonedTime timeA2 = ZonedTime.of(PL, LocalTime.of(9, 20, 0));
+        ZonedTime time2a = ZonedTime.of(PL, LocalTime.of(8, 15, 0));
+        ZonedTime time2b = ZonedTime.of(PL, LocalTime.of(9, 20, 0));
 
-        System.out.println(timeA1.until(timeA2, ChronoUnit.MINUTES));
+        assertEquals(65L, time2a.until(time2b, ChronoUnit.MINUTES));
 
-        ZonedTime timeB1 = ZonedTime.of(ZonedDateTime.of(1980, 1, 1, 8, 15, 0, 0, PL));
-        ZonedTime timeB2 = ZonedTime.of(ZonedDateTime.of(1980, 1, 1, 9, 20, 0, 0, PL));
+        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));
 
-        System.out.println(timeB1.until(timeB2, ChronoUnit.MINUTES));
+        assertEquals(65L, time3a.until(time3b, ChronoUnit.MINUTES));
 
-        ZonedDateTime datetimeC1 = DateTimeLiteral.datetimeUtc(1980, 1, 1, 23, 45, 0);
-        ZonedDateTime datetimeC2 = DateTimeLiteral.datetime(1980, 1, 1, 23, 45, 0);
+        // @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);
 
-        System.out.println(datetimeC1);
-        System.out.println(datetimeC2);
-        System.out.println(DateTimeUtils.diff(datetimeC1, datetimeC2));
+        assertEquals(Duration.ofHours(23), DateTimeUtils.diff(datetime4a, datetime4b));
+        assertEquals(Duration.ofHours(1), DateTimeUtils.diff(datetime4b, datetime4a));
 
     }