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));
+ }
+}