diff --git a/src/main/java/com/cedarsoftware/util/DateUtilities.java b/src/main/java/com/cedarsoftware/util/DateUtilities.java index ae251165..1336edd2 100644 --- a/src/main/java/com/cedarsoftware/util/DateUtilities.java +++ b/src/main/java/com/cedarsoftware/util/DateUtilities.java @@ -260,9 +260,10 @@ public static ZonedDateTime parseDate(String dateStr, ZoneId defaultZoneId, bool tz = matcher.group(5).trim(); } if (matcher.group(6) != null) { - if (StringUtilities.isEmpty(tz)) { // Only use timezone name when offset is not used - tz = stripBrackets(matcher.group(6).trim()); - } + // to make round trip of ZonedDateTime equivalent we need to use the original Zone as ZoneId + // ZoneId is a much broader definition handling multiple possible dates, and we want this to + // be equivalent to the original zone that was used if one was present. + tz = stripBrackets(matcher.group(6).trim()); } } diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index 2bcbbde1..505a43cb 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -620,6 +620,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(OffsetDateTime.class, OffsetDateTime.class), Converter::identity); CONVERSION_DB.put(pair(Map.class, OffsetDateTime.class), MapConversions::toOffsetDateTime); CONVERSION_DB.put(pair(String.class, OffsetDateTime.class), StringConversions::toOffsetDateTime); + CONVERSION_DB.put(pair(Long.class, OffsetDateTime.class), NumberConversions::toOffsetDateTime); // toOffsetTime CONVERSION_DB.put(pair(Void.class, OffsetTime.class), VoidConversions::toNull); @@ -854,6 +855,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(Number.class, Map.class), MapConversions::initMap); CONVERSION_DB.put(pair(Map.class, Map.class), MapConversions::toMap); CONVERSION_DB.put(pair(Enum.class, Map.class), MapConversions::initMap); + CONVERSION_DB.put(pair(OffsetDateTime.class, Map.class), OffsetDateTimeConversions::toMap); } public Converter(ConverterOptions options) { diff --git a/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java b/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java index c8472e84..0c239533 100644 --- a/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java @@ -80,7 +80,7 @@ static Date toDate(Object from, Converter converter) { static Calendar toCalendar(Object from, Converter converter) { return CalendarConversions.create(toLong(from, converter), converter); } - + static BigInteger toBigInteger(Object from, Converter converter) { return BigInteger.valueOf(toLong(from, converter)); } diff --git a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java index 4c099793..192aca4b 100644 --- a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java @@ -52,6 +52,7 @@ final class MapConversions { private static final String V = "_v"; private static final String VALUE = "value"; + private static final String DATE = "date"; private static final String TIME = "time"; private static final String ZONE = "zone"; private static final String YEAR = "year"; @@ -73,6 +74,14 @@ final class MapConversions { private static final String MOST_SIG_BITS = "mostSigBits"; private static final String LEAST_SIG_BITS = "leastSigBits"; + static final String OFFSET = "offset"; + + private static final String TOTAL_SECONDS = "totalSeconds"; + + static final String DATE_TIME = "dateTime"; + + private static final String ID = "id"; + private MapConversions() {} public static final String KEY_VALUE_ERROR_MESSAGE = "To convert from Map to %s the map must include one of the following: %s[_v], or [value] with associated values."; @@ -245,8 +254,12 @@ static OffsetTime toOffsetTime(Object from, Converter converter) { private static final String[] OFFSET_DATE_TIME_PARAMS = new String[] { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, NANO, OFFSET_HOUR, OFFSET_MINUTE }; static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(YEAR) && map.containsKey(OFFSET_HOUR)) { - ConverterOptions options = converter.getOptions(); + ConverterOptions options = converter.getOptions(); + if (map.containsKey(DATE_TIME) && map.containsKey(OFFSET)) { + LocalDateTime dateTime = converter.convert(map.get(DATE_TIME), LocalDateTime.class); + ZoneOffset zoneOffset = converter.convert(map.get(OFFSET), ZoneOffset.class); + return OffsetDateTime.of(dateTime, zoneOffset); + } else if (map.containsKey(YEAR) && map.containsKey(OFFSET_HOUR)) { int year = converter.convert(map.get(YEAR), int.class); int month = converter.convert(map.get(MONTH), int.class); int day = converter.convert(map.get(DAY), int.class); @@ -263,12 +276,30 @@ static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { } } + private static final String[] LOCAL_DATE_TIME_PARAMS = new String[] { DATE, TIME }; + static LocalDateTime toLocalDateTime(Object from, Converter converter) { - return fromValue(from, converter, LocalDateTime.class); + Map map = (Map) from; + if (map.containsKey(DATE)) { + LocalDate localDate = converter.convert(map.get(DATE), LocalDate.class); + LocalTime localTime = map.containsKey(TIME) ? converter.convert(map.get(TIME), LocalTime.class) : LocalTime.MIDNIGHT; + // validate date isn't null? + return LocalDateTime.of(localDate, localTime); + } else { + return fromValueForMultiKey(from, converter, LocalDateTime.class, LOCAL_DATE_TIME_PARAMS); + } } + private static final String[] ZONED_DATE_TIME_PARAMS = new String[] { ZONE, DATE_TIME }; static ZonedDateTime toZonedDateTime(Object from, Converter converter) { - return fromValue(from, converter, ZonedDateTime.class); + Map map = (Map) from; + if (map.containsKey(ZONE) && map.containsKey(DATE_TIME)) { + ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); + LocalDateTime localDateTime = converter.convert(map.get(DATE_TIME), LocalDateTime.class); + return ZonedDateTime.of(localDateTime, zoneId); + } else { + return fromValueForMultiKey(from, converter, ZonedDateTime.class, ZONED_DATE_TIME_PARAMS); + } } static Class toClass(Object from, Converter converter) { @@ -347,6 +378,10 @@ static ZoneId toZoneId(Object from, Converter converter) { ConverterOptions options = converter.getOptions(); ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); return zoneId; + } else if (map.containsKey(ID)) { + ConverterOptions options = converter.getOptions(); + ZoneId zoneId = converter.convert(map.get(ID), ZoneId.class); + return zoneId; } else { return fromSingleKey(from, converter, ZONE, ZoneId.class); } diff --git a/src/main/java/com/cedarsoftware/util/convert/NumberConversions.java b/src/main/java/com/cedarsoftware/util/convert/NumberConversions.java index 55527f06..d7da59bc 100644 --- a/src/main/java/com/cedarsoftware/util/convert/NumberConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/NumberConversions.java @@ -7,6 +7,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.Year; import java.time.ZonedDateTime; import java.util.Calendar; @@ -229,11 +230,15 @@ static LocalDateTime toLocalDateTime(Object from, Converter converter) { static LocalTime toLocalTime(Object from, Converter converter) { return toZonedDateTime(from, converter).toLocalTime(); } - + static ZonedDateTime toZonedDateTime(Object from, Converter converter) { return toInstant(from, converter).atZone(converter.getOptions().getZoneId()); } + static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { + return toZonedDateTime(from, converter).toOffsetDateTime(); + } + static Year toYear(Object from, Converter converter) { if (from instanceof Byte) { throw new IllegalArgumentException("Cannot convert Byte to Year, not enough precision."); diff --git a/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java b/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java index b7ea90ae..707b70a6 100644 --- a/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java @@ -9,11 +9,15 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.OffsetTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; +import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import com.cedarsoftware.util.CompactLinkedMap; + /** * @author Kenny Partlow (kpartlow@gmail.com) *
@@ -34,11 +38,6 @@ final class OffsetDateTimeConversions { private OffsetDateTimeConversions() {} - static OffsetDateTime toDifferentZone(Object from, Converter converter) { - OffsetDateTime offsetDateTime = (OffsetDateTime) from; - return offsetDateTime.toInstant().atZone(converter.getOptions().getZoneId()).toOffsetDateTime(); - } - static Instant toInstant(Object from, Converter converter) { return ((OffsetDateTime)from).toInstant(); } @@ -48,15 +47,15 @@ static long toLong(Object from, Converter converter) { } static LocalDateTime toLocalDateTime(Object from, Converter converter) { - return toDifferentZone(from, converter).toLocalDateTime(); + return ((OffsetDateTime)from).toLocalDateTime(); } static LocalDate toLocalDate(Object from, Converter converter) { - return toDifferentZone(from, converter).toLocalDate(); + return ((OffsetDateTime)from).toLocalDate(); } static LocalTime toLocalTime(Object from, Converter converter) { - return toDifferentZone(from, converter).toLocalTime(); + return ((OffsetDateTime)from).toLocalTime(); } static AtomicLong toAtomicLong(Object from, Converter converter) { @@ -98,4 +97,16 @@ static String toString(Object from, Converter converter) { OffsetDateTime offsetDateTime = (OffsetDateTime) from; return offsetDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); } + + static Map toMap(Object from, Converter converter) { + OffsetDateTime offsetDateTime = (OffsetDateTime) from; + + LocalDateTime localDateTime = offsetDateTime.toLocalDateTime(); + ZoneOffset zoneOffset = offsetDateTime.getOffset(); + + Map target = new CompactLinkedMap<>(); + target.put(MapConversions.DATE_TIME, converter.convert(localDateTime, String.class)); + target.put(MapConversions.OFFSET, converter.convert(zoneOffset, String.class)); + return target; + } } diff --git a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java index e9e73a65..6652e50b 100644 --- a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java +++ b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java @@ -1,11 +1,5 @@ package com.cedarsoftware.util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; @@ -17,6 +11,12 @@ import java.util.TimeZone; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index 3c844a4c..315eac5c 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -28,7 +28,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; -import com.cedarsoftware.util.DeepEquals; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -38,6 +37,8 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; +import com.cedarsoftware.util.DeepEquals; + import static com.cedarsoftware.util.ArrayUtilities.EMPTY_BYTE_ARRAY; import static com.cedarsoftware.util.ArrayUtilities.EMPTY_CHAR_ARRAY; import static com.cedarsoftware.util.Converter.zonedDateTimeToMillis; @@ -3076,7 +3077,7 @@ void testMapToLocalDateTime() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, LocalDateTime.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to LocalDateTime the map must include one of the following: [_v], or [value] with associated values"); + .hasMessageContaining("To convert from Map to LocalDateTime the map must include one of the following: [date, time], [_v], or [value] with associated values"); } @Test @@ -3095,7 +3096,7 @@ void testMapToZonedDateTime() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, ZonedDateTime.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to ZonedDateTime the map must include one of the following: [_v], or [value] with associated values"); + .hasMessageContaining("To convert from Map to ZonedDateTime the map must include one of the following: [zone, dateTime], [_v], or [value] with associated values"); } diff --git a/src/test/java/com/cedarsoftware/util/convert/ZonedDateTimeConversionsTests.java b/src/test/java/com/cedarsoftware/util/convert/ZonedDateTimeConversionsTests.java new file mode 100644 index 00000000..62d59971 --- /dev/null +++ b/src/test/java/com/cedarsoftware/util/convert/ZonedDateTimeConversionsTests.java @@ -0,0 +1,59 @@ +package com.cedarsoftware.util.convert; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.cedarsoftware.util.DeepEquals; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ZonedDateTimeConversionsTests { + + private Converter converter; + + + private static final ZoneId TOKYO = ZoneId.of("Asia/Tokyo"); + private static final ZoneId CHICAGO = ZoneId.of("America/Chicago"); + private static final ZoneId ALASKA = ZoneId.of("America/Anchorage"); + + private static final ZonedDateTime ZDT_1 = ZonedDateTime.of(LocalDateTime.of(2019, 12, 15, 9, 7, 16, 2000), CHICAGO); + private static final ZonedDateTime ZDT_2 = ZonedDateTime.of(LocalDateTime.of(2027, 12, 23, 9, 7, 16, 2000), TOKYO); + private static final ZonedDateTime ZDT_3 = ZonedDateTime.of(LocalDateTime.of(2027, 12, 23, 9, 7, 16, 2000), ALASKA); + + @BeforeEach + public void before() { + // create converter with default options + this.converter = new Converter(new DefaultConverterOptions()); + } + + private static Stream roundTripZDT() { + return Stream.of( + Arguments.of(ZDT_1), + Arguments.of(ZDT_2), + Arguments.of(ZDT_3) + ); + } + + @ParameterizedTest + @MethodSource("roundTripZDT") + void testZonedDateTime(ZonedDateTime zdt) { + + String value = this.converter.convert(zdt, String.class); + ZonedDateTime actual = this.converter.convert(value, ZonedDateTime.class); + + assertTrue(DeepEquals.deepEquals(actual, zdt)); + + value = DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zdt); + actual = this.converter.convert(value, ZonedDateTime.class); + + assertTrue(DeepEquals.deepEquals(actual, zdt)); + } +}