Преглед на файлове

fix: DateTimeWriter zone conversion

Ranides Atterwim преди 11 месеца
родител
ревизия
dd3a26086e

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

@@ -227,7 +227,7 @@ public class DateTimeUtils {
         if(input != null) {
             ZoneId zone = getZone(input);
             if(zone !=null && !zone.equals(expected)) {
-                throw new IllegalArgumentException("TimeZone mismatch: " + expected + " != " + input);
+                throw new DateTimeException("TimeZone mismatch: " + expected + " != " + input);
             }
         }
         return input;

+ 28 - 41
assira.commons/src/main/java/net/ranides/assira/time/format/DateTimeReader.java

@@ -3,6 +3,7 @@ package net.ranides.assira.time.format;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import net.ranides.assira.collection.lists.ListUtils;
 import net.ranides.assira.generic.ValueUtils;
 import net.ranides.assira.time.DateTimeUtils;
 import net.ranides.assira.time.ZonedDate;
@@ -24,7 +25,9 @@ import java.time.format.SignStyle;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalQueries;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
@@ -39,7 +42,7 @@ import static java.time.temporal.ChronoField.YEAR;
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 public class DateTimeReader {
 
-    private static final DateTimeFormatter ECMA_PARSER;
+    static final DateTimeFormatter ECMA_PARSER;
     static {
         Map<Long, String> dow = new HashMap<>();
         dow.put(1L, "Mon");
@@ -87,7 +90,7 @@ public class DateTimeReader {
             .toFormatter();
     }
 
-    private static final DateTimeFormatter DATETIME_PARSER = new DateTimeFormatterBuilder()
+    static final DateTimeFormatter DATETIME_PARSER = new DateTimeFormatterBuilder()
         .parseCaseInsensitive()
         .append(DateTimeFormatter.ISO_LOCAL_DATE)
         .appendLiteral('T')
@@ -103,7 +106,7 @@ public class DateTimeReader {
         .optionalEnd()
         .toFormatter();
 
-    private static final DateTimeFormatter DATE_PARSER = new DateTimeFormatterBuilder()
+    static final DateTimeFormatter DATE_PARSER = new DateTimeFormatterBuilder()
         .parseCaseInsensitive()
         .append(DateTimeFormatter.ISO_LOCAL_DATE)
         .optionalStart() // time is optional
@@ -121,7 +124,7 @@ public class DateTimeReader {
         .optionalEnd()
         .toFormatter();
 
-    public static final DateTimeFormatter TIME_PARSER = new DateTimeFormatterBuilder()
+    static final DateTimeFormatter TIME_PARSER = new DateTimeFormatterBuilder()
         .parseCaseInsensitive()
         .optionalStart() // date is optional
             .append(DateTimeFormatter.ISO_LOCAL_DATE)
@@ -143,7 +146,7 @@ public class DateTimeReader {
 
     public static final DateTimeReader RFC = new DateTimeReader(DateTimeFormatter.RFC_1123_DATE_TIME);
 
-    public static final DateTimeReader ECMA262 = new DateTimeReader(ECMA_PARSER).withParseAll(false);
+    public static final DateTimeReader ECMA262 = new DateTimeReader(ECMA_PARSER).withStrictMode(false);
 
     public static final DateTimeReader DATETIME = new DateTimeReader(DATETIME_PARSER);
 
@@ -161,41 +164,25 @@ public class DateTimeReader {
     @Getter
     private final ZoneId targetZone;
 
-    private final boolean parseAll;
+    private final boolean strictMode;
 
     public static DateTimeReader of(String... list) {
-        return of(0, list);
+        return of(ListUtils.map(Arrays.asList(list), DateTimeReader::new));
     }
 
     public static DateTimeReader of(DateTimeFormatter... list) {
-        return of(0, list);
+        return of(ListUtils.map(Arrays.asList(list), DateTimeReader::new));
     }
 
     public static DateTimeReader of(DateTimeReader... list) {
-        return of(0, list);
+        return of(Arrays.asList(list));
     }
 
-    private static DateTimeReader of(int offset, DateTimeReader... list) {
-        if(offset == list.length - 1) {
-            return list[offset];
+    private static DateTimeReader of(List<DateTimeReader> list) {
+        if(list.size() == 1) {
+            return list.get(0);
         } else {
-            return list[offset].withFallback(of(offset + 1, list));
-        }
-    }
-
-    private static DateTimeReader of(int offset, DateTimeFormatter... list) {
-        if(offset == list.length - 1) {
-            return new DateTimeReader(list[offset]);
-        } else {
-            return new DateTimeReader(list[offset]).withFallback(of(offset + 1, list));
-        }
-    }
-
-    private static DateTimeReader of(int offset, String... list) {
-        if(offset == list.length - 1) {
-            return new DateTimeReader(list[offset]);
-        } else {
-            return new DateTimeReader(list[offset]).withFallback(of(offset + 1, list));
+            return list.get(0).withFallback(of(list.subList(1, list.size())));
         }
     }
 
@@ -208,7 +195,7 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withFallback(DateTimeReader fallback) {
-        return new DateTimeReader(fallback, parser, sourceZone, targetZone, parseAll);
+        return new DateTimeReader(fallback, parser, sourceZone, targetZone, strictMode);
     }
 
     public DateTimeReader withFormat(String newFormat) {
@@ -216,11 +203,11 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withFormat(DateTimeFormatter newFormat) {
-        return new DateTimeReader(fallback, newFormat, sourceZone, targetZone, parseAll);
+        return new DateTimeReader(fallback, newFormat, sourceZone, targetZone, strictMode);
     }
 
     public DateTimeReader withLocale(Locale newLocale) {
-        return new DateTimeReader(fallback, parser.withLocale(newLocale), sourceZone, targetZone, parseAll);
+        return new DateTimeReader(fallback, parser.withLocale(newLocale), sourceZone, targetZone, strictMode);
     }
 
     public DateTimeReader withLocale(String newLocale) {
@@ -232,7 +219,7 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withZone(ZoneId newZone) {
-        return new DateTimeReader(fallback, parser, newZone, newZone, parseAll);
+        return new DateTimeReader(fallback, parser, newZone, newZone, strictMode);
     }
 
     public DateTimeReader withZone(String newZone) {
@@ -240,11 +227,11 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withoutZone() {
-        return new DateTimeReader(fallback, parser, null, null, parseAll);
+        return new DateTimeReader(fallback, parser, null, null, strictMode);
     }
 
     public DateTimeReader withSourceZone(ZoneId newZone) {
-        return new DateTimeReader(fallback, parser, newZone, targetZone, parseAll);
+        return new DateTimeReader(fallback, parser, newZone, targetZone, strictMode);
     }
 
     public DateTimeReader withSourceZone(String newZone) {
@@ -252,11 +239,11 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withoutSourceZone() {
-        return new DateTimeReader(fallback, parser, null, targetZone, parseAll);
+        return new DateTimeReader(fallback, parser, null, targetZone, strictMode);
     }
 
     public DateTimeReader withTargetZone(ZoneId newZone) {
-        return new DateTimeReader(fallback, parser, sourceZone, newZone, parseAll);
+        return new DateTimeReader(fallback, parser, sourceZone, newZone, strictMode);
     }
 
     public DateTimeReader withTargetZone(String newZone) {
@@ -264,11 +251,11 @@ public class DateTimeReader {
     }
 
     public DateTimeReader withoutTargetZone() {
-        return new DateTimeReader(fallback, parser, sourceZone, null, parseAll);
+        return new DateTimeReader(fallback, parser, sourceZone, null, strictMode);
     }
 
-    DateTimeReader withParseAll(boolean newParseAll) {
-        return new DateTimeReader(fallback, parser, sourceZone, targetZone, newParseAll);
+    DateTimeReader withStrictMode(boolean newMode) {
+        return new DateTimeReader(fallback, parser, sourceZone, targetZone, newMode);
     }
 
     public ZonedDateTime parse(String input) {
@@ -315,7 +302,7 @@ public class DateTimeReader {
 
     @NotNull
     private TemporalAccessor parse0(String input) {
-        return parseAll ? parser.parse(input) : parser.parse(input, new ParsePosition(0));
+        return strictMode ? parser.parse(input) : parser.parse(input, new ParsePosition(0));
     }
 
 

+ 47 - 31
assira.commons/src/main/java/net/ranides/assira/time/format/DateTimeWriter.java

@@ -2,43 +2,52 @@ package net.ranides.assira.time.format;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import net.ranides.assira.time.DateTimeUtils;
+import net.ranides.assira.time.ZonedDate;
+import net.ranides.assira.time.ZonedTime;
+import org.jetbrains.annotations.NotNull;
 
 import java.time.LocalDate;
 import java.time.LocalTime;
+import java.time.OffsetDateTime;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.TextStyle;
 import java.time.temporal.Temporal;
 import java.util.Locale;
 
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 public class DateTimeWriter {
 
-    // @todo test
+    static final DateTimeFormatter ECMA_WRITER = new DateTimeFormatterBuilder()
+        .append(DateTimeReader.ECMA_PARSER)
+        .appendZoneText(TextStyle.FULL)
+        .appendLiteral(')')
+        .toFormatter();
 
-    public static final DateTimeWriter RFC = new DateTimeWriter(DateTimeFormatter.RFC_1123_DATE_TIME);
 
     public static final DateTimeWriter ISO = new DateTimeWriter(DateTimeFormatter.ISO_DATE_TIME);
 
+    public static final DateTimeWriter RFC = new DateTimeWriter(DateTimeFormatter.RFC_1123_DATE_TIME);
+
     public static final DateTimeWriter DATETIME = new DateTimeWriter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
 
+    public static final DateTimeWriter ECMA262 = new DateTimeWriter(ECMA_WRITER);
+
     public static final DateTimeWriter DATE = new DateTimeWriter(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
 
     public static final DateTimeWriter TIME = new DateTimeWriter(DateTimeFormatter.ofPattern("HH:mm:ss"));
 
-    private final DateTimeWriter fallback;
-
     private final DateTimeFormatter printer;
 
-    public DateTimeWriter(DateTimeFormatter printer) {
-        this(null, printer);
-    }
-
-    public DateTimeWriter(String pattern) {
-        this(DateTimeFormatter.ofPattern(pattern));
+    public static DateTimeWriter of(String pattern) {
+        return new DateTimeWriter(DateTimeFormatter.ofPattern(pattern));
     }
 
-    public DateTimeWriter withFallback(DateTimeWriter fallback) {
-        return new DateTimeWriter(fallback, printer);
+    public static DateTimeWriter of(DateTimeFormatter formatter) {
+        return new DateTimeWriter(formatter);
     }
 
     public DateTimeWriter withFormat(String newFormat) {
@@ -46,11 +55,11 @@ public class DateTimeWriter {
     }
 
     public DateTimeWriter withFormat(DateTimeFormatter newFormat) {
-        return new DateTimeWriter(fallback, newFormat);
+        return new DateTimeWriter(newFormat);
     }
 
     public DateTimeWriter withLocale(Locale newLocale) {
-        return new DateTimeWriter(fallback, printer.withLocale(newLocale));
+        return new DateTimeWriter(printer.withLocale(newLocale));
     }
 
     public DateTimeWriter withLocale(String newLocale) {
@@ -62,7 +71,7 @@ public class DateTimeWriter {
     }
 
     public DateTimeWriter withZone(ZoneId newZone) {
-        return new DateTimeWriter(fallback, printer.withZone(newZone));
+        return new DateTimeWriter(printer.withZone(newZone));
     }
 
     public DateTimeWriter withZone(String newZone) {
@@ -70,31 +79,38 @@ public class DateTimeWriter {
     }
 
     public DateTimeWriter withoutZone() {
-        return new DateTimeWriter(fallback, printer.withZone(null));
-    }
-
-    public DateTimeWriter withTargetZone(ZoneId newZone) {
-        return withZone(newZone);
-    }
-
-    public DateTimeWriter withTargetZone(String newZone) {
-        return withZone(newZone);
-    }
-
-    public DateTimeWriter withoutTargetZone() {
-        return withoutZone();
+        return new DateTimeWriter(printer.withZone(null));
     }
 
     public String format(Temporal input) {
         return printer.format(input);
     }
 
-    public String formatDate(Temporal input) {
-        return printer.format(input.with(LocalTime.MIN));
+    public String formatDate(@NotNull Temporal input) {
+        return printer.format(adjustZone(input).with(LocalTime.MIN));
     }
 
     public String formatTime(Temporal input) {
-        return printer.format(input.with(LocalDate.ofEpochDay(0)));
+        return printer.format(adjustZone(input).with(LocalDate.ofEpochDay(0)));
+    }
+
+    private Temporal adjustZone(Temporal input) {
+        if(null == printer.getZone()) {
+            return input;
+        }
+        if(input instanceof ZonedDateTime) {
+            return DateTimeUtils.withZone((ZonedDateTime) input, printer.getZone());
+        }
+        if(input instanceof OffsetDateTime) {
+            return DateTimeUtils.withZone((OffsetDateTime) input, printer.getZone());
+        }
+        if(input instanceof ZonedDate) {
+            DateTimeUtils.requireZone(printer.getZone(), input);
+        }
+        if(input instanceof ZonedTime) {
+            DateTimeUtils.requireZone(printer.getZone(), input);
+        }
+        return input;
     }
 
     public ZoneId getTargetZone() {

+ 1 - 1
assira.commons/src/main/java/net/ranides/assira/time/json/JsonFormatUtils.java

@@ -54,7 +54,7 @@ class JsonFormatUtils {
         if(newZones.length == 1) {
             return base.withZone(newZones[0]);
         } else if(newZones.length == 2) {
-            return base.withTargetZone(newZones[1]);
+            return base.withZone(newZones[1]);
         } else {
             throw new UnsupportedOperationException();
         }

+ 1 - 1
assira.commons/src/main/java/net/ranides/assira/time/json/ZonedDateSerializer.java

@@ -18,7 +18,7 @@ public class ZonedDateSerializer extends StdSerializer<ZonedDate> implements Con
     private static final DateTimeWriter DEFAULT = DateTimeWriter.DATE
         .withFormat("yyyy-MM-dd")
         .withoutLocale()
-        .withoutTargetZone();
+        .withoutZone();
 
     private final DateTimeWriter writer;
     private final boolean useNumeric;

+ 1 - 1
assira.commons/src/main/java/net/ranides/assira/time/json/ZonedTimeSerializer.java

@@ -17,7 +17,7 @@ public class ZonedTimeSerializer extends StdSerializer<ZonedTime> implements Con
     private static final DateTimeWriter DEFAULT = DateTimeWriter.TIME
         .withFormat("HH:mm:ss.SSS")
         .withoutLocale()
-        .withoutTargetZone();
+        .withoutZone();
 
     private final DateTimeWriter writer;
     private final boolean useNumeric;

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

@@ -0,0 +1,77 @@
+package net.ranides.assira.time.format;
+
+import net.ranides.assira.time.DateTimeLiteral;
+import net.ranides.assira.time.DateTimeZones;
+import net.ranides.assira.time.ZonedDate;
+import org.junit.Test;
+
+import java.time.DateTimeException;
+import java.time.ZonedDateTime;
+import java.time.temporal.UnsupportedTemporalTypeException;
+import java.util.Locale;
+
+import static net.ranides.assira.junit.NewAssert.assertThrows;
+import static org.junit.Assert.assertEquals;
+
+public class DateTimeWriterTest {
+
+    @Test
+    public void testFormat() {
+        ZonedDateTime time1 = DateTimeLiteral.datetime(DateTimeZones.TV, 2024, 10, 24, 23, 30, 15);
+        ZonedDateTime time2 = DateTimeLiteral.datetime(DateTimeZones.PL, 2024, 10, 24, 23, 30, 15);
+
+        assertEquals("2024-10-24T23:30:15+12:00[Tuvalu]", DateTimeWriter.ISO.format(time1) );
+        assertEquals("2024-10-24 23:30:15", DateTimeWriter.DATETIME.format(time1) );
+        assertEquals("2024-10-24", DateTimeWriter.DATE.format(time1) );
+        assertEquals("23:30:15", DateTimeWriter.TIME.format(time1) );
+        assertEquals("Thu, 24 Oct 2024 23:30:15 +1200", DateTimeWriter.RFC.format(time1) );
+
+        assertEquals("Thu Oct 24 2024 23:30:15 GMT+1200 (Tuvalu)", DateTimeWriter.ECMA262.format(time1) );
+        assertEquals("Thu Oct 24 2024 23:30:15 GMT+0200 (Central European Summer Time)", DateTimeWriter.ECMA262.format(time2) );
+        assertEquals("Thu Oct 24 2024 23:30:15 GMT+0200 (Central European Summer Time)", DateTimeWriter.ECMA262.withLocale(Locale.forLanguageTag("pl")).format(time2) );
+    }
+
+    @Test
+    public void testFormatDateTimeZoned() {
+        ZonedDateTime time1 = DateTimeLiteral.datetime(DateTimeZones.TV, 2024, 10, 24, 11, 30, 15);
+
+        assertEquals("2024-10-24T11:30:15+12:00[Tuvalu]", DateTimeWriter.ISO.withoutZone().format(time1) );
+        assertEquals("2024-10-24T01:30:15+02:00[Europe/Warsaw]", DateTimeWriter.ISO.withZone(DateTimeZones.PL).format(time1));
+        assertEquals("2024-10-23T23:30:15Z[GMT]", DateTimeWriter.ISO.withZone(DateTimeZones.GMT).format(time1));
+    }
+
+    @Test
+    public void testFormatDateZoned() {
+        ZonedDateTime time1 = DateTimeLiteral.datetime(DateTimeZones.TV, 2024, 10, 24, 11, 30, 15);
+        ZonedDate date1 = DateTimeLiteral.date(DateTimeZones.TV, 2024, 10, 24);
+
+        // little awkward but expected:
+        // timestamp is first converted to correct zone, and time component is truncated later
+        assertEquals("2024-10-24T00:00:00+12:00[Tuvalu]", DateTimeWriter.ISO.withoutZone().formatDate(time1) );
+        assertEquals("2024-10-24T00:00:00+02:00[Europe/Warsaw]", DateTimeWriter.ISO.withZone(DateTimeZones.PL).formatDate(time1));
+        assertEquals("2024-10-23T00:00:00Z[GMT]", DateTimeWriter.ISO.withZone(DateTimeZones.GMT).formatDate(time1));
+
+        // expected: throws
+        assertThrows(DateTimeException.class, () -> {
+            DateTimeWriter.ISO.withoutZone().formatDate(date1);
+        });
+        assertThrows(DateTimeException.class, () -> {
+            DateTimeWriter.ISO.withZone(DateTimeZones.PL).formatDate(date1);
+        });
+        assertThrows(DateTimeException.class, () -> {
+            DateTimeWriter.ISO.withZone(DateTimeZones.GMT).formatDate(date1);
+        });
+
+        // fully reasonable
+        assertEquals("2024-10-24", DateTimeWriter.DATE.withoutZone().formatDate(date1) );
+        assertEquals("2024-10-24", DateTimeWriter.DATE.withZone(DateTimeZones.TV).formatDate(date1) );
+
+        // sad but only possible choice: we shouldn't convert plain date
+        assertThrows(DateTimeException.class, () -> {
+            DateTimeWriter.DATE.withZone(DateTimeZones.PL).formatDate(date1);
+        });
+        assertThrows(DateTimeException.class, () -> {
+            DateTimeWriter.DATE.withZone(DateTimeZones.GMT).formatDate(date1);
+        });
+    }
+}