diff --git a/src/main/java/com/cedarsoftware/util/MapUtilities.java b/src/main/java/com/cedarsoftware/util/MapUtilities.java index a24dab7e..d731b69b 100644 --- a/src/main/java/com/cedarsoftware/util/MapUtilities.java +++ b/src/main/java/com/cedarsoftware/util/MapUtilities.java @@ -203,4 +203,16 @@ public static Map mapOf(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V map.put(k5, v5); return Collections.unmodifiableMap(map); } + + public static Map mapOf(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) + { + Map map = new LinkedHashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + return Collections.unmodifiableMap(map); + } } diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index 6168817c..ed8ae5ba 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -681,6 +681,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(String.class, TimeZone.class), StringConversions::toTimeZone); CONVERSION_DB.put(pair(Map.class, TimeZone.class), MapConversions::toTimeZone); CONVERSION_DB.put(pair(ZoneId.class, TimeZone.class), ZoneIdConversions::toTimeZone); + CONVERSION_DB.put(pair(ZoneOffset.class, TimeZone.class), UNSUPPORTED); // Duration conversions supported CONVERSION_DB.put(pair(Void.class, Duration.class), VoidConversions::toNull); @@ -719,13 +720,16 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(String.class, ZoneId.class), StringConversions::toZoneId); CONVERSION_DB.put(pair(Map.class, ZoneId.class), MapConversions::toZoneId); CONVERSION_DB.put(pair(TimeZone.class, ZoneId.class), TimeZoneConversions::toZoneId); + CONVERSION_DB.put(pair(ZoneOffset.class, ZoneId.class), ZoneOffsetConversions::toZoneId); // ZoneOffset conversions supported CONVERSION_DB.put(pair(Void.class, ZoneOffset.class), VoidConversions::toNull); CONVERSION_DB.put(pair(ZoneOffset.class, ZoneOffset.class), Converter::identity); CONVERSION_DB.put(pair(String.class, ZoneOffset.class), StringConversions::toZoneOffset); CONVERSION_DB.put(pair(Map.class, ZoneOffset.class), MapConversions::toZoneOffset); - + CONVERSION_DB.put(pair(ZoneId.class, ZoneOffset.class), UNSUPPORTED); + CONVERSION_DB.put(pair(TimeZone.class, ZoneOffset.class), UNSUPPORTED); + // MonthDay conversions supported CONVERSION_DB.put(pair(Void.class, MonthDay.class), VoidConversions::toNull); CONVERSION_DB.put(pair(MonthDay.class, MonthDay.class), Converter::identity); diff --git a/src/main/java/com/cedarsoftware/util/convert/EnumConversions.java b/src/main/java/com/cedarsoftware/util/convert/EnumConversions.java index fbd041b4..c9126220 100644 --- a/src/main/java/com/cedarsoftware/util/convert/EnumConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/EnumConversions.java @@ -1,12 +1,6 @@ package com.cedarsoftware.util.convert; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.sql.Timestamp; -import java.time.Duration; -import java.time.Instant; import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; import com.cedarsoftware.util.CompactLinkedMap; @@ -37,39 +31,4 @@ static Map toMap(Object from, Converter converter) { target.put("name", enumInstance.name()); return target; } - - static long toLong(Object from, Converter converter) { - return ((Duration) from).toMillis(); - } - - static AtomicLong toAtomicLong(Object from, Converter converter) { - Duration duration = (Duration) from; - return new AtomicLong(duration.toMillis()); - } - - static BigInteger toBigInteger(Object from, Converter converter) { - Duration duration = (Duration) from; - BigInteger epochSeconds = BigInteger.valueOf(duration.getSeconds()); - BigInteger nanos = BigInteger.valueOf(duration.getNano()); - - // Convert seconds to nanoseconds and add the nanosecond part - return epochSeconds.multiply(BigIntegerConversions.BILLION).add(nanos); - } - - static double toDouble(Object from, Converter converter) { - Duration duration = (Duration) from; - return BigDecimalConversions.secondsAndNanosToDouble(duration.getSeconds(), duration.getNano()).doubleValue(); - } - - static BigDecimal toBigDecimal(Object from, Converter converter) { - Duration duration = (Duration) from; - return BigDecimalConversions.secondsAndNanosToDouble(duration.getSeconds(), duration.getNano()); - } - - static Timestamp toTimestamp(Object from, Converter converter) { - Duration duration = (Duration) from; - Instant epoch = Instant.EPOCH; - Instant timeAfterDuration = epoch.plus(duration); - return Timestamp.from(timeAfterDuration); - } } diff --git a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java index 417e8bfe..2a1acbd1 100644 --- a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java @@ -19,6 +19,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.AbstractMap; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -29,7 +30,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import com.cedarsoftware.util.ArrayUtilities; import com.cedarsoftware.util.CompactLinkedMap; import com.cedarsoftware.util.DateUtilities; import com.cedarsoftware.util.StringUtilities; @@ -79,7 +79,7 @@ final class MapConversions { static final String OFFSET_HOUR = "offsetHour"; static final String OFFSET_MINUTE = "offsetMinute"; static final String DATE_TIME = "dateTime"; - private static final String ID = "id"; + static final String ID = "id"; static final String LANGUAGE = "language"; static final String COUNTRY = "country"; static final String SCRIPT = "script"; @@ -87,18 +87,10 @@ final class MapConversions { static final String URI_KEY = "URI"; static final String URL_KEY = "URL"; static final String UUID = "UUID"; - static final String JAR = "jar"; - static final String AUTHORITY = "authority"; - static final String REF = "ref"; - static final String PORT = "port"; - static final String FILE = "file"; - static final String HOST = "host"; - static final String PROTOCOL = "protocol"; + static final String OPTIONAL = " (optional)"; 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."; - static Object toUUID(Object from, Converter converter) { Map map = (Map) from; @@ -115,7 +107,7 @@ static Object toUUID(Object from, Converter converter) { return new UUID(most, least); } - return fromMap(from, converter, UUID.class, UUID); + return fromMap(from, converter, UUID.class, new String[]{UUID}, new String[]{MOST_SIG_BITS, LEAST_SIG_BITS}); } static Byte toByte(Object from, Converter converter) { @@ -175,48 +167,75 @@ static AtomicBoolean toAtomicBoolean(Object from, Converter converter) { } static java.sql.Date toSqlDate(Object from, Converter converter) { - Long epochMillis = toEpochMillis(from, converter); - if (epochMillis == null) { - return fromMap(from, converter, java.sql.Date.class, EPOCH_MILLIS); + Map.Entry epochTime = toEpochMillis(from, converter); + if (epochTime == null) { + return fromMap(from, converter, java.sql.Date.class, new String[]{EPOCH_MILLIS}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); } - return new java.sql.Date(epochMillis); + return new java.sql.Date(epochTime.getKey()); } static Date toDate(Object from, Converter converter) { - Long epochMillis = toEpochMillis(from, converter); - if (epochMillis == null) { - return fromMap(from, converter, Date.class, EPOCH_MILLIS); + Map.Entry epochTime = toEpochMillis(from, converter); + if (epochTime == null) { + return fromMap(from, converter, Date.class, new String[]{EPOCH_MILLIS}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); } - return new Date(epochMillis); + return new Date(epochTime.getKey()); } static Timestamp toTimestamp(Object from, Converter converter) { - Long epochMillis = toEpochMillis(from, converter); - if (epochMillis == null) { - return fromMap(from, converter, Timestamp.class, EPOCH_MILLIS, NANOS); + Map map = (Map) from; + Object epochMillis = map.get(EPOCH_MILLIS); + if (epochMillis != null) { + long time = converter.convert(epochMillis, long.class); + int ns = converter.convert(map.get(NANOS), int.class); // optional + Timestamp timeStamp = new Timestamp(time); + timeStamp.setNanos(ns); + return timeStamp; + } + + Map.Entry epochTime = toEpochMillis(from, converter); + if (epochTime == null) { + return fromMap(from, converter, Timestamp.class, new String[]{EPOCH_MILLIS, NANOS + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}, new String[]{TIME, ZONE + OPTIONAL}); } - Map map = (Map) from; - Timestamp timestamp = new Timestamp(epochMillis); - int ns = converter.convert(map.get(NANOS), int.class); - timestamp.setNanos(ns); + Timestamp timestamp = new Timestamp(epochTime.getKey()); + int ns = converter.convert(epochTime.getValue(), int.class); + setNanosPreserveMillis(timestamp, ns); return timestamp; } + static void setNanosPreserveMillis(Timestamp timestamp, int nanoToSet) { + // Extract the current milliseconds and nanoseconds + int currentNanos = timestamp.getNanos(); + int milliPart = currentNanos / 1_000_000; // Milliseconds part of the current nanoseconds + + // Preserve the millisecond part of the current time and add the new nanoseconds + int newNanos = milliPart * 1_000_000 + (nanoToSet % 1_000_000); + + // Set the new nanoseconds value, preserving the milliseconds + timestamp.setNanos(newNanos); + } + static TimeZone toTimeZone(Object from, Converter converter) { - return fromMap(from, converter, TimeZone.class, ZONE); + return fromMap(from, converter, TimeZone.class, new String[]{ZONE}); } static Calendar toCalendar(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(EPOCH_MILLIS)) { - return converter.convert(map.get(EPOCH_MILLIS), Calendar.class); - } else if (map.containsKey(DATE) && map.containsKey(TIME)) { - LocalDate localDate = converter.convert(map.get(DATE), LocalDate.class); - LocalTime localTime = converter.convert(map.get(TIME), LocalTime.class); + Object epochMillis = map.get(EPOCH_MILLIS); + if (epochMillis != null) { + return converter.convert(epochMillis, Calendar.class); + } + + Object date = map.get(DATE); + Object time = map.get(TIME); + Object zone = map.get(ZONE); + if (date != null && time != null) { + LocalDate localDate = converter.convert(date, LocalDate.class); + LocalTime localTime = converter.convert(time, LocalTime.class); ZoneId zoneId; - if (map.containsKey(ZONE)) { - zoneId = converter.convert(map.get(ZONE), ZoneId.class); + if (zone != null) { + zoneId = converter.convert(zone, ZoneId.class); } else { zoneId = converter.getOptions().getZoneId(); } @@ -232,28 +251,29 @@ static Calendar toCalendar(Object from, Converter converter) { cal.set(Calendar.MILLISECOND, zdt.getNano() / 1_000_000); cal.getTime(); return cal; - } else if (map.containsKey(TIME) && !map.containsKey(DATE)) { - TimeZone timeZone; - if (map.containsKey(ZONE)) { - timeZone = converter.convert(map.get(ZONE), TimeZone.class); + } + + if (time != null && date == null) { + ZoneId zoneId; + if (zone != null) { + zoneId = converter.convert(zone, ZoneId.class); } else { - timeZone = converter.getOptions().getTimeZone(); + zoneId = converter.getOptions().getZoneId(); } - Calendar cal = Calendar.getInstance(timeZone); - String time = (String) map.get(TIME); - ZonedDateTime zdt = DateUtilities.parseDate(time, converter.getOptions().getZoneId(), true); + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId)); + ZonedDateTime zdt = DateUtilities.parseDate((String)time, zoneId, true); cal.setTimeInMillis(zdt.toInstant().toEpochMilli()); return cal; } - return fromMap(from, converter, Calendar.class, DATE, TIME, ZONE); + return fromMap(from, converter, Calendar.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } - static Long toEpochMillis(Object from, Converter converter) { + private static Map.Entry toEpochMillis(Object from, Converter converter) { Map map = (Map) from; Object epochMillis = map.get(EPOCH_MILLIS); if (epochMillis != null) { - return converter.convert(epochMillis, long.class); + return new AbstractMap.SimpleImmutableEntry<>(converter.convert(epochMillis, long.class), 0); } Object time = map.get(TIME); @@ -266,12 +286,13 @@ static Long toEpochMillis(Object from, Converter converter) { LocalTime lt = converter.convert(time, LocalTime.class); ZoneId zoneId = converter.convert(zone, ZoneId.class); ZonedDateTime zdt = ZonedDateTime.of(ld, lt, zoneId); - return zdt.toInstant().toEpochMilli(); + Instant instant = zdt.toInstant(); + return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); } // Time only if (time != null && date == null && zone == null) { - return converter.convert(time, Date.class).getTime(); + return new AbstractMap.SimpleImmutableEntry<>(converter.convert(time, Date.class).getTime(), 0); } // Time & Zone, no Date @@ -279,7 +300,8 @@ static Long toEpochMillis(Object from, Converter converter) { LocalDateTime ldt = converter.convert(time, LocalDateTime.class); ZoneId zoneId = converter.convert(zone, ZoneId.class); ZonedDateTime zdt = ZonedDateTime.of(ldt, zoneId); - return zdt.toInstant().toEpochMilli(); + Instant instant = zdt.toInstant(); + return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); } // Time & Date, no zone @@ -287,7 +309,8 @@ static Long toEpochMillis(Object from, Converter converter) { LocalDate ld = converter.convert(date, LocalDate.class); LocalTime lt = converter.convert(time, LocalTime.class); ZonedDateTime zdt = ZonedDateTime.of(ld, lt, converter.getOptions().getZoneId()); - return zdt.toInstant().toEpochMilli(); + Instant instant = zdt.toInstant(); + return new AbstractMap.SimpleImmutableEntry<>(instant.toEpochMilli(), instant.getNano()); } return null; @@ -298,7 +321,7 @@ static Locale toLocale(Object from, Converter converter) { String language = converter.convert(map.get(LANGUAGE), String.class); if (StringUtilities.isEmpty(language)) { - return fromMap(from, converter, Locale.class, LANGUAGE, COUNTRY, SCRIPT, VARIANT); + return fromMap(from, converter, Locale.class, new String[] {LANGUAGE, COUNTRY + OPTIONAL, SCRIPT + OPTIONAL, VARIANT + OPTIONAL}); } String country = converter.convert(map.get(COUNTRY), String.class); String script = converter.convert(map.get(SCRIPT), String.class); @@ -336,95 +359,123 @@ static Locale toLocale(Object from, Converter converter) { static LocalDate toLocalDate(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(YEAR) && map.containsKey(MONTH) && map.containsKey(DAY)) { - int month = converter.convert(map.get(MONTH), int.class); - int day = converter.convert(map.get(DAY), int.class); - int year = converter.convert(map.get(YEAR), int.class); - return LocalDate.of(year, month, day); + Object year = map.get(YEAR); + Object month = map.get(MONTH); + Object day = map.get(DAY); + if (year != null && month != null && day != null) { + int y = converter.convert(year, int.class); + int m = converter.convert(month, int.class); + int d = converter.convert(day, int.class); + return LocalDate.of(y, m, d); } - return fromMap(from, converter, LocalDate.class, DATE); + return fromMap(from, converter, LocalDate.class, new String[]{DATE}, new String[] {YEAR, MONTH, DAY}); } static LocalTime toLocalTime(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(HOUR) && map.containsKey(MINUTE)) { - int hour = converter.convert(map.get(HOUR), int.class); - int minute = converter.convert(map.get(MINUTE), int.class); - int second = converter.convert(map.get(SECOND), int.class); - int nano = converter.convert(map.get(NANO), int.class); - return LocalTime.of(hour, minute, second, nano); + Object hour = map.get(HOUR); + Object minute = map.get(MINUTE); + Object second = map.get(SECOND); + Object nano = map.get(NANO); + if (hour != null && minute != null) { + int h = converter.convert(hour, int.class); + int m = converter.convert(minute, int.class); + int s = converter.convert(second, int.class); + int n = converter.convert(nano, int.class); + return LocalTime.of(h, m, s, n); } - return fromMap(from, converter, LocalTime.class, TIME); + return fromMap(from, converter, LocalTime.class, new String[]{TIME}, new String[]{HOUR, MINUTE, SECOND + OPTIONAL, NANO + OPTIONAL}); } static OffsetTime toOffsetTime(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(HOUR) && map.containsKey(MINUTE)) { - int hour = converter.convert(map.get(HOUR), int.class); - int minute = converter.convert(map.get(MINUTE), int.class); - int second = converter.convert(map.get(SECOND), int.class); - int nano = converter.convert(map.get(NANO), int.class); - int offsetHour = converter.convert(map.get(OFFSET_HOUR), int.class); - int offsetMinute = converter.convert(map.get(OFFSET_MINUTE), int.class); - ZoneOffset zoneOffset = ZoneOffset.ofHoursMinutes(offsetHour, offsetMinute); - return OffsetTime.of(hour, minute, second, nano, zoneOffset); - } - - if (map.containsKey(TIME)) { - String ot = (String) map.get(TIME); - try { - return OffsetTime.parse(ot); - } catch (Exception e) { - throw new IllegalArgumentException("Unable to parse OffsetTime: " + ot, e); + Object hour = map.get(HOUR); + Object minute = map.get(MINUTE); + Object second = map.get(SECOND); + Object nano = map.get(NANO); + Object oh = map.get(OFFSET_HOUR); + Object om = map.get(OFFSET_MINUTE); + if (hour != null && minute != null) { + int h = converter.convert(hour, int.class); + int m = converter.convert(minute, int.class); + int s = converter.convert(second, int.class); + int n = converter.convert(nano, int.class); + ZoneOffset zoneOffset; + if (oh != null && om != null) { + int offsetHour = converter.convert(oh, int.class); + int offsetMinute = converter.convert(om, int.class); + try { + zoneOffset = ZoneOffset.ofHoursMinutes(offsetHour, offsetMinute); + } catch (Exception e) { + throw new IllegalArgumentException("Offset 'hour' and 'minute' are not correct", e); + } + return OffsetTime.of(h, m, s, n, zoneOffset); } } - return fromMap(from, converter, OffsetTime.class, TIME); + + Object time = map.get(TIME); + if (time != null) { + return converter.convert(time, OffsetTime.class); + } + return fromMap(from, converter, OffsetTime.class, new String[] {TIME}, new String[] {HOUR, MINUTE, SECOND + OPTIONAL, NANO + OPTIONAL, OFFSET_HOUR, OFFSET_MINUTE}); } static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(DATE) && map.containsKey(TIME)) { - LocalDate date = converter.convert(map.get(DATE), LocalDate.class); - LocalTime time = converter.convert(map.get(TIME), LocalTime.class); - ZoneOffset zoneOffset = converter.convert(map.get(OFFSET), ZoneOffset.class); - return OffsetDateTime.of(date, time, zoneOffset); + Object offset = map.get(OFFSET); + Object time = map.get(TIME); + Object date = map.get(DATE); + + if (time != null && offset != null && date == null) { + LocalDateTime ldt = converter.convert(time, LocalDateTime.class); + ZoneOffset zoneOffset = converter.convert(offset, ZoneOffset.class); + return OffsetDateTime.of(ldt, zoneOffset); } - 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); + + if (time != null && offset != null && date != null) { + LocalDate ld = converter.convert(date, LocalDate.class); + LocalTime lt = converter.convert(time, LocalTime.class); + ZoneOffset zoneOffset = converter.convert(offset, ZoneOffset.class); + return OffsetDateTime.of(ld, lt, zoneOffset); } - return fromMap(from, converter, OffsetDateTime.class, DATE, TIME, OFFSET); + + return fromMap(from, converter, OffsetDateTime.class, new String[] {TIME, OFFSET}, new String[] {DATE, TIME, OFFSET}); } static LocalDateTime toLocalDateTime(Object from, Converter converter) { 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? + Object date = map.get(DATE); + if (date != null) { + LocalDate localDate = converter.convert(date, LocalDate.class); + Object time = map.get(TIME); + LocalTime localTime = time != null ? converter.convert(time, LocalTime.class) : LocalTime.MIDNIGHT; return LocalDateTime.of(localDate, localTime); } - return fromMap(from, converter, LocalDateTime.class, DATE, TIME); + return fromMap(from, converter, LocalDateTime.class, new String[] {DATE, TIME + OPTIONAL}); } static ZonedDateTime toZonedDateTime(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(EPOCH_MILLIS)) { - return converter.convert(map.get(EPOCH_MILLIS), ZonedDateTime.class); + Object epochMillis = map.get(EPOCH_MILLIS); + if (epochMillis != null) { + return converter.convert(epochMillis, ZonedDateTime.class); } - if (map.containsKey(DATE) && map.containsKey(TIME) && map.containsKey(ZONE)) { - LocalDate localDate = converter.convert(map.get(DATE), LocalDate.class); - LocalTime localTime = converter.convert(map.get(TIME), LocalTime.class); - ZoneId zoneId = converter.convert(map.get(ZONE), ZoneId.class); + + Object date = map.get(DATE); + Object time = map.get(TIME); + Object zone = map.get(ZONE); + if (date != null && time != null && zone != null) { + LocalDate localDate = converter.convert(date, LocalDate.class); + LocalTime localTime = converter.convert(time, LocalTime.class); + ZoneId zoneId = converter.convert(zone, ZoneId.class); return ZonedDateTime.of(localDate, localTime, zoneId); } - 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); + if (zone != null && time != null && date == null) { + ZoneId zoneId = converter.convert(zone, ZoneId.class); + LocalDateTime localDateTime = converter.convert(time, LocalDateTime.class); return ZonedDateTime.of(localDateTime, zoneId); } - return fromMap(from, converter, ZonedDateTime.class, DATE, TIME, ZONE); + return fromMap(from, converter, ZonedDateTime.class, new String[] {EPOCH_MILLIS}, new String[] {DATE_TIME, ZONE}, new String[] {DATE, TIME, ZONE}); } static Class toClass(Object from, Converter converter) { @@ -433,42 +484,48 @@ static Class toClass(Object from, Converter converter) { static Duration toDuration(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(SECONDS)) { - long sec = converter.convert(map.get(SECONDS), long.class); + Object seconds = map.get(SECONDS); + if (seconds != null) { + long sec = converter.convert(seconds, long.class); int nanos = converter.convert(map.get(NANOS), int.class); return Duration.ofSeconds(sec, nanos); } - return fromMap(from, converter, Duration.class, SECONDS, NANOS); + return fromMap(from, converter, Duration.class, new String[] {SECONDS, NANOS + OPTIONAL}); } static Instant toInstant(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(SECONDS)) { - long sec = converter.convert(map.get(SECONDS), long.class); + Object seconds = map.get(SECONDS); + if (seconds != null) { + long sec = converter.convert(seconds, long.class); long nanos = converter.convert(map.get(NANOS), long.class); return Instant.ofEpochSecond(sec, nanos); } - return fromMap(from, converter, Instant.class, SECONDS, NANOS); + return fromMap(from, converter, Instant.class, new String[] {SECONDS, NANOS + OPTIONAL}); } static MonthDay toMonthDay(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(MONTH) && map.containsKey(DAY)) { - int month = converter.convert(map.get(MONTH), int.class); - int day = converter.convert(map.get(DAY), int.class); - return MonthDay.of(month, day); + Object month = map.get(MONTH); + Object day = map.get(DAY); + if (month != null && day != null) { + int m = converter.convert(month, int.class); + int d = converter.convert(day, int.class); + return MonthDay.of(m, d); } - return fromMap(from, converter, MonthDay.class, MONTH, DAY); + return fromMap(from, converter, MonthDay.class, new String[] {MONTH, DAY}); } static YearMonth toYearMonth(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(YEAR) && map.containsKey(MONTH)) { - int year = converter.convert(map.get(YEAR), int.class); - int month = converter.convert(map.get(MONTH), int.class); - return YearMonth.of(year, month); + Object year = map.get(YEAR); + Object month = map.get(MONTH); + if (year != null && month != null) { + int y = converter.convert(year, int.class); + int m = converter.convert(month, int.class); + return YearMonth.of(y, m); } - return fromMap(from, converter, YearMonth.class, YEAR, MONTH); + return fromMap(from, converter, YearMonth.class, new String[] {YEAR, MONTH}); } static Period toPeriod(Object from, Converter converter) { @@ -476,7 +533,7 @@ static Period toPeriod(Object from, Converter converter) { Map map = (Map) from; if (map.containsKey(VALUE) || map.containsKey(V)) { - return fromMap(from, converter, Period.class, YEARS, MONTHS, DAYS); + return fromMap(from, converter, Period.class, new String[] {YEARS, MONTHS, DAYS}); } Number years = converter.convert(map.getOrDefault(YEARS, 0), int.class); @@ -488,12 +545,15 @@ static Period toPeriod(Object from, Converter converter) { static ZoneId toZoneId(Object from, Converter converter) { Map map = (Map) from; - if (map.containsKey(ZONE)) { - return converter.convert(map.get(ZONE), ZoneId.class); - } else if (map.containsKey(ID)) { - return converter.convert(map.get(ID), ZoneId.class); + Object zone = map.get(ZONE); + if (zone != null) { + return converter.convert(zone, ZoneId.class); + } + Object id = map.get(ID); + if (id != null) { + return converter.convert(id, ZoneId.class); } - return fromMap(from, converter, ZoneId.class, ZONE); + return fromMap(from, converter, ZoneId.class, new String[] {ZONE}, new String[] {ID}); } static ZoneOffset toZoneOffset(Object from, Converter converter) { @@ -504,94 +564,30 @@ static ZoneOffset toZoneOffset(Object from, Converter converter) { int seconds = converter.convert(map.getOrDefault(SECONDS, 0), int.class); // optional return ZoneOffset.ofHoursMinutesSeconds(hours, minutes, seconds); } - return fromMap(from, converter, ZoneOffset.class, HOURS, MINUTES, SECONDS); + return fromMap(from, converter, ZoneOffset.class, new String[] {HOURS, MINUTES + OPTIONAL, SECONDS + OPTIONAL}); } static Year toYear(Object from, Converter converter) { - return fromMap(from, converter, Year.class, YEAR); + return fromMap(from, converter, Year.class, new String[] {YEAR}); } static URL toURL(Object from, Converter converter) { Map map = (Map) from; - - String url = null; - try { - url = (String) map.get(URL_KEY); - if (StringUtilities.hasContent(url)) { - return converter.convert(url, URL.class); - } - url = (String) map.get(VALUE); - if (StringUtilities.hasContent(url)) { - return converter.convert(url, URL.class); - } - url = (String) map.get(V); - if (StringUtilities.hasContent(url)) { - return converter.convert(url, URL.class); - } - - url = mapToUrlString(map); - return URI.create(url).toURL(); - } catch (Exception e) { - throw new IllegalArgumentException("Cannot convert Map to URL. Malformed URL: '" + url + "'"); + String url = (String) map.get(URL_KEY); + if (StringUtilities.hasContent(url)) { + return converter.convert(url, URL.class); } + return fromMap(from, converter, URL.class, new String[] {URL_KEY}); } static URI toURI(Object from, Converter converter) { Map map = (Map) from; String uri = null; - try { - uri = (String) map.get(URI_KEY); - if (StringUtilities.hasContent(uri)) { - return converter.convert(map.get(URI_KEY), URI.class); - } - uri = (String) map.get(VALUE); - if (StringUtilities.hasContent(uri)) { - return converter.convert(map.get(VALUE), URI.class); - } - uri = (String) map.get(V); - if (StringUtilities.hasContent(uri)) { - return converter.convert(map.get(V), URI.class); - } - - uri = mapToUrlString(map); - return URI.create(uri); - } catch (Exception e) { - throw new IllegalArgumentException("Cannot convert Map to URI. Malformed URI: '" + uri + "'"); + uri = (String) map.get(URI_KEY); + if (StringUtilities.hasContent(uri)) { + return converter.convert(map.get(URI_KEY), URI.class); } - } - - private static String mapToUrlString(Map map) { - StringBuilder builder = new StringBuilder(20); - String protocol = (String) map.get(PROTOCOL); - String host = (String) map.get(HOST); - String file = (String) map.get(FILE); - String authority = (String) map.get(AUTHORITY); - String ref = (String) map.get(REF); - Long port = (Long) map.get(PORT); - - builder.append(protocol); - builder.append(':'); - if (!protocol.equalsIgnoreCase(JAR)) { - builder.append("//"); - } - if (authority != null && !authority.isEmpty()) { - builder.append(authority); - } else { - if (host != null && !host.isEmpty()) { - builder.append(host); - } - if (!port.equals(-1L)) { - builder.append(":" + port); - } - } - if (file != null && !file.isEmpty()) { - builder.append(file); - } - if (ref != null && !ref.isEmpty()) { - builder.append("#" + ref); - } - - return builder.toString(); + return fromMap(from, converter, URI.class, new String[] {URI_KEY}); } static Map initMap(Object from, Converter converter) { @@ -600,14 +596,19 @@ private static String mapToUrlString(Map map) { return map; } - private static T fromMap(Object from, Converter converter, Class type, String...keys) { + private static T fromMap(Object from, Converter converter, Class type, String[]...keySets) { Map map = (Map) from; - if (keys.length == 1) { - String key = keys[0]; - if (map.containsKey(key)) { - return converter.convert(map.get(key), type); + + // For any single-key Map types, convert them + for (String[] keys : keySets) { + if (keys.length == 1) { + String key = keys[0]; + if (map.containsKey(key)) { + return converter.convert(map.get(key), type); + } } } + if (map.containsKey(V)) { return converter.convert(map.get(V), type); } @@ -616,7 +617,17 @@ private static T fromMap(Object from, Converter converter, Class type, St return converter.convert(map.get(VALUE), type); } - String keyText = ArrayUtilities.isEmpty(keys) ? "" : "[" + String.join(", ", keys) + "], "; - throw new IllegalArgumentException(String.format(KEY_VALUE_ERROR_MESSAGE, Converter.getShortName(type), keyText)); + StringBuilder builder = new StringBuilder("To convert from Map to '" + Converter.getShortName(type) + "' the map must include: "); + + for (int i = 0; i < keySets.length; i++) { + builder.append("["); + // Convert the inner String[] to a single string, joined by ", " + builder.append(String.join(", ", keySets[i])); + builder.append("]"); + builder.append(", "); + } + + builder.append("[value], or [_v] as keys with associated values."); + throw new IllegalArgumentException(builder.toString()); } } diff --git a/src/main/java/com/cedarsoftware/util/convert/TimeZoneConversions.java b/src/main/java/com/cedarsoftware/util/convert/TimeZoneConversions.java index 7536d55f..9a2305f8 100644 --- a/src/main/java/com/cedarsoftware/util/convert/TimeZoneConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/TimeZoneConversions.java @@ -8,7 +8,25 @@ import static com.cedarsoftware.util.convert.MapConversions.ZONE; -public class TimeZoneConversions { +/** + * @author John DeRegnaucourt (jdereg@gmail.com) + * @author Kenny Partlow (kpartlow@gmail.com) + *
+ * Copyright (c) Cedar Software LLC + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * License + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +final class TimeZoneConversions { static String toString(Object from, Converter converter) { TimeZone timezone = (TimeZone)from; return timezone.getID(); @@ -18,7 +36,7 @@ static ZoneId toZoneId(Object from, Converter converter) { TimeZone tz = (TimeZone) from; return tz.toZoneId(); } - + static Map toMap(Object from, Converter converter) { TimeZone tz = (TimeZone) from; Map target = new CompactLinkedMap<>(); diff --git a/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java b/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java index 036bb766..59a4f4a8 100644 --- a/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java @@ -7,7 +7,6 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Date; @@ -64,14 +63,8 @@ static Duration toDuration(Object from, Converter converter) { static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { Timestamp timestamp = (Timestamp) from; - - // Get the current date-time in the options ZoneId timezone - ZonedDateTime zonedDateTime = ZonedDateTime.now(converter.getOptions().getZoneId()); - - // Extract the ZoneOffset - ZoneOffset zoneOffset = zonedDateTime.getOffset(); - - return timestamp.toInstant().atOffset(zoneOffset); + ZonedDateTime zdt = ZonedDateTime.ofInstant(timestamp.toInstant(), converter.getOptions().getZoneId()); + return zdt.toOffsetDateTime(); } static Calendar toCalendar(Object from, Converter converter) { diff --git a/src/main/java/com/cedarsoftware/util/convert/ZoneIdConversions.java b/src/main/java/com/cedarsoftware/util/convert/ZoneIdConversions.java index eaad6489..ce35c701 100644 --- a/src/main/java/com/cedarsoftware/util/convert/ZoneIdConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/ZoneIdConversions.java @@ -27,7 +27,7 @@ final class ZoneIdConversions { private ZoneIdConversions() {} - static Map toMap(Object from, Converter converter) { + static Map toMap(Object from, Converter converter) { ZoneId zoneID = (ZoneId) from; Map target = new CompactLinkedMap<>(); target.put("zone", zoneID.toString()); diff --git a/src/main/java/com/cedarsoftware/util/convert/ZoneOffsetConversions.java b/src/main/java/com/cedarsoftware/util/convert/ZoneOffsetConversions.java index 4e84b05e..c9c9e12c 100644 --- a/src/main/java/com/cedarsoftware/util/convert/ZoneOffsetConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/ZoneOffsetConversions.java @@ -1,5 +1,6 @@ package com.cedarsoftware.util.convert; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Map; @@ -7,6 +8,7 @@ import static com.cedarsoftware.util.convert.MapConversions.HOURS; import static com.cedarsoftware.util.convert.MapConversions.MINUTES; +import static com.cedarsoftware.util.convert.MapConversions.SECONDS; /** * @author John DeRegnaucourt (jdereg@gmail.com) @@ -29,7 +31,7 @@ final class ZoneOffsetConversions { private ZoneOffsetConversions() {} - static Map toMap(Object from, Converter converter) { + static Map toMap(Object from, Converter converter) { ZoneOffset offset = (ZoneOffset) from; Map target = new CompactLinkedMap<>(); int totalSeconds = offset.getTotalSeconds(); @@ -41,8 +43,12 @@ static Map toMap(Object from, Converter converter) { target.put(HOURS, hours); target.put(MINUTES, minutes); if (seconds != 0) { - target.put("seconds", seconds); + target.put(SECONDS, seconds); } return target; } + + static ZoneId toZoneId(Object from, Converter converter) { + return (ZoneId) from; + } } diff --git a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java index 965cd7a1..8e80569d 100644 --- a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java +++ b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java @@ -3,7 +3,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; -import java.time.DateTimeException; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; @@ -561,11 +560,7 @@ void testDateToStringFormat() } } - if (!okToTest) { - assertThatThrownBy(() -> DateUtilities.parseDate(x.toString())) - .isInstanceOf(DateTimeException.class) - .hasMessageContaining("Unknown time-zone ID"); - } else { + if (okToTest) { Date y = DateUtilities.parseDate(x.toString()); assertEquals(x.toString(), y.toString()); } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 326823c0..5a028d9b 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -56,19 +56,31 @@ import static com.cedarsoftware.util.convert.Converter.pair; import static com.cedarsoftware.util.convert.MapConversions.COUNTRY; import static com.cedarsoftware.util.convert.MapConversions.DATE; -import static com.cedarsoftware.util.convert.MapConversions.DATE_TIME; +import static com.cedarsoftware.util.convert.MapConversions.DAY; import static com.cedarsoftware.util.convert.MapConversions.EPOCH_MILLIS; +import static com.cedarsoftware.util.convert.MapConversions.HOUR; import static com.cedarsoftware.util.convert.MapConversions.HOURS; +import static com.cedarsoftware.util.convert.MapConversions.ID; import static com.cedarsoftware.util.convert.MapConversions.LANGUAGE; +import static com.cedarsoftware.util.convert.MapConversions.LEAST_SIG_BITS; +import static com.cedarsoftware.util.convert.MapConversions.MINUTE; import static com.cedarsoftware.util.convert.MapConversions.MINUTES; +import static com.cedarsoftware.util.convert.MapConversions.MONTH; +import static com.cedarsoftware.util.convert.MapConversions.MOST_SIG_BITS; +import static com.cedarsoftware.util.convert.MapConversions.NANO; import static com.cedarsoftware.util.convert.MapConversions.NANOS; +import static com.cedarsoftware.util.convert.MapConversions.OFFSET; +import static com.cedarsoftware.util.convert.MapConversions.OFFSET_HOUR; +import static com.cedarsoftware.util.convert.MapConversions.OFFSET_MINUTE; import static com.cedarsoftware.util.convert.MapConversions.SCRIPT; +import static com.cedarsoftware.util.convert.MapConversions.SECOND; import static com.cedarsoftware.util.convert.MapConversions.SECONDS; import static com.cedarsoftware.util.convert.MapConversions.TIME; import static com.cedarsoftware.util.convert.MapConversions.URI_KEY; import static com.cedarsoftware.util.convert.MapConversions.URL_KEY; import static com.cedarsoftware.util.convert.MapConversions.V; import static com.cedarsoftware.util.convert.MapConversions.VARIANT; +import static com.cedarsoftware.util.convert.MapConversions.YEAR; import static com.cedarsoftware.util.convert.MapConversions.ZONE; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -196,6 +208,19 @@ public ZoneId getZoneId() { loadUriTests(); loadUrlTests(); loadUuidTests(); + loadEnumTests(); + } + + /** + * Enum + */ + private static void loadEnumTests() { + TEST_DB.put(pair(Enum.class, Map.class), new Object[][]{ + { DayOfWeek.FRIDAY, mapOf("name", DayOfWeek.FRIDAY.name())}, + }); + TEST_DB.put(pair(Map.class, Enum.class), new Object[][]{ + { mapOf("name", "funky bunch"), new IllegalArgumentException("Unsupported conversion, source type [UnmodifiableMap ({name=funky bunch})] target type 'Enum'")}, + }); } /** @@ -211,6 +236,8 @@ private static void loadUuidTests() { TEST_DB.put(pair(Map.class, UUID.class), new Object[][]{ {mapOf("UUID", "f0000000-0000-0000-0000-000000000001"), UUID.fromString("f0000000-0000-0000-0000-000000000001"), true}, {mapOf("UUID", "f0000000-0000-0000-0000-00000000000x"), new IllegalArgumentException("Unable to convert 'f0000000-0000-0000-0000-00000000000x' to UUID")}, + {mapOf("xyz", "f0000000-0000-0000-0000-000000000000"), new IllegalArgumentException("Map to 'UUID' the map must include: [UUID], [mostSigBits, leastSigBits], [value], or [_v] as keys with associated values")}, + {mapOf(MOST_SIG_BITS, "1", LEAST_SIG_BITS, "2"), UUID.fromString("00000000-0000-0001-0000-000000000002")}, }); TEST_DB.put(pair(String.class, UUID.class), new Object[][]{ {"f0000000-0000-0000-0000-000000000001", UUID.fromString("f0000000-0000-0000-0000-000000000001"), true}, @@ -263,6 +290,12 @@ private static void loadUrlTests() { TEST_DB.put(pair(URL.class, URL.class), new Object[][]{ {toURL("https://chat.openai.com"), toURL("https://chat.openai.com")}, }); + TEST_DB.put(pair(URI.class, URL.class), new Object[][]{ + {toURI("urn:isbn:0451450523"), new IllegalArgumentException("Unable to convert URI to URL")}, + {toURI("https://cedarsoftware.com"), toURL("https://cedarsoftware.com"), true}, + {toURI("https://cedarsoftware.com:8001"), toURL("https://cedarsoftware.com:8001"), true}, + {toURI("https://cedarsoftware.com:8001#ref1"), toURL("https://cedarsoftware.com:8001#ref1"), true}, + }); TEST_DB.put(pair(String.class, URL.class), new Object[][]{ {"", null}, {"https://domain.com", toURL("https://domain.com"), true}, @@ -286,7 +319,9 @@ private static void loadUrlTests() { }); TEST_DB.put(pair(Map.class, URL.class), new Object[][]{ { mapOf(URL_KEY, "https://domain.com"), toURL("https://domain.com"), true}, - { mapOf(URL_KEY, "bad earl"), new IllegalArgumentException("Cannot convert Map to URL. Malformed URL: 'bad earl'")}, + { mapOf(URL_KEY, "bad earl"), new IllegalArgumentException("Cannot convert String 'bad earl' to URL")}, + { mapOf(MapConversions.VALUE, "https://domain.com"), toURL("https://domain.com")}, + { mapOf(V, "https://domain.com"), toURL("https://domain.com")}, }); TEST_DB.put(pair(URI.class, URL.class), new Object[][]{ {toURI("urn:isbn:0451450523"), new IllegalArgumentException("Unable to convert URI to URL")}, @@ -303,6 +338,14 @@ private static void loadUriTests() { TEST_DB.put(pair(URI.class, URI.class), new Object[][]{ {toURI("https://chat.openai.com"), toURI("https://chat.openai.com"), true}, }); + TEST_DB.put(pair(URL.class, URI.class), new Object[][]{ + { (Supplier) () -> { + try {return new URL("https://domain.com");} catch(Exception e){return null;} + }, toURI("https://domain.com"), true}, + { (Supplier) () -> { + try {return new URL("http://example.com/query?param=value with spaces");} catch(Exception e){return null;} + }, new IllegalArgumentException("Unable to convert URL to URI")}, + }); TEST_DB.put(pair(String.class, URI.class), new Object[][]{ {"", null}, {"https://domain.com", toURI("https://domain.com"), true}, @@ -326,15 +369,8 @@ private static void loadUriTests() { }); TEST_DB.put(pair(Map.class, URI.class), new Object[][]{ { mapOf(URI_KEY, "https://domain.com"), toURI("https://domain.com"), true}, - { mapOf(URI_KEY, "bad uri"), new IllegalArgumentException("Cannot convert Map to URI. Malformed URI: 'bad uri'")}, - }); - TEST_DB.put(pair(URL.class, URI.class), new Object[][]{ - { (Supplier) () -> { - try {return new URL("https://domain.com");} catch(Exception e){return null;} - }, toURI("https://domain.com"), true}, - { (Supplier) () -> { - try {return new URL("http://example.com/query?param=value with spaces");} catch(Exception e){return null;} - }, new IllegalArgumentException("Unable to convert URL to URI")}, + { mapOf(URI_KEY, "bad uri"), new IllegalArgumentException("Illegal character in path at index 3: bad uri")}, + { mapOf(MapConversions.VALUE, "https://domain.com"), toURI("https://domain.com")}, }); } @@ -348,6 +384,10 @@ private static void loadTimeZoneTests() { TEST_DB.put(pair(TimeZone.class, TimeZone.class), new Object[][]{ {TimeZone.getTimeZone("GMT"), TimeZone.getTimeZone("GMT")}, }); + TEST_DB.put(pair(ZoneOffset.class, TimeZone.class), new Object[][]{ + {ZoneOffset.of("Z"), new IllegalArgumentException("Unsupported conversion, source type [ZoneOffset (Z)] target type 'TimeZone'")}, + {ZoneOffset.of("+09:00"), new IllegalArgumentException("Unsupported conversion, source type [ZoneOffset (+09:00)] target type 'TimeZone'")}, + }); TEST_DB.put(pair(String.class, TimeZone.class), new Object[][]{ {"", null}, {"America/New_York", TimeZone.getTimeZone("America/New_York"), true}, @@ -383,11 +423,17 @@ private static void loadOffsetTimeTests() { TEST_DB.put(pair(Map.class, OffsetTime.class), new Object[][]{ {mapOf(TIME, "00:00+09:00"), OffsetTime.parse("00:00+09:00"), true}, {mapOf(TIME, "00:00+09:01:23"), OffsetTime.parse("00:00+09:01:23"), true}, - {mapOf(TIME, "00:00+09:01:23.1"), new IllegalArgumentException("Unable to parse OffsetTime")}, + {mapOf(TIME, "00:00+09:01:23.1"), new IllegalArgumentException("Unable to parse '00:00+09:01:23.1' as an OffsetTime")}, {mapOf(TIME, "00:00-09:00"), OffsetTime.parse("00:00-09:00"), true}, {mapOf(TIME, "00:00:00+09:00"), OffsetTime.parse("00:00+09:00")}, // no reverse {mapOf(TIME, "00:00:00+09:00:00"), OffsetTime.parse("00:00+09:00")}, // no reverse - {mapOf(TIME, "garbage"), new IllegalArgumentException("Unable to parse OffsetTime: garbage")}, // no reverse + {mapOf(TIME, "garbage"), new IllegalArgumentException("Unable to parse 'garbage' as an OffsetTime")}, // no reverse + {mapOf(HOUR, 1, MINUTE,30), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789), new IllegalArgumentException("Map to 'OffsetTime' the map must include: [time], [hour, minute, second (optional), nano (optional), offsetHour, offsetMinute], [value], or [_v] as keys with associated values")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, -30), OffsetTime.parse("01:30:59.123456789-05:30")}, + {mapOf(HOUR, 1, MINUTE,30, SECOND, 59, NANO, 123456789, OFFSET_HOUR, -5, OFFSET_MINUTE, 30), new IllegalArgumentException("Offset 'hour' and 'minute' are not correct")}, + {mapOf(VALUE, "16:20:00-05:00"), OffsetTime.parse("16:20:00-05:00") }, }); TEST_DB.put(pair(OffsetDateTime.class, OffsetTime.class), new Object[][]{ {odt("1969-12-31T23:59:59.999999999Z"), OffsetTime.parse("08:59:59.999999999+09:00")}, @@ -441,6 +487,10 @@ private static void loadClassTests() { {"java.util.Date", Date.class, true}, {"NoWayJose", new IllegalArgumentException("not found")}, }); + TEST_DB.put(pair(Map.class, Class.class), new Object[][]{ + { mapOf(VALUE, Long.class), Long.class, true}, + { mapOf(VALUE, "not a class"), new IllegalArgumentException("Cannot convert String 'not a class' to class. Class not found")}, + }); } /** @@ -453,78 +503,6 @@ private static void loadMapTests() { TEST_DB.put(pair(Map.class, Map.class), new Object[][]{ { new HashMap<>(), new IllegalArgumentException("Unsupported conversion") } }); - TEST_DB.put(pair(Byte.class, Map.class), new Object[][]{ - {(byte)1, mapOf(VALUE, (byte)1)}, - {(byte)2, mapOf(VALUE, (byte)2)} - }); - TEST_DB.put(pair(Integer.class, Map.class), new Object[][]{ - {-1, mapOf(VALUE, -1)}, - {0, mapOf(VALUE, 0)}, - {1, mapOf(VALUE, 1)} - }); - TEST_DB.put(pair(Float.class, Map.class), new Object[][]{ - {1.0f, mapOf(VALUE, 1.0f)}, - {2.0f, mapOf(VALUE, 2.0f)} - }); - TEST_DB.put(pair(Double.class, Map.class), new Object[][]{ - {1.0, mapOf(VALUE, 1.0)}, - {2.0, mapOf(VALUE, 2.0)} - }); - TEST_DB.put(pair(Calendar.class, Map.class), new Object[][]{ - {(Supplier) () -> { - Calendar cal = Calendar.getInstance(TOKYO_TZ); - cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17); - cal.set(Calendar.MILLISECOND, 409); - return cal; - }, (Supplier>) () -> { - Map map = new CompactLinkedMap<>(); - map.put(DATE, "2024-02-05"); - map.put(TIME, "22:31:17.409"); - map.put(ZONE, TOKYO); - map.put(EPOCH_MILLIS, 1707139877409L); - return map; - }, true}, - }); - TEST_DB.put(pair(Timestamp.class, Map.class), new Object[][] { - { timestamp("1969-12-31T23:59:59.999999999Z"), mapOf(EPOCH_MILLIS, -1L, NANOS, 999999999, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), true}, - { new Timestamp(-1L), mapOf(EPOCH_MILLIS, -1L, NANOS, 999000000, DATE, "1970-01-01", TIME, "08:59:59.999", ZONE, TOKYO_Z.toString()), true}, - { timestamp("1970-01-01T00:00:00Z"), mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), true}, - { new Timestamp(0L), mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), true}, - { timestamp("1970-01-01T00:00:00.000000001Z"), mapOf(EPOCH_MILLIS, 0L, NANOS, 1, DATE, "1970-01-01", TIME, "09:00:00.000000001", ZONE, TOKYO_Z.toString()), true}, - { new Timestamp(1L), mapOf(EPOCH_MILLIS, 1L, NANOS, 1000000, DATE, "1970-01-01", TIME, "09:00:00.001", ZONE, TOKYO_Z.toString()), true}, - { new Timestamp(1710714535152L), mapOf(EPOCH_MILLIS, 1710714535152L, NANOS, 152000000, DATE, "2024-03-18", TIME, "07:28:55.152", ZONE, TOKYO_Z.toString()), true}, - }); - TEST_DB.put(pair(LocalDateTime.class, Map.class), new Object[][] { - { ldt("1969-12-31T23:59:59.999999999"), mapOf(DATE, "1969-12-31", TIME, "23:59:59.999999999"), true}, - { ldt("1970-01-01T00:00"), mapOf(DATE, "1970-01-01", TIME, "00:00"), true}, - { ldt("1970-01-01T00:00:00.000000001"), mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001"), true}, - { ldt("2024-03-10T11:07:00.123456789"), mapOf(DATE, "2024-03-10", TIME, "11:07:00.123456789"), true}, - }); - TEST_DB.put(pair(OffsetDateTime.class, Map.class), new Object[][] { - { OffsetDateTime.parse("1969-12-31T23:59:59.999999999+09:00"), mapOf(DATE, "1969-12-31", TIME, "23:59:59.999999999", "offset", "+09:00"), true}, - { OffsetDateTime.parse("1970-01-01T00:00+09:00"), mapOf(DATE, "1970-01-01", TIME, "00:00", "offset", "+09:00"), true}, - { OffsetDateTime.parse("1970-01-01T00:00:00.000000001+09:00"), mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001", "offset", "+09:00"), true}, - { OffsetDateTime.parse("2024-03-10T11:07:00.123456789+09:00"), mapOf(DATE, "2024-03-10", TIME, "11:07:00.123456789", "offset", "+09:00"), true}, - }); - TEST_DB.put(pair(Duration.class, Map.class), new Object[][] { - { Duration.ofMillis(-1), mapOf("seconds", -1L, "nanos", 999000000), true}, - }); - TEST_DB.put(pair(Instant.class, Map.class), new Object[][] { - { Instant.parse("2024-03-10T11:07:00.123456789Z"), mapOf("seconds", 1710068820L, "nanos", 123456789), true}, - }); - TEST_DB.put(pair(Character.class, Map.class), new Object[][]{ - {(char) 0, mapOf(VALUE, (char)0)}, - {(char) 1, mapOf(VALUE, (char)1)}, - {(char) 65535, mapOf(VALUE, (char)65535)}, - {(char) 48, mapOf(VALUE, '0')}, - {(char) 49, mapOf(VALUE, '1')}, - }); - TEST_DB.put(pair(Class.class, Map.class), new Object[][]{ - { Long.class, mapOf(VALUE, Long.class), true} - }); - TEST_DB.put(pair(Enum.class, Map.class), new Object[][]{ - { DayOfWeek.FRIDAY, mapOf("name", DayOfWeek.FRIDAY.name())} - }); } /** @@ -763,17 +741,6 @@ private static void loadStringTests() { TEST_DB.put(pair(Void.class, String.class), new Object[][]{ {null, null} }); - TEST_DB.put(pair(Double.class, String.class), new Object[][]{ - {0.0, "0"}, - {0.0, "0"}, - {Double.MIN_VALUE, "4.9E-324"}, - {-Double.MAX_VALUE, "-1.7976931348623157E308"}, - {Double.MAX_VALUE, "1.7976931348623157E308"}, - {123456789.0, "1.23456789E8"}, - {0.000000123456789, "1.23456789E-7"}, - {12345.0, "12345.0"}, - {0.00012345, "1.2345E-4"}, - }); TEST_DB.put(pair(BigInteger.class, String.class), new Object[][]{ {new BigInteger("-1"), "-1"}, {BigInteger.ZERO, "0"}, @@ -843,6 +810,9 @@ private static void loadZoneOffsetTests() { {ZoneOffset.of("-05:00"), ZoneOffset.of("-05:00")}, {ZoneOffset.of("+5"), ZoneOffset.of("+05:00")}, }); + TEST_DB.put(pair(ZoneId.class, ZoneOffset.class), new Object[][]{ + {ZoneId.of("Asia/Tokyo"), new IllegalArgumentException("Unsupported conversion, source type [ZoneRegion (Asia/Tokyo)] target type 'ZoneOffset'")}, + }); TEST_DB.put(pair(String.class, ZoneOffset.class), new Object[][]{ {"", null}, {"-00:00", ZoneOffset.of("+00:00")}, @@ -858,7 +828,7 @@ private static void loadZoneOffsetTests() { {mapOf("_v", "-10"), ZoneOffset.of("-10:00")}, {mapOf(HOURS, -10L), ZoneOffset.of("-10:00")}, {mapOf(HOURS, -10, MINUTES, 0), ZoneOffset.of("-10:00"), true}, - {mapOf("hrs", -10L, "mins", "0"), new IllegalArgumentException("Map to ZoneOffset the map must include one of the following: [hours, minutes, seconds], [_v], or [value]")}, + {mapOf("hrs", -10L, "mins", "0"), new IllegalArgumentException("Map to 'ZoneOffset' the map must include: [hours, minutes (optional), seconds (optional)], [value], or [_v] as keys with associated values")}, {mapOf(HOURS, -10L, MINUTES, "0", SECONDS, 0), ZoneOffset.of("-10:00")}, {mapOf(HOURS, "-10", MINUTES, (byte) -15, SECONDS, "-1"), ZoneOffset.of("-10:15:01")}, {mapOf(HOURS, "10", MINUTES, (byte) 15, SECONDS, true), ZoneOffset.of("+10:15:01")}, @@ -930,7 +900,7 @@ private static void loadZoneDateTimeTests() { TEST_DB.put(pair(Map.class, ZonedDateTime.class), new Object[][]{ {mapOf(VALUE, new AtomicLong(now)), Instant.ofEpochMilli(now).atZone(TOKYO_Z)}, {mapOf(EPOCH_MILLIS, now), Instant.ofEpochMilli(now).atZone(TOKYO_Z)}, - {mapOf(DATE_TIME, "1970-01-01T00:00:00", ZONE, TOKYO), zdt("1970-01-01T00:00:00+09:00")}, + {mapOf(TIME, "1970-01-01T00:00:00", ZONE, TOKYO), zdt("1970-01-01T00:00:00+09:00")}, {mapOf(DATE, "1969-12-31", TIME, "23:59:59.999999999", ZONE, TOKYO), zdt("1969-12-31T23:59:59.999999999+09:00"), true}, {mapOf(DATE, "1970-01-01", TIME, "00:00", ZONE, TOKYO), zdt("1970-01-01T00:00:00+09:00"), true}, {mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001", ZONE, TOKYO), zdt("1970-01-01T00:00:00.000000001+09:00"), true}, @@ -986,6 +956,14 @@ private static void loadLocalDateTimeTests() { {"", null}, {"1965-12-31T16:20:00", ldt("1965-12-31T16:20:00"), true}, }); + TEST_DB.put(pair(Map.class, LocalDateTime.class), new Object[][] { + { mapOf(DATE, "1969-12-31", TIME, "23:59:59.999999999"), ldt("1969-12-31T23:59:59.999999999"), true}, + { mapOf(DATE, "1970-01-01", TIME, "00:00"), ldt("1970-01-01T00:00"), true}, + { mapOf(DATE, "1970-01-01"), ldt("1970-01-01T00:00")}, + { mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001"), ldt("1970-01-01T00:00:00.000000001"), true}, + { mapOf(DATE, "2024-03-10", TIME, "11:07:00.123456789"), ldt("2024-03-10T11:07:00.123456789"), true}, + { mapOf(VALUE, "2024-03-10T11:07:00.123456789"), ldt("2024-03-10T11:07:00.123456789")}, + }); } /** @@ -1116,6 +1094,9 @@ private static void loadLocalTimeTests() { {mapOf(TIME, "00:00"), LocalTime.parse("00:00:00"), true}, {mapOf(TIME, "23:59:59.999999999"), LocalTime.parse("23:59:59.999999999"), true}, {mapOf(VALUE, "23:59:59.999999999"), LocalTime.parse("23:59:59.999999999") }, + {mapOf(HOUR, 23, MINUTE, 59), LocalTime.parse("23:59") }, + {mapOf(HOUR, 23, MINUTE, 59, SECOND, 59), LocalTime.parse("23:59:59") }, + {mapOf(HOUR, 23, MINUTE, 59, SECOND, 59, NANO, 999999999), LocalTime.parse("23:59:59.999999999") }, }); } @@ -1198,6 +1179,7 @@ private static void loadLocalDateTests() { {mapOf(DATE, "1970-01-01"), LocalDate.parse("1970-01-01"), true}, {mapOf(DATE, "1970-01-02"), LocalDate.parse("1970-01-02"), true}, {mapOf(VALUE, "2024-03-18"), LocalDate.parse("2024-03-18")}, + {mapOf(YEAR, "2024", MONTH, 3, DAY, 18), LocalDate.parse("2024-03-18")}, }); } @@ -1300,11 +1282,21 @@ private static void loadTimestampTests() { {Instant.parse("2024-03-10T11:36:00.123456789Z"), timestamp("2024-03-10T11:36:00.123456789Z"), true}, }); // No symmetry checks - because an OffsetDateTime of "2024-02-18T06:31:55.987654321+00:00" and "2024-02-18T15:31:55.987654321+09:00" are equivalent but not equals. They both describe the same Instant. - TEST_DB.put(pair(OffsetDateTime.class, Timestamp.class), new Object[][]{ - {OffsetDateTime.parse("1969-12-31T23:59:59.999999999Z"), timestamp("1969-12-31T23:59:59.999999999Z")}, - {OffsetDateTime.parse("1970-01-01T00:00:00.000000000Z"), timestamp("1970-01-01T00:00:00.000000000Z")}, - {OffsetDateTime.parse("1970-01-01T00:00:00.000000001Z"), timestamp("1970-01-01T00:00:00.000000001Z")}, - {OffsetDateTime.parse("2024-02-18T06:31:55.987654321Z"), timestamp("2024-02-18T06:31:55.987654321Z")}, + TEST_DB.put(pair(Map.class, Timestamp.class), new Object[][] { + { mapOf(EPOCH_MILLIS, -1L, NANOS, 999999999, DATE, "1970-01-01", TIME, "08:59:59.999999999", ZONE, TOKYO_Z.toString()), timestamp("1969-12-31T23:59:59.999999999Z"), true}, // redundant DATE, TIME, and ZONE fields for reverse test + { mapOf(EPOCH_MILLIS, -1L, NANOS, 999000000, DATE, "1970-01-01", TIME, "08:59:59.999", ZONE, TOKYO_Z.toString()), new Timestamp(-1L), true}, + { mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), timestamp("1970-01-01T00:00:00Z"), true}, + { mapOf(EPOCH_MILLIS, 0L, NANOS, 0, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), new Timestamp(0L), true}, + { mapOf(EPOCH_MILLIS, 0L, NANOS, 1, DATE, "1970-01-01", TIME, "09:00:00.000000001", ZONE, TOKYO_Z.toString()), timestamp("1970-01-01T00:00:00.000000001Z"), true}, + { mapOf(EPOCH_MILLIS, 1L, NANOS, 1000000, DATE, "1970-01-01", TIME, "09:00:00.001", ZONE, TOKYO_Z.toString()), new Timestamp(1L), true}, + { mapOf(EPOCH_MILLIS, 1710714535152L, NANOS, 152000000, DATE, "2024-03-18", TIME, "07:28:55.152", ZONE, TOKYO_Z.toString()), new Timestamp(1710714535152L), true}, + { mapOf(TIME, "2024-03-18T07:28:55.152", ZONE, TOKYO_Z.toString()), new Timestamp(1710714535152L)}, + { mapOf(TIME, "2024-03-18T07:28:55.152000001", ZONE, TOKYO_Z.toString()), (Supplier) () -> { + Timestamp ts = new Timestamp(1710714535152L); + MapConversions.setNanosPreserveMillis(ts, 1); + return ts; + }}, + { mapOf("bad key", "2024-03-18T07:28:55.152", ZONE, TOKYO_Z.toString()), new IllegalArgumentException("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -1322,6 +1314,16 @@ private static void loadZoneIdTests() { {NY_Z, NY_Z}, {TOKYO_Z, TOKYO_Z}, }); + TEST_DB.put(pair(ZoneOffset.class, ZoneId.class), new Object[][]{ + {ZoneOffset.of("+09:00"), ZoneId.of("+09:00")}, + {ZoneOffset.of("-05:00"), ZoneId.of("-05:00")}, + }); + TEST_DB.put(pair(TimeZone.class, ZoneId.class), new Object[][]{ + {TimeZone.getTimeZone("America/New_York"), ZoneId.of("America/New_York"),true}, + {TimeZone.getTimeZone("Asia/Tokyo"), ZoneId.of("Asia/Tokyo"),true}, + {TimeZone.getTimeZone("GMT"), ZoneId.of("GMT"), true}, + {TimeZone.getTimeZone("UTC"), ZoneId.of("UTC"), true}, + }); TEST_DB.put(pair(String.class, ZoneId.class), new Object[][]{ {"", null}, {"America/New_York", NY_Z, true}, @@ -1331,17 +1333,12 @@ private static void loadZoneIdTests() { {"UTC", ZoneId.of("UTC"), true}, {"GMT", ZoneId.of("GMT"), true}, }); - TEST_DB.put(pair(TimeZone.class, ZoneId.class), new Object[][]{ - {TimeZone.getTimeZone("America/New_York"), ZoneId.of("America/New_York"),true}, - {TimeZone.getTimeZone("Asia/Tokyo"), ZoneId.of("Asia/Tokyo"),true}, - {TimeZone.getTimeZone("GMT"), ZoneId.of("GMT"), true}, - {TimeZone.getTimeZone("UTC"), ZoneId.of("UTC"), true}, - }); TEST_DB.put(pair(Map.class, ZoneId.class), new Object[][]{ {mapOf("_v", "America/New_York"), NY_Z}, {mapOf("_v", NY_Z), NY_Z}, - {mapOf("zone", "America/New_York"), NY_Z, true}, - {mapOf("zone", NY_Z), NY_Z}, + {mapOf(ZONE, "America/New_York"), NY_Z, true}, + {mapOf(ZONE, NY_Z), NY_Z}, + {mapOf(ID, NY_Z), NY_Z}, {mapOf("_v", "Asia/Tokyo"), TOKYO_Z}, {mapOf("_v", TOKYO_Z), TOKYO_Z}, {mapOf("zone", mapOf("_v", TOKYO_Z)), TOKYO_Z}, @@ -1550,8 +1547,15 @@ private static void loadOffsetDateTimeTests() { }); TEST_DB.put(pair(Timestamp.class, OffsetDateTime.class), new Object[][]{ {new Timestamp(-1), odt("1969-12-31T23:59:59.999+00:00"), true}, + {new Timestamp(-1), odt("1969-12-31T23:59:59.999-00:00"), true}, {new Timestamp(0), odt("1970-01-01T00:00:00+00:00"), true}, + {new Timestamp(0), odt("1970-01-01T00:00:00-00:00"), true}, {new Timestamp(1), odt("1970-01-01T00:00:00.001+00:00"), true}, + {new Timestamp(1), odt("1970-01-01T00:00:00.001-00:00"), true}, + {timestamp("1969-12-31T23:59:59.999999999Z"), OffsetDateTime.parse("1970-01-01T08:59:59.999999999+09:00"), true}, + {timestamp("1970-01-01T00:00:00Z"), OffsetDateTime.parse("1970-01-01T09:00:00+09:00"), true}, + {timestamp("1970-01-01T00:00:00.000000001Z"), OffsetDateTime.parse("1970-01-01T09:00:00.000000001+09:00"), true}, + {timestamp("2024-02-18T06:31:55.987654321Z"), OffsetDateTime.parse("2024-02-18T15:31:55.987654321+09:00"), true}, }); TEST_DB.put(pair(LocalDateTime.class, OffsetDateTime.class), new Object[][]{ {ldt("1970-01-01T08:59:59.999999999"), odt("1969-12-31T23:59:59.999999999Z"), true}, @@ -1572,6 +1576,15 @@ private static void loadOffsetDateTimeTests() { {"", null}, {"2024-02-10T10:15:07+01:00", OffsetDateTime.parse("2024-02-10T10:15:07+01:00"), true}, }); + TEST_DB.put(pair(Map.class, OffsetDateTime.class), new Object[][] { + { mapOf(DATE, "1969-12-31", TIME, "23:59:59.999999999", OFFSET, "+09:00"), OffsetDateTime.parse("1969-12-31T23:59:59.999999999+09:00"), true}, + { mapOf(DATE, "1970-01-01", TIME, "00:00", OFFSET, "+09:00"), OffsetDateTime.parse("1970-01-01T00:00+09:00"), true}, + { mapOf(DATE, "1970-01-01", TIME, "00:00:00.000000001", OFFSET, "+09:00"), OffsetDateTime.parse("1970-01-01T00:00:00.000000001+09:00"), true}, + { mapOf(DATE, "2024-03-10", TIME, "11:07:00.123456789", OFFSET, "+09:00"), OffsetDateTime.parse("2024-03-10T11:07:00.123456789+09:00"), true}, + { mapOf(DATE, "2024-03-10", TIME, "11:07:00.123456789"), new IllegalArgumentException("Map to 'OffsetDateTime' the map must include: [time, offset], [date, time, offset], [value], or [_v] as keys with associated values")}, + { mapOf(TIME, "2024-03-10T11:07:00.123456789", OFFSET, "+09:00"), OffsetDateTime.parse("2024-03-10T11:07:00.123456789+09:00")}, + { mapOf(VALUE, "2024-03-10T11:07:00.123456789+09:00"), OffsetDateTime.parse("2024-03-10T11:07:00.123456789+09:00")}, + }); } /** @@ -1608,6 +1621,12 @@ private static void loadDurationTests() { {BigInteger.valueOf(Long.MAX_VALUE), Duration.ofNanos(Long.MAX_VALUE), true}, {BigInteger.valueOf(Long.MIN_VALUE), Duration.ofNanos(Long.MIN_VALUE), true}, }); + TEST_DB.put(pair(Map.class, Duration.class), new Object[][] { + { mapOf(SECONDS, -1L, NANOS, 999000000), Duration.ofMillis(-1), true}, + { mapOf(SECONDS, 0L, NANOS, 0), Duration.ofMillis(0), true}, + { mapOf(SECONDS, 0L, NANOS, 1000000), Duration.ofMillis(1), true}, + { mapOf(VALUE, 16000L), Duration.ofSeconds(16)}, // VALUE is in milliseconds + }); } /** @@ -1732,7 +1751,7 @@ private static void loadSqlDateTests() { { mapOf(TIME, "1970-01-01T00:00", ZONE, "Z"), new java.sql.Date(0L)}, { mapOf(TIME, "X1970-01-01T00:00", ZONE, "Z"), new IllegalArgumentException("Issue parsing date-time, other characters present: X")}, { mapOf(TIME, "1970-01-01T00:00", ZONE, "bad zone"), new IllegalArgumentException("Unknown time-zone ID: 'bad zone'")}, - { mapOf("foo", "bar"), new IllegalArgumentException("Map to java.sql.Date the map must include one of the following: [epochMillis], [_v], or [value] with associated values")}, + { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'java.sql.Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -1829,7 +1848,7 @@ private static void loadDateTests() { { mapOf(TIME, "1970-01-01T00:00", ZONE, "Z"), new Date(0L)}, { mapOf(TIME, "X1970-01-01T00:00", ZONE, "Z"), new IllegalArgumentException("Issue parsing date-time, other characters present: X")}, { mapOf(TIME, "1970-01-01T00:00", ZONE, "bad zone"), new IllegalArgumentException("Unknown time-zone ID: 'bad zone'")}, - { mapOf("foo", "bar"), new IllegalArgumentException("Map to Date the map must include one of the following: [epochMillis], [_v], or [value] with associated values")}, + { mapOf("foo", "bar"), new IllegalArgumentException("Map to 'Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values")}, }); } @@ -1868,21 +1887,13 @@ private static void loadCalendarTests() { {new BigDecimal(1), cal(1000), true}, }); TEST_DB.put(pair(Map.class, Calendar.class), new Object[][]{ - {(Supplier>) () -> { - Map map = new CompactLinkedMap<>(); - map.put(VALUE, "2024-02-05T22:31:17.409[" + TOKYO + "]"); - return map; - }, (Supplier) () -> { + {mapOf(VALUE, "2024-02-05T22:31:17.409[" + TOKYO + "]"), (Supplier) () -> { Calendar cal = Calendar.getInstance(TOKYO_TZ); cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17); cal.set(Calendar.MILLISECOND, 409); return cal; }}, - {(Supplier>) () -> { - Map map = new CompactLinkedMap<>(); - map.put(VALUE, "2024-02-05T22:31:17.409" + TOKYO_ZO.toString()); - return map; - }, (Supplier) () -> { + {mapOf(VALUE, "2024-02-05T22:31:17.409" + TOKYO_ZO.toString()), (Supplier) () -> { Calendar cal = Calendar.getInstance(TOKYO_TZ); cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17); cal.set(Calendar.MILLISECOND, 409); @@ -1901,6 +1912,43 @@ private static void loadCalendarTests() { cal.set(Calendar.MILLISECOND, 409); return cal; }}, + {mapOf(DATE, "1970-01-01", TIME, "00:00:00"), (Supplier) () -> { + Calendar cal = Calendar.getInstance(TOKYO_TZ); + cal.set(1970, Calendar.JANUARY, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal; + }}, + {mapOf(DATE, "1970-01-01", TIME, "00:00:00", ZONE, "America/New_York"), (Supplier) () -> { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(ZoneId.of("America/New_York"))); + cal.set(1970, Calendar.JANUARY, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal; + }}, + {mapOf(TIME, "1970-01-01T00:00:00"), (Supplier) () -> { + Calendar cal = Calendar.getInstance(TOKYO_TZ); + cal.set(1970, Calendar.JANUARY, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal; + }}, + {mapOf(TIME, "1970-01-01T00:00:00", ZONE, "America/New_York"), (Supplier) () -> { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(ZoneId.of("America/New_York"))); + cal.set(1970, Calendar.JANUARY, 1, 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal; + }}, + {(Supplier>) () -> { + Map map = new CompactLinkedMap<>(); + map.put(DATE, "2024-02-05"); + map.put(TIME, "22:31:17.409"); + map.put(ZONE, TOKYO); + map.put(EPOCH_MILLIS, 1707139877409L); + return map; + }, (Supplier) () -> { + Calendar cal = Calendar.getInstance(TOKYO_TZ); + cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17); + cal.set(Calendar.MILLISECOND, 409); + return cal; + }, true}, }); TEST_DB.put(pair(ZonedDateTime.class, Calendar.class), new Object[][] { {zdt("1969-12-31T23:59:59.999Z"), cal(-1), true}, @@ -1983,6 +2031,15 @@ private static void loadInstantTests() { {odt("1980-01-01T00:00:00Z"), Instant.parse("1980-01-01T00:00:00Z"), true}, {odt("2024-12-31T23:59:59.999999999Z"), Instant.parse("2024-12-31T23:59:59.999999999Z"), true}, }); + TEST_DB.put(pair(Map.class, Instant.class), new Object[][] { + { mapOf(SECONDS, 1710068820L, NANOS, 123456789), Instant.parse("2024-03-10T11:07:00.123456789Z"), true}, + { mapOf(SECONDS, -1L, NANOS, 999999999), Instant.parse("1969-12-31T23:59:59.999999999Z"), true}, + { mapOf(SECONDS, 0L, NANOS, 0), Instant.parse("1970-01-01T00:00:00Z"), true}, + { mapOf(SECONDS, 0L, NANOS, 1), Instant.parse("1970-01-01T00:00:00.000000001Z"), true}, + { mapOf(VALUE, -1L), Instant.parse("1969-12-31T23:59:59.999Z")}, + { mapOf(VALUE, 0L), Instant.parse("1970-01-01T00:00:00Z")}, + { mapOf(VALUE, 1L), Instant.parse("1970-01-01T00:00:00.001Z")}, + }); } /** @@ -2319,6 +2376,11 @@ private static void loadCharacterTests() { {mapOf("_v", mapOf("_v", 65535)), (char) 65535}, {mapOf("_v", "0"), (char) 48}, {mapOf("_v", 65536), new IllegalArgumentException("Value '65536' out of range to be converted to character")}, + {mapOf(VALUE, (char)0), (char) 0, true}, + {mapOf(VALUE, (char)1), (char) 1, true}, + {mapOf(VALUE, (char)65535), (char) 65535, true}, + {mapOf(VALUE, '0'), (char) 48, true}, + {mapOf(VALUE, '1'), (char) 49, true}, }); TEST_DB.put(pair(String.class, Character.class), new Object[][]{ {"", (char) 0}, @@ -2586,15 +2648,15 @@ private static void loadDoubleTests() { }); TEST_DB.put(pair(Map.class, Double.class), new Object[][]{ {mapOf("_v", "-1"), -1.0}, - {mapOf("_v", -1), -1.0}, + {mapOf("_v", -1.0), -1.0, true}, {mapOf("value", "-1"), -1.0}, {mapOf("value", -1L), -1.0}, {mapOf("_v", "0"), 0.0}, - {mapOf("_v", 0), 0.0}, + {mapOf("_v", 0.0), 0.0, true}, {mapOf("_v", "1"), 1.0}, - {mapOf("_v", 1), 1.0}, + {mapOf("_v", 1.0), 1.0, true}, {mapOf("_v", "-9007199254740991"), -9007199254740991.0}, {mapOf("_v", -9007199254740991L), -9007199254740991.0}, @@ -2605,13 +2667,13 @@ private static void loadDoubleTests() { {mapOf("_v", mapOf("_v", -9007199254740991L)), -9007199254740991.0}, // Prove use of recursive call to .convert() }); TEST_DB.put(pair(String.class, Double.class), new Object[][]{ - {"-1", -1.0}, + {"-1.0", -1.0, true}, {"-1.1", -1.1}, {"-1.9", -1.9}, - {"0", 0.0}, - {"1", 1.0}, - {"1.1", 1.1}, - {"1.9", 1.9}, + {"0", 0.0, true}, + {"1.0", 1.0, true}, + {"1.1", 1.1, true}, + {"1.9", 1.9, true}, {"-2147483648", -2147483648.0}, {"2147483647", 2147483647.0}, {"", 0.0}, @@ -2621,6 +2683,14 @@ private static void loadDoubleTests() { {"54crapola", new IllegalArgumentException("Value '54crapola' not parseable as a double")}, {"crapola 54", new IllegalArgumentException("Value 'crapola 54' not parseable as a double")}, {"crapola54", new IllegalArgumentException("Value 'crapola54' not parseable as a double")}, + {"4.9E-324", Double.MIN_VALUE, true}, + {"-1.7976931348623157E308", -Double.MAX_VALUE, true}, + {"1.7976931348623157E308", Double.MAX_VALUE}, + {"1.23456789E8", 123456789.0, true}, + {"1.23456789E-7", 0.000000123456789, true}, + {"12345.0", 12345.0, true}, + {"1.2345E-4", 0.00012345, true}, + }); TEST_DB.put(pair(Year.class, Double.class), new Object[][]{ {Year.of(2024), 2024.0} @@ -2694,15 +2764,15 @@ private static void loadFloatTests() { }); TEST_DB.put(pair(Map.class, Float.class), new Object[][]{ {mapOf("_v", "-1"), -1f}, - {mapOf("_v", -1), -1f}, + {mapOf("_v", -1f), -1f, true}, {mapOf("value", "-1"), -1f}, - {mapOf("value", -1L), -1f}, + {mapOf("value", -1f), -1f}, {mapOf("_v", "0"), 0f}, - {mapOf("_v", 0), 0f}, + {mapOf("_v", 0f), 0f, true}, {mapOf("_v", "1"), 1f}, - {mapOf("_v", 1), 1f}, + {mapOf("_v", 1f), 1f, true}, {mapOf("_v", "-16777216"), -16777216f}, {mapOf("_v", -16777216), -16777216f}, @@ -3043,15 +3113,15 @@ private static void loadIntegerTests() { }); TEST_DB.put(pair(Map.class, Integer.class), new Object[][]{ {mapOf("_v", "-1"), -1}, - {mapOf("_v", -1), -1}, + {mapOf("_v", -1), -1, true}, {mapOf("value", "-1"), -1}, {mapOf("value", -1L), -1}, {mapOf("_v", "0"), 0}, - {mapOf("_v", 0), 0}, + {mapOf("_v", 0), 0, true}, {mapOf("_v", "1"), 1}, - {mapOf("_v", 1), 1}, + {mapOf("_v", 1), 1, true}, {mapOf("_v", "-2147483648"), Integer.MIN_VALUE}, {mapOf("_v", -2147483648), Integer.MIN_VALUE}, @@ -3424,6 +3494,11 @@ private static void loadByteTest() { {"-129", new IllegalArgumentException("'-129' not parseable as a byte value or outside -128 to 127")}, {"128", new IllegalArgumentException("'128' not parseable as a byte value or outside -128 to 127")}, }); + TEST_DB.put(pair(Map.class, Byte.class), new Object[][]{ + {mapOf(VALUE, (byte)1), (byte)1, true}, + {mapOf(VALUE, (byte)2), (byte)2, true}, + {mapOf(VALUE, "nope"), new IllegalArgumentException("Value 'nope' not parseable as a byte value or outside -128 to 127")}, + }); } /** @@ -3698,16 +3773,16 @@ void testConvert(String shortNameSource, String shortNameTarget, Object source, Object actual = converter.convert(source, targetClass); try { if (target instanceof CharSequence) { - assertEquals(actual.toString(), target.toString()); + assertEquals(target.toString(), actual.toString()); updateStat(pair(sourceClass, targetClass), true); } else if (targetClass.equals(byte[].class)) { - assertArrayEquals((byte[]) actual, (byte[]) target); + assertArrayEquals((byte[]) target, (byte[]) actual); updateStat(pair(sourceClass, targetClass), true); } else if (targetClass.equals(char[].class)) { - assertArrayEquals((char[]) actual, (char[]) target); + assertArrayEquals((char[]) target, (char[]) actual); updateStat(pair(sourceClass, targetClass), true); } else if (targetClass.equals(Character[].class)) { - assertArrayEquals((Character[]) actual, (Character[]) target); + assertArrayEquals((Character[]) target, (Character[]) actual); updateStat(pair(sourceClass, targetClass), true); } else if (target instanceof AtomicBoolean) { assertEquals(((AtomicBoolean) target).get(), ((AtomicBoolean) actual).get()); @@ -3724,7 +3799,7 @@ void testConvert(String shortNameSource, String shortNameTarget, Object source, } updateStat(pair(sourceClass, targetClass), true); } else { - assertEquals(actual, target); + assertEquals(target, actual); updateStat(pair(sourceClass, targetClass), true); } } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index c67f9db2..ecb7c3f7 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -2788,7 +2788,7 @@ void testMapToAtomicBoolean() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, AtomicBoolean.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to AtomicBoolean the map must include one of the following"); + .hasMessageContaining("Map to 'AtomicBoolean' the map must include: [value], or [_v] as keys with associated values"); } @Test @@ -2811,7 +2811,7 @@ void testMapToAtomicInteger() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, AtomicInteger.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to AtomicInteger the map must include one of the following"); + .hasMessageContaining("Map to 'AtomicInteger' the map must include: [value], or [_v] as keys with associated values"); } @Test @@ -2834,7 +2834,7 @@ void testMapToAtomicLong() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, AtomicLong.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to AtomicLong the map must include one of the following"); + .hasMessageContaining("Map to 'AtomicLong' the map must include: [value], or [_v] as keys with associated values"); } @ParameterizedTest @@ -2858,7 +2858,7 @@ void testMapToCalendar(Object value) map.clear(); assertThatThrownBy(() -> this.converter.convert(map, Calendar.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to Calendar the map must include one of the following: [date, time, zone]"); + .hasMessageContaining("Map to 'Calendar' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2926,7 +2926,7 @@ void testMapToGregCalendar() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, GregorianCalendar.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Map to Calendar the map must include one of the following: [date, time, zone]"); + .hasMessageContaining("Map to 'Calendar' the map must include: [epochMillis], [time, zone (optional)], [date, time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2949,7 +2949,7 @@ void testMapToDate() { map.clear(); assertThatThrownBy(() -> this.converter.convert(map, Date.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to Date the map must include one of the following"); + .hasMessageContaining("Map to 'Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2972,7 +2972,7 @@ void testMapToSqlDate() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, java.sql.Date.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to java.sql.Date the map must include"); + .hasMessageContaining("Map to 'java.sql.Date' the map must include: [epochMillis], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -2995,7 +2995,7 @@ void testMapToTimestamp() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, Timestamp.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to Timestamp the map must include one of the following"); + .hasMessageContaining("Map to 'Timestamp' the map must include: [epochMillis, nanos (optional)], [date, time, zone (optional)], [time, zone (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -3018,7 +3018,7 @@ void testMapToLocalDate() map.clear(); assertThatThrownBy(() -> this.converter.convert(map, LocalDate.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to LocalDate the map must include one of the following: [date], [_v], or [value] with associated values"); + .hasMessageContaining("Map to 'LocalDate' the map must include: [date], [year, month, day], [value], or [_v] as keys with associated values"); } @Test @@ -3041,7 +3041,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: [date, time], [_v], or [value] with associated values"); + .hasMessageContaining("Map to 'LocalDateTime' the map must include: [date, time (optional)], [value], or [_v] as keys with associated values"); } @Test @@ -3060,7 +3060,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: [date, time, zone], [_v], or [value] with associated values"); + .hasMessageContaining("Map to 'ZonedDateTime' the map must include: [epochMillis], [dateTime, zone], [date, time, zone], [value], or [_v] as keys with associated values"); } @@ -3481,7 +3481,7 @@ void testBadMapToUUID() map.put("leastSigBits", uuid.getLeastSignificantBits()); assertThatThrownBy(() -> this.converter.convert(map, UUID.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("To convert from Map to UUID the map must include one of the following"); + .hasMessageContaining("Map to 'UUID' the map must include: [UUID], [mostSigBits, leastSigBits], [value], or [_v] as keys with associated values"); } @Test