From 0a8269718748b2471c2195bb4107eecbdfaad6f2 Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sun, 25 Feb 2024 02:24:00 -0500 Subject: [PATCH] Double's time related conversions (bi-directional) added, with supporting tests. --- .../cedarsoftware/util/convert/Converter.java | 4 +- .../util/convert/DoubleConversions.java | 21 ++-- .../util/convert/DurationConversions.java | 6 ++ .../util/convert/LocalDateConversions.java | 4 +- .../util/convert/ConverterEverythingTest.java | 97 ++++++++++++++++++- 5 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/convert/Converter.java b/src/main/java/com/cedarsoftware/util/convert/Converter.java index a3c9e0f9..f614fd77 100644 --- a/src/main/java/com/cedarsoftware/util/convert/Converter.java +++ b/src/main/java/com/cedarsoftware/util/convert/Converter.java @@ -224,6 +224,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(Double.class, Double.class), Converter::identity); CONVERSION_DB.put(pair(Boolean.class, Double.class), BooleanConversions::toDouble); CONVERSION_DB.put(pair(Character.class, Double.class), CharacterConversions::toDouble); + CONVERSION_DB.put(pair(Duration.class, Double.class), DurationConversions::toDouble); CONVERSION_DB.put(pair(Instant.class, Double.class), InstantConversions::toDouble); CONVERSION_DB.put(pair(LocalDate.class, Double.class), LocalDateConversions::toDouble); CONVERSION_DB.put(pair(LocalDateTime.class, Double.class), LocalDateTimeConversions::toDouble); @@ -517,7 +518,7 @@ private static void buildFactoryConversions() { CONVERSION_DB.put(pair(Short.class, LocalDate.class), UNSUPPORTED); CONVERSION_DB.put(pair(Integer.class, LocalDate.class), UNSUPPORTED); CONVERSION_DB.put(pair(Long.class, LocalDate.class), NumberConversions::toLocalDate); - CONVERSION_DB.put(pair(Double.class, LocalDate.class), NumberConversions::toLocalDate); + CONVERSION_DB.put(pair(Double.class, LocalDate.class), DoubleConversions::toLocalDate); CONVERSION_DB.put(pair(BigInteger.class, LocalDate.class), NumberConversions::toLocalDate); CONVERSION_DB.put(pair(BigDecimal.class, LocalDate.class), NumberConversions::toLocalDate); CONVERSION_DB.put(pair(AtomicInteger.class, LocalDate.class), UNSUPPORTED); @@ -727,6 +728,7 @@ private static void buildFactoryConversions() { // Duration conversions supported CONVERSION_DB.put(pair(Void.class, Duration.class), VoidConversions::toNull); CONVERSION_DB.put(pair(Duration.class, Duration.class), Converter::identity); + CONVERSION_DB.put(pair(Double.class, Duration.class), DoubleConversions::toDuration); CONVERSION_DB.put(pair(BigInteger.class, Duration.class), BigIntegerConversions::toDuration); CONVERSION_DB.put(pair(Timestamp.class, Duration.class), TimestampConversions::toDuration); CONVERSION_DB.put(pair(String.class, Duration.class), StringConversions::toDuration); diff --git a/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java b/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java index e86230ac..45a497e9 100644 --- a/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/DoubleConversions.java @@ -1,7 +1,9 @@ package com.cedarsoftware.util.convert; import java.sql.Timestamp; +import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; @@ -33,6 +35,10 @@ static Instant toInstant(Object from, Converter converter) { return Instant.ofEpochSecond(seconds, nanoAdjustment); } + static LocalDate toLocalDate(Object from, Converter converter) { + return toZonedDateTime(from, converter).toLocalDate(); + } + static LocalDateTime toLocalDateTime(Object from, Converter converter) { return toZonedDateTime(from, converter).toLocalDateTime(); } @@ -46,11 +52,14 @@ static OffsetDateTime toOffsetDateTime(Object from, Converter converter) { } static Timestamp toTimestamp(Object from, Converter converter) { - double milliseconds = (Double) from; - long millisPart = (long) milliseconds; - int nanosPart = (int) ((milliseconds - millisPart) * 1_000_000); - Timestamp timestamp = new Timestamp(millisPart); - timestamp.setNanos(timestamp.getNanos() + nanosPart); - return timestamp; + return Timestamp.from(toInstant(from, converter)); + } + + static Duration toDuration(Object from, Converter converter) { + double d = (Double) from; + // Separate whole seconds and nanoseconds + long seconds = (long) d; + int nanoAdjustment = (int) ((d - seconds) * 1_000_000_000); + return Duration.ofSeconds(seconds, nanoAdjustment); } } diff --git a/src/main/java/com/cedarsoftware/util/convert/DurationConversions.java b/src/main/java/com/cedarsoftware/util/convert/DurationConversions.java index 8fa7fc29..d1329b18 100644 --- a/src/main/java/com/cedarsoftware/util/convert/DurationConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/DurationConversions.java @@ -47,6 +47,12 @@ static BigInteger toBigInteger(Object from, Converter converter) { return seconds.multiply(BigIntegerConversions.BILLION).add(nanos); } + static double toDouble(Object from, Converter converter) { + Duration duration = (Duration) from; + // Convert to seconds with nanosecond precision + return duration.getSeconds() + duration.getNano() / 1_000_000_000.0; + } + static Timestamp toTimestamp(Object from, Converter converter) { Duration duration = (Duration) from; Instant epoch = Instant.EPOCH; diff --git a/src/main/java/com/cedarsoftware/util/convert/LocalDateConversions.java b/src/main/java/com/cedarsoftware/util/convert/LocalDateConversions.java index bd75fd25..a2dc2d81 100644 --- a/src/main/java/com/cedarsoftware/util/convert/LocalDateConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/LocalDateConversions.java @@ -62,7 +62,7 @@ static ZonedDateTime toZonedDateTime(Object from, Converter converter) { } static double toDouble(Object from, Converter converter) { - return toLong(from, converter); + return toInstant(from, converter).toEpochMilli(); } static AtomicLong toAtomicLong(Object from, Converter converter) { @@ -89,10 +89,12 @@ static Date toDate(Object from, Converter converter) { } static BigInteger toBigInteger(Object from, Converter converter) { + // TODO: Upgrade precision return BigInteger.valueOf(toLong(from, converter)); } static BigDecimal toBigDecimal(Object from, Converter converter) { + // TODO: Upgrade precision return BigDecimal.valueOf(toLong(from, converter)); } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 617f64de..94a3efe1 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -1060,6 +1060,15 @@ public ZoneId getZoneId() { {(char) 1, 1d}, {(char) 0, 0d}, }); + TEST_DB.put(pair(Duration.class, Double.class), new Object[][] { + { Duration.ofSeconds(-1), -1d, true }, + { Duration.ofSeconds(0), 0d, true }, + { Duration.ofSeconds(1), 1d, true }, + { Duration.ofNanos(1), 0.000000001d, true }, + { Duration.ofNanos(1_000_000_000), 1d, true }, + { Duration.ofNanos(2_000_000_001), 2.000000001d, true }, + { Duration.ofSeconds(10, 9), 10.000000009d, true }, + }); 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}, @@ -1072,13 +1081,16 @@ 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[][]{ + {LocalDate.parse("1969-12-31"), -118800000d, true}, // Proves it always works from "startOfDay", using the zoneId from options + {LocalDate.parse("1970-01-01"), -32400000d, true}, // Proves it always works from "startOfDay", using the zoneId from options + {LocalDate.parse("1970-01-02"), 54000000d, true}, // Proves it always works from "startOfDay", using the zoneId from options {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 + {ZonedDateTime.parse("2024-02-12T11:38:00.123456789+01:00").withZoneSameInstant(TOKYO_Z).toLocalDate(), 1.7076636E12, true}, // Only to start of day resolution }); TEST_DB.put(pair(LocalDateTime.class, Double.class), new Object[][]{ // {ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), -0.000001d, true}, // IEEE-754 double does not quite have the resolution @@ -1144,6 +1156,27 @@ public ZoneId getZoneId() { {new Timestamp(Long.MAX_VALUE), (double) Long.MAX_VALUE}, }); TEST_DB.put(pair(Calendar.class, Double.class), new Object[][]{ + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(-1); + return cal; + }, -1d}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(0); + return cal; + }, 0d}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(1); + return cal; + }, 1d}, {(Supplier) () -> { Calendar cal = Calendar.getInstance(); cal.clear(); @@ -1710,6 +1743,11 @@ public ZoneId getZoneId() { 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 }, }); + TEST_DB.put(pair(Double.class, OffsetDateTime.class), new Object[][]{ + {-0.000001d, OffsetDateTime.parse("1969-12-31T23:59:59.999999999Z").withOffsetSameInstant(ZonedDateTime.now(TOKYO_Z).getOffset()) }, // IEEE-754 resolution prevents perfect symmetry (close) + {0d, OffsetDateTime.parse("1970-01-01T00:00:00Z").withOffsetSameInstant(ZonedDateTime.now(TOKYO_Z).getOffset()), true }, + {0.000001d, OffsetDateTime.parse("1970-01-01T00:00:00.000000001Z").withOffsetSameInstant(ZonedDateTime.now(TOKYO_Z).getOffset()), true }, + }); ///////////////////////////////////////////////////////////// // MonthDay @@ -1868,6 +1906,16 @@ public ZoneId getZoneId() { ///////////////////////////////////////////////////////////// // Timestamp ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, Timestamp.class), new Object[][]{ + { null, null }, + }); + // No identity test - Timestamp is mutable + TEST_DB.put(pair(Double.class, Timestamp.class), new Object[][]{ + { -0.000001d, Timestamp.from(ZonedDateTime.parse("1969-12-31T23:59:59.999999999Z").toInstant())}, // IEEE-754 limit prevents reverse test + { 0d, Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00Z").toInstant()), true}, + { 0.000001d, Timestamp.from(ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").toInstant()), true}, + { (double)now, new Timestamp(now), true}, + }); TEST_DB.put(pair(BigDecimal.class, Timestamp.class), new Object[][] { { new BigDecimal("-62167219200000.000000"), Timestamp.from(ZonedDateTime.parse("0000-01-01T00:00:00Z").toInstant()), true }, { new BigDecimal("-62167219199999.999999"), Timestamp.from(ZonedDateTime.parse("0000-01-01T00:00:00.000000001Z").toInstant()), true }, @@ -1906,6 +1954,53 @@ public ZoneId getZoneId() { {OffsetDateTime.parse("2024-02-18T06:31:55.987654321+00:00"), Timestamp.from(ZonedDateTime.parse("2024-02-18T06:31:55.987654321+00:00").toInstant()) }, }); + ///////////////////////////////////////////////////////////// + // LocalDate + ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, LocalDate.class), new Object[][] { + { null, null } + }); + TEST_DB.put(pair(LocalDate.class, LocalDate.class), new Object[][] { + { LocalDate.parse("1970-01-01"), LocalDate.parse("1970-01-01"), true } + }); + TEST_DB.put(pair(Double.class, LocalDate.class), new Object[][] { // options timezone is factored in (86,400,000 millis per day) + { -32400001d, LocalDate.parse("1969-12-31") }, + { -32400000d, LocalDate.parse("1970-01-01"), true }, + { 0d, LocalDate.parse("1970-01-01") }, + { 53999999d, LocalDate.parse("1970-01-01") }, + { 54000000d, LocalDate.parse("1970-01-02"), true }, + }); + + ///////////////////////////////////////////////////////////// + // LocalDateTime + ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, LocalDateTime.class), new Object[][] { + { null, null } + }); + TEST_DB.put(pair(LocalDateTime.class, LocalDateTime.class), new Object[][] { + { LocalDateTime.of(1970, 1, 1, 0,0), LocalDateTime.of(1970, 1, 1, 0,0), true } + }); + TEST_DB.put(pair(Double.class, LocalDateTime.class), new Object[][] { + { -0.000001d, LocalDateTime.parse("1969-12-31T23:59:59.999999999").atZone(ZoneId.of("UTC")).withZoneSameInstant(TOKYO_Z).toLocalDateTime() }, // IEEE-754 prevents perfect symmetry + { 0d, LocalDateTime.parse("1970-01-01T00:00:00").atZone(ZoneId.of("UTC")).withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true }, + { 0.000001d, LocalDateTime.parse("1970-01-01T00:00:00.000000001").atZone(ZoneId.of("UTC")).withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true }, + }); + + ///////////////////////////////////////////////////////////// + // ZonedDateTime + ///////////////////////////////////////////////////////////// + TEST_DB.put(pair(Void.class, ZonedDateTime.class), new Object[][]{ + { null, null }, + }); + TEST_DB.put(pair(ZonedDateTime.class, ZonedDateTime.class), new Object[][]{ + { ZonedDateTime.parse("1970-01-01T00:00:00.000000000Z").withZoneSameInstant(TOKYO_Z), ZonedDateTime.parse("1970-01-01T00:00:00.000000000Z").withZoneSameInstant(TOKYO_Z) }, + }); + TEST_DB.put(pair(Double.class, ZonedDateTime.class), new Object[][]{ + { -0.000001d, ZonedDateTime.parse("1969-12-31T23:59:59.999999999+00:00").withZoneSameInstant(TOKYO_Z)}, // IEEE-754 limit prevents reverse test + { 0d, ZonedDateTime.parse("1970-01-01T00:00:00+00:00").withZoneSameInstant(TOKYO_Z), true}, + { 0.000001d, ZonedDateTime.parse("1970-01-01T00:00:00.000000001Z").withZoneSameInstant(TOKYO_Z), true}, + }); + ///////////////////////////////////////////////////////////// // ZoneOffset /////////////////////////////////////////////////////////////