From 54b72be07818625556a0691300a949f4a8a2db6f Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sun, 3 Mar 2024 18:47:31 -0500 Subject: [PATCH] Calendar, Date, and Timestamp use the same conversion to String, which outputs the date-time in local-time zone, which they are used to, but includes milliseconds and the offset from GMT, or Z if GMT. This way, the user can always tell what this time is in terms of UTC/GMT. --- .../com/cedarsoftware/util/Convention.java | 2 +- .../util/convert/CalendarConversions.java | 6 +-- .../cedarsoftware/util/convert/Converter.java | 4 +- .../util/convert/DateConversions.java | 40 ++++++++++++------- .../util/convert/StringConversions.java | 5 ++- .../util/convert/ConverterEverythingTest.java | 33 +++++++++------ .../util/convert/ConverterTest.java | 23 ++++++----- .../util/convert/StringConversionsTests.java | 3 -- 8 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/Convention.java b/src/main/java/com/cedarsoftware/util/Convention.java index 111211de..4ed94944 100644 --- a/src/main/java/com/cedarsoftware/util/Convention.java +++ b/src/main/java/com/cedarsoftware/util/Convention.java @@ -31,7 +31,7 @@ public static void throwIfNull(Object value, String message) { * @throws IllegalArgumentException if the string passed in is null or empty */ public static void throwIfNullOrEmpty(String value, String message) { - if (value == null || value.isEmpty()) { + if (StringUtilities.isEmpty(value)) { throw new IllegalArgumentException(message); } } diff --git a/src/main/java/com/cedarsoftware/util/convert/CalendarConversions.java b/src/main/java/com/cedarsoftware/util/convert/CalendarConversions.java index d65422a3..dab5a3d9 100644 --- a/src/main/java/com/cedarsoftware/util/convert/CalendarConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/CalendarConversions.java @@ -8,7 +8,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.Map; @@ -109,9 +108,8 @@ static Calendar create(long epochMilli, Converter converter) { } static String toString(Object from, Converter converter) { - ZonedDateTime zdt = toZonedDateTime(from, converter); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX", converter.getOptions().getLocale()); - return formatter.format(zdt); + Calendar cal = (Calendar) from; + return DateConversions.toString(cal.getTime(), converter); } static Map toMap(Object from, Converter converter) { diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index 35abfa10..129a5578 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -680,9 +680,9 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(ByteBuffer.class, String.class), ByteBufferConversions::toString); CONVERSION_DB.put(pair(CharBuffer.class, String.class), CharBufferConversions::toString); CONVERSION_DB.put(pair(Class.class, String.class), ClassConversions::toString); - CONVERSION_DB.put(pair(Date.class, String.class), DateConversions::dateToString); + CONVERSION_DB.put(pair(Date.class, String.class), DateConversions::toString); CONVERSION_DB.put(pair(java.sql.Date.class, String.class), DateConversions::sqlDateToString); - CONVERSION_DB.put(pair(Timestamp.class, String.class), DateConversions::timestampToString); + CONVERSION_DB.put(pair(Timestamp.class, String.class), DateConversions::toString); CONVERSION_DB.put(pair(LocalDate.class, String.class), LocalDateConversions::toString); CONVERSION_DB.put(pair(LocalTime.class, String.class), LocalTimeConversions::toString); CONVERSION_DB.put(pair(LocalDateTime.class, String.class), LocalDateTimeConversions::toString); diff --git a/src/main/java/com/cedarsoftware/util/convert/DateConversions.java b/src/main/java/com/cedarsoftware/util/convert/DateConversions.java index d789ce9a..7fff24c8 100644 --- a/src/main/java/com/cedarsoftware/util/convert/DateConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/DateConversions.java @@ -4,12 +4,13 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.sql.Timestamp; -import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.util.Calendar; import java.util.Date; import java.util.concurrent.atomic.AtomicLong; @@ -102,21 +103,32 @@ static AtomicLong toAtomicLong(Object from, Converter converter) { return new AtomicLong(toLong(from, converter)); } - static String dateToString(Object from, Converter converter) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone()); - return simpleDateFormat.format(((Date) from)); - } - static String sqlDateToString(Object from, Converter converter) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone()); - return simpleDateFormat.format(((Date) from)); + java.sql.Date sqlDate = (java.sql.Date) from; + return toString(new Date(sqlDate.getTime()), converter); } - static String timestampToString(Object from, Converter converter) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone()); - return simpleDateFormat.format(((Date) from)); + static String toString(Object from, Converter converter) { + Date date = (Date) from; + + // Convert Date to ZonedDateTime + ZonedDateTime zonedDateTime = date.toInstant().atZone(converter.getOptions().getZoneId()); + + // Build a formatter with optional milliseconds and always show timezone offset + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") + .appendOffset("+HH:MM", "Z") // Timezone offset + .toFormatter(); + + // Build a formatter with optional milliseconds and always show the timezone name +// DateTimeFormatter formatter = new DateTimeFormatterBuilder() +// .appendPattern("yyyy-MM-dd'T'HH:mm:ss") +// .appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true) // Optional milliseconds +// .appendLiteral('[') // Space separator +// .appendZoneId() +// .appendLiteral(']') +// .toFormatter(); + + return zonedDateTime.format(formatter); } } diff --git a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java index 432b82eb..5c4b7ca5 100644 --- a/src/main/java/com/cedarsoftware/util/convert/StringConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/StringConversions.java @@ -337,8 +337,9 @@ static Period toPeriod(Object from, Converter converter) { } static Date toDate(Object from, Converter converter) { - Instant instant = toInstant(from, converter); - return instant == null ? null : Date.from(instant); + String strDate = (String) from; + ZonedDateTime zdt = DateUtilities.parseDate(strDate, converter.getOptions().getZoneId(), true); + return Date.from(zdt.toInstant()); } static java.sql.Date toSqlDate(Object from, Converter converter) { diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 16ccb43d..67bda75c 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -381,19 +381,19 @@ private static void loadStringTests() { {Date.class, "java.util.Date", true} }); TEST_DB.put(pair(Date.class, String.class), new Object[][]{ - {new Date(1), toGmtString(new Date(1))}, - {new Date(Integer.MAX_VALUE), toGmtString(new Date(Integer.MAX_VALUE))}, - {new Date(Long.MAX_VALUE), toGmtString(new Date(Long.MAX_VALUE))} + {new Date(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly) + {new Date(0), "1970-01-01T09:00:00.000+09:00", true}, + {new Date(1), "1970-01-01T09:00:00.001+09:00", true}, }); TEST_DB.put(pair(java.sql.Date.class, String.class), new Object[][]{ - {new java.sql.Date(1), toGmtString(new java.sql.Date(1))}, - {new java.sql.Date(Integer.MAX_VALUE), toGmtString(new java.sql.Date(Integer.MAX_VALUE))}, - {new java.sql.Date(Long.MAX_VALUE), toGmtString(new java.sql.Date(Long.MAX_VALUE))} + {new java.sql.Date(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly) + {new java.sql.Date(0), "1970-01-01T09:00:00.000+09:00", true}, + {new java.sql.Date(1), "1970-01-01T09:00:00.001+09:00", true}, }); TEST_DB.put(pair(Timestamp.class, String.class), new Object[][]{ - {new Timestamp(1), toGmtString(new Timestamp(1))}, - {new Timestamp(Integer.MAX_VALUE), toGmtString(new Timestamp(Integer.MAX_VALUE))}, - {new Timestamp(Long.MAX_VALUE), toGmtString(new Timestamp(Long.MAX_VALUE))}, + {new Timestamp(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly) + {new Timestamp(0), "1970-01-01T09:00:00.000+09:00", true}, + {new Timestamp(1), "1970-01-01T09:00:00.001+09:00", true}, }); TEST_DB.put(pair(LocalDate.class, String.class), new Object[][]{ {LocalDate.parse("1965-12-31"), "1965-12-31"}, @@ -418,10 +418,19 @@ private static void loadStringTests() { TEST_DB.put(pair(Calendar.class, String.class), new Object[][]{ {(Supplier) () -> { Calendar cal = Calendar.getInstance(TOKYO_TZ); - cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17); - cal.set(Calendar.MILLISECOND, 409); + cal.setTimeInMillis(-1); + return cal; + }, "1970-01-01T08:59:59.999+09:00", true}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(TOKYO_TZ); + cal.setTimeInMillis(0); + return cal; + }, "1970-01-01T09:00:00.000+09:00", true}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(TOKYO_TZ); + cal.setTimeInMillis(1); return cal; - }, "2024-02-05T22:31:17.409+09:00"} + }, "1970-01-01T09:00:00.001+09:00", true}, }); TEST_DB.put(pair(Number.class, String.class), new Object[][]{ {(byte) 1, "1"}, diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index d7e73998..6c9b2afe 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -1794,18 +1794,20 @@ void testString_fromDate() Date date = cal.getTime(); String converted = this.converter.convert(date, String.class); - assertThat(converted).isEqualTo("2015-01-17T08:34:49"); + assertThat(converted).startsWith("2015-01-17T08:34:49"); } @Test void testString_fromCalendar() { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); - cal.clear(); - cal.set(2015, 0, 17, 8, 34, 49); - // TODO: Gets fixed when Date.class ==> String.class is tested/added -// assertEquals("2015-01-17T08:34:49", this.converter.convert(cal.getTime(), String.class)); - assertEquals("2015-01-17T08:34:49.000Z", this.converter.convert(cal, String.class)); + cal.setTimeInMillis(1421483689000L); + + Converter converter1 = new Converter(new ConverterOptions() { + public ZoneId getZoneId() { return ZoneId.of("GMT"); } + }); + assertEquals("2015-01-17T08:34:49.000Z", converter1.convert(cal.getTime(), String.class)); + assertEquals("2015-01-17T08:34:49.000Z", converter1.convert(cal, String.class)); } @Test @@ -1943,7 +1945,6 @@ void testAtomicInteger(Object value, int expectedResult) @Test void testAtomicInteger_withEmptyString() { AtomicInteger converted = this.converter.convert("", AtomicInteger.class); - //TODO: Do we want nullable types to default to zero assertThat(converted.get()).isEqualTo(0); } @@ -2503,7 +2504,9 @@ private static Stream unparseableDates() { @MethodSource("unparseableDates") void testUnparseableDates_Date(String date) { - assertNull(this.converter.convert(date, Date.class)); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.converter.convert(date, Date.class)) + .withMessageContaining("'dateStr' must not be null or empty String"); } @ParameterizedTest @@ -2923,7 +2926,9 @@ void testMapToDate() { map.clear(); map.put("value", ""); - assert null == this.converter.convert(map, Date.class); + assertThatThrownBy(() -> converter.convert(map, Date.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("'dateStr' must not be null or empty String"); map.clear(); map.put("value", null); diff --git a/src/test/java/com/cedarsoftware/util/convert/StringConversionsTests.java b/src/test/java/com/cedarsoftware/util/convert/StringConversionsTests.java index f3d2e786..5631f4c6 100644 --- a/src/test/java/com/cedarsoftware/util/convert/StringConversionsTests.java +++ b/src/test/java/com/cedarsoftware/util/convert/StringConversionsTests.java @@ -11,7 +11,6 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.Date; import java.util.stream.Stream; import com.cedarsoftware.util.ClassUtilities; @@ -248,9 +247,7 @@ private static Stream classesThatReturnNull_whenTrimmedToEmpty() { Arguments.of(Year.class), Arguments.of(Timestamp.class), Arguments.of(java.sql.Date.class), - Arguments.of(Date.class), Arguments.of(Instant.class), - Arguments.of(Date.class), Arguments.of(java.sql.Date.class), Arguments.of(Timestamp.class), Arguments.of(ZonedDateTime.class),