diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index d06c5a14..a3c9e0f9 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -184,12 +184,12 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(Instant.class, Long.class), InstantConversions::toLong); CONVERSION_DB.put(pair(LocalDate.class, Long.class), LocalDateConversions::toLong); CONVERSION_DB.put(pair(LocalDateTime.class, Long.class), LocalDateTimeConversions::toLong); + CONVERSION_DB.put(pair(OffsetDateTime.class, Long.class), OffsetDateTimeConversions::toLong); CONVERSION_DB.put(pair(ZonedDateTime.class, Long.class), ZonedDateTimeConversions::toLong); CONVERSION_DB.put(pair(Calendar.class, Long.class), CalendarConversions::toLong); CONVERSION_DB.put(pair(Number.class, Long.class), NumberConversions::toLong); CONVERSION_DB.put(pair(Map.class, Long.class), MapConversions::toLong); CONVERSION_DB.put(pair(String.class, Long.class), StringConversions::toLong); - CONVERSION_DB.put(pair(OffsetDateTime.class, Long.class), OffsetDateTimeConversions::toLong); CONVERSION_DB.put(pair(Year.class, Long.class), YearConversions::toLong); // toFloat @@ -614,8 +614,10 @@ private static void buildFactoryConversions() { 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); + CONVERSION_DB.put(pair(Double.class, OffsetDateTime.class), DoubleConversions::toOffsetDateTime); CONVERSION_DB.put(pair(BigInteger.class, OffsetDateTime.class), BigIntegerConversions::toOffsetDateTime); CONVERSION_DB.put(pair(BigDecimal.class, OffsetDateTime.class), BigDecimalConversions::toOffsetDateTime); + CONVERSION_DB.put(pair(Timestamp.class, OffsetDateTime.class), TimestampConversions::toOffsetDateTime); CONVERSION_DB.put(pair(ZonedDateTime.class, OffsetDateTime.class), ZonedDateTimeConversions::toOffsetDateTime); // toOffsetTime diff --git a/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java b/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java index 395f1a4e..e86230ac 100644 --- a/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java @@ -3,6 +3,7 @@ import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.time.ZonedDateTime; /** @@ -40,6 +41,10 @@ static ZonedDateTime toZonedDateTime(Object from, Converter converter) { return toInstant(from, converter).atZone(converter.getOptions().getZoneId()); } + static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { + return toInstant(from, converter).atZone(converter.getOptions().getZoneId()).toOffsetDateTime(); + } + static Timestamp toTimestamp(Object from, Converter converter) { double milliseconds = (Double) from; long millisPart = (long) milliseconds; diff --git a/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java b/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java index 7e466e4a..7d64e25c 100644 --- a/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/OffsetDateTimeConversions.java @@ -65,7 +65,8 @@ static AtomicLong toAtomicLong(Object from, Converter converter) { } static Timestamp toTimestamp(Object from, Converter converter) { - return new Timestamp(toLong(from, converter)); + OffsetDateTime odt = (OffsetDateTime) from; + return Timestamp.from(odt.toInstant()); } static Calendar toCalendar(Object from, Converter converter) { @@ -80,7 +81,6 @@ static java.sql.Date toSqlDate(Object from, Converter converter) { static ZonedDateTime toZonedDateTime(Object from, Converter converter) { return ((OffsetDateTime) from).toInstant().atZone(converter.getOptions().getZoneId()); -// return ((OffsetDateTime) from).atZoneSameInstant(converter.getOptions().getZoneId()); } static Date toDate(Object from, Converter converter) { @@ -88,10 +88,12 @@ static Date toDate(Object from, Converter converter) { } static BigInteger toBigInteger(Object from, Converter converter) { + // TODO: nanosecond resolution needed return BigInteger.valueOf(toLong(from, converter)); } static BigDecimal toBigDecimal(Object from, Converter converter) { + // TODO: nanosecond resolution needed return BigDecimal.valueOf(toLong(from, converter)); } @@ -118,6 +120,13 @@ static Map toMap(Object from, Converter converter) { } static double toDouble(Object from, Converter converter) { - throw new UnsupportedOperationException("This needs to be implemented"); + OffsetDateTime odt = (OffsetDateTime) from; + Instant instant = odt.toInstant(); + + long epochSecond = instant.getEpochSecond(); + int nano = instant.getNano(); + + // Convert seconds to milliseconds and add the fractional milliseconds + return epochSecond * 1000.0 + nano / 1_000_000.0; } } diff --git a/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java b/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java index 387f375c..b91372d1 100644 --- a/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/TimestampConversions.java @@ -5,6 +5,9 @@ import java.sql.Timestamp; import java.time.Duration; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; /** * @author John DeRegnaucourt (jdereg@gmail.com) @@ -24,8 +27,6 @@ * limitations under the License. */ final class TimestampConversions { - private static final BigInteger MILLION = BigInteger.valueOf(1_000_000); - private TimestampConversions() {} static double toDouble(Object from, Converter converter) { @@ -59,4 +60,16 @@ static Duration toDuration(Object from, Converter converter) { Instant timestampInstant = timestamp.toInstant(); return Duration.between(epoch, timestampInstant); } + + 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); + } } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 3373b4ba..617f64de 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -1061,6 +1061,9 @@ public ZoneId getZoneId() { {(char) 0, 0d}, }); TEST_DB.put(pair(Instant.class, Double.class), new Object[][]{ // JDK 1.8 cannot handle the format +01:00 in Instant.parse(). JDK11+ handle it fine. +// {ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").toInstant(), -0.000001d, true}, // IEEE-754 double cannot represent this number precisely + {ZonedDateTime.parse("1970-01-01T00:00:00Z").toInstant(), 0d, true}, + {ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").toInstant(), 0.000001d, true}, {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").toInstant(), 1707734280000d, true}, {ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00").toInstant(), 1707734280123d, true}, {ZonedDateTime.parse("2024-02-12T11:38:00.1234+01:00").toInstant(), 1707734280123.4d}, // fractional milliseconds (nano support) - no reverse because of IEEE-754 limitations @@ -1069,22 +1072,44 @@ public ZoneId getZoneId() { {ZonedDateTime.parse("2024-02-12T11:38:00.123937482+01:00").toInstant(), 1707734280123.937482d}, // nano = one-millionth of a milli }); TEST_DB.put(pair(LocalDate.class, Double.class), new Object[][]{ + {ZonedDateTime.parse("1969-12-31T00:00:00Z").withZoneSameInstant(ZoneId.of("UTC")).toLocalDate(), -1.188E8, true}, // Proves it always works from "startOfDay", using the zoneId from options + {ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").withZoneSameInstant(ZoneId.of("UTC")).toLocalDate(), -1.188E8, true}, // Proves it always works from "startOfDay", using the zoneId from options + {ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(ZoneId.of("UTC")).toLocalDate(), -32400000d, true}, // Proves it always works from "startOfDay", using the zoneId from options + {ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), -32400000d, true}, + {ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), -32400000.000000001d, true}, {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").withZoneSameInstant(TOKYO_Z).toLocalDate(), 1.7076636E12, true}, // Epoch millis in Tokyo timezone (at start of day - no time) {ZonedDateTime.parse("2024-02-12T11:38:00.123456789+01:00").withZoneSameInstant(TOKYO_Z).toLocalDate(), 1.7076636E12}, // Only to start of day resolution }); TEST_DB.put(pair(LocalDateTime.class, Double.class), new Object[][]{ - {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1.70773428E12, true}, // Epoch millis in Tokyo timezone - {ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1.707734280123E12, true}, // Epoch millis in Tokyo timezone - {ZonedDateTime.parse("2024-02-12T11:38:00.1239+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1707734280123.9d}, // Epoch millis in Tokyo timezone (no reverse - IEEE-754 limitations) - {ZonedDateTime.parse("2024-02-12T11:38:00.123937482+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1707734280123.937482d}, // Epoch millis in Tokyo timezone (no reverse - IEEE-754 limitations) +// {ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), -0.000001d, true}, // IEEE-754 double does not quite have the resolution + {ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime(), -32400000d, true}, // Time portion affects the answer unlike LocalDate + {ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 0d, true}, + {ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 0.000001d, true}, + {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1.70773428E12, true}, + {ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1.707734280123E12, true}, + {ZonedDateTime.parse("2024-02-12T11:38:00.1239+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1707734280123.9d}, + {ZonedDateTime.parse("2024-02-12T11:38:00.123937482+01:00").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), 1707734280123.937482d}, }); TEST_DB.put(pair(ZonedDateTime.class, Double.class), new Object[][]{ // no reverse due to .toString adding zone name +// {ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z"), -0.000001d}, // IEEE-754 double resolution not quite good enough to represent, but very close. + {ZonedDateTime.parse("1970-01-01T00:00:00Z"), 0d}, + {ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z"), 0.000001d}, {ZonedDateTime.parse("2024-02-12T11:38:00+01:00"), 1707734280000d}, {ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00"), 1707734280123d}, {ZonedDateTime.parse("2024-02-12T11:38:00.1234+01:00"), 1707734280123.4d}, {ZonedDateTime.parse("2024-02-12T11:38:00.123456789+01:00"), 1707734280123.456789d}, {ZonedDateTime.parse("2024-02-12T11:38:00.123937482+01:00"), 1707734280123.937482d}, }); + TEST_DB.put(pair(OffsetDateTime.class, Double.class), new Object[][]{ +// {OffsetDateTime.parse("1969-12-31T23:59:59.999999999Z"), -0.000001d}, // IEEE-754 double resolution not quite good enough to represent, but very close. + {OffsetDateTime.parse("1970-01-01T00:00:00Z"), 0d }, // Can't reverse because of offsets that are equivalent but not equals. + {OffsetDateTime.parse("1970-01-01T00:00:00.000000001Z"), 0.000001d}, + {OffsetDateTime.parse("2024-02-12T11:38:00+01:00"), 1707734280000d}, + {OffsetDateTime.parse("2024-02-12T11:38:00.123+01:00"), 1707734280123d}, + {OffsetDateTime.parse("2024-02-12T11:38:00.1234+01:00"), 1707734280123.4d}, + {OffsetDateTime.parse("2024-02-12T11:38:00.123456789+01:00"), 1707734280123.456789d}, + {OffsetDateTime.parse("2024-02-12T11:38:00.123937482+01:00"), 1707734280123.937482d}, + }); TEST_DB.put(pair(Date.class, Double.class), new Object[][]{ {new Date(Long.MIN_VALUE), (double) Long.MIN_VALUE, true}, {new Date(Integer.MIN_VALUE), (double) Integer.MIN_VALUE, true}, @@ -1110,6 +1135,9 @@ public ZoneId getZoneId() { {new Timestamp(Integer.MIN_VALUE), (double) Integer.MIN_VALUE}, {new Timestamp(0), 0d, true}, {new Timestamp(now), (double) now}, +// { Timestamp.from(ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").toInstant()), -0.000001d}, // no equals IEEE-754 limitations (so close) + { Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00Z").toInstant()), 0d, true}, + { Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").toInstant()), 0.000001d, true }, { Timestamp.from(ZonedDateTime.parse("2024-02-18T06:31:55.987654321+00:00").toInstant()), 1708237915987.654321d }, // no reverse due to IEEE-754 limitations { Timestamp.from(ZonedDateTime.parse("2024-02-18T06:31:55.123456789+00:00").toInstant()), 1708237915123.456789d }, // no reverse due to IEEE-754 limitations {new Timestamp(Integer.MAX_VALUE), (double) Integer.MAX_VALUE}, @@ -1663,6 +1691,9 @@ public ZoneId getZoneId() { ///////////////////////////////////////////////////////////// // Instant ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, Instant.class), new Object[][]{ + { null, null } + }); TEST_DB.put(pair(String.class, Instant.class), new Object[][]{ {"", null}, {" ", null}, @@ -1670,6 +1701,16 @@ public ZoneId getZoneId() { {"2024-12-31T23:59:59.999999999Z", Instant.parse("2024-12-31T23:59:59.999999999Z")}, }); + ///////////////////////////////////////////////////////////// + // OffsetDateTime + ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, OffsetDateTime.class), new Object[][]{ + { null, null } + }); + TEST_DB.put(pair(OffsetDateTime.class, OffsetDateTime.class), new Object[][]{ + {OffsetDateTime.parse("2024-02-18T06:31:55.987654321+00:00"), OffsetDateTime.parse("2024-02-18T06:31:55.987654321+00:00"), true }, + }); + ///////////////////////////////////////////////////////////// // MonthDay ///////////////////////////////////////////////////////////// @@ -1857,6 +1898,14 @@ public ZoneId getZoneId() { { Duration.ofNanos(2682374400000000001L), Timestamp.from(ZonedDateTime.parse("2055-01-01T00:00:00.000000001Z").toInstant()), 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.from(ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").toInstant()) }, + {OffsetDateTime.parse("1970-01-01T00:00:00.000000000Z"), Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00.000000000Z").toInstant()) }, + {OffsetDateTime.parse("1970-01-01T00:00:00.000000001Z"), Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").toInstant()) }, + {OffsetDateTime.parse("2024-02-18T06:31:55.987654321+00:00"), Timestamp.from(ZonedDateTime.parse("2024-02-18T06:31:55.987654321+00:00").toInstant()) }, + }); + ///////////////////////////////////////////////////////////// // ZoneOffset ///////////////////////////////////////////////////////////// @@ -2331,4 +2380,26 @@ void testConvert(String shortNameSource, String shortNameTarget, Object source, void testConvertReverse(String shortNameSource, String shortNameTarget, Object source, Object target, Class sourceClass, Class targetClass) { testConvert(shortNameSource, shortNameTarget, source, target, sourceClass, targetClass); } + + @Test + void testFoo() + { + // LocalDate to convert + LocalDate localDate = LocalDate.of(1970, 1, 1); + + // Array of time zones to test + String[] zoneIds = {"UTC", "America/New_York", "Europe/London", "Asia/Tokyo", "Australia/Sydney"}; + + // Perform conversion for each time zone and print the result + for (String zoneIdStr : zoneIds) { + ZoneId zoneId = ZoneId.of(zoneIdStr); + long epochMillis = convertLocalDateToEpochMillis(localDate, zoneId); + System.out.println("Epoch Milliseconds for " + zoneId + ": " + epochMillis); + } + } + public static long convertLocalDateToEpochMillis(LocalDate localDate, ZoneId zoneId) { + ZonedDateTime zonedDateTime = localDate.atStartOfDay(zoneId); + Instant instant = zonedDateTime.toInstant(); + return instant.toEpochMilli(); + } } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java index c7c69cd7..324cad13 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterTest.java @@ -11,6 +11,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -4243,6 +4244,21 @@ void testStringToCharArray(String source, Charset charSet, char[] expected) { assertThat(actual).isEqualTo(expected); } + @Test + void testTimestampAndOffsetDateTimeSymmetry() + { + Timestamp ts1 = new Timestamp(System.currentTimeMillis()); + Instant instant1 = ts1.toInstant(); + + OffsetDateTime odt = converter.convert(ts1, OffsetDateTime.class); + Instant instant2 = odt.toInstant(); + + assertEquals(instant1, instant2); + + Timestamp ts2 = converter.convert(odt, Timestamp. class); + assertEquals(ts1, ts2); + } + @Test void testKnownUnsupportedConversions() { assertThatThrownBy(() -> converter.convert((byte)50, Date.class))