From ef92119c5119e298323409a127ab735066f0ef2c Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sat, 17 Feb 2024 09:45:32 -0500 Subject: [PATCH] Improving accuracy. Before, times were only getting millisecond accuracy, even when the converted "to" data type could hold more than millisecond accuracy. Now, the nanosecond portion is being factored in, when the converted "to" datatype can hold it. --- .../util/convert/InstantConversions.java | 12 ++- .../convert/LocalDateTimeConversions.java | 3 +- .../util/convert/ConverterEverythingTest.java | 93 +++++++++++++++++-- 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java b/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java index 0c239533..acf843fb 100644 --- a/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/InstantConversions.java @@ -57,8 +57,18 @@ static float toFloat(Object from, Converter converter) { return toLong(from, converter); } + /** + * @return double number of milliseconds. When integerized, the number returned is always the number of epoch + * milliseconds. If the Instant specified resolution further than milliseconds, the double returned captures + * that as fractional milliseconds. + * Example 1: "2024-02-12T11:38:00.123937482+01:00" (as an Instant) = 1707734280123.937482d + * Example 2: "2024-02-12T11:38:00.1239+01:00" (as an Instant) = 1707734280123.9d + */ static double toDouble(Object from, Converter converter) { - return toLong(from, converter); + Instant instant = (Instant) from; + long millis = instant.toEpochMilli(); + int nanos = instant.getNano(); + return millis + (nanos % 1_000_000) / 1_000_000.0d; } static AtomicLong toAtomicLong(Object from, Converter converter) { diff --git a/src/main/java/com/cedarsoftware/util/convert/LocalDateTimeConversions.java b/src/main/java/com/cedarsoftware/util/convert/LocalDateTimeConversions.java index 2260f5d0..eece425c 100644 --- a/src/main/java/com/cedarsoftware/util/convert/LocalDateTimeConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/LocalDateTimeConversions.java @@ -48,7 +48,8 @@ static long toLong(Object from, Converter converter) { } static double toDouble(Object from, Converter converter) { - return toInstant(from, converter).toEpochMilli(); + Instant instant = toInstant(from, converter); + return InstantConversions.toDouble(instant, converter); } static LocalDateTime toLocalDateTime(Object from, Converter converter) { diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index b5a35d4b..899bea19 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -89,6 +89,9 @@ public ZoneId getZoneId() { // ... // {source-n, answer-n} + // Useful values for input + long now = System.currentTimeMillis(); + ///////////////////////////////////////////////////////////// // Byte/byte ///////////////////////////////////////////////////////////// @@ -777,6 +780,7 @@ public ZoneId getZoneId() { }); TEST_DB.put(pair(Date.class, Long.class), new Object[][]{ {new Date(Long.MIN_VALUE), Long.MIN_VALUE}, + {new Date(now), now}, {new Date(Integer.MIN_VALUE), (long) Integer.MIN_VALUE}, {new Date(0), 0L}, {new Date(Integer.MAX_VALUE), (long) Integer.MAX_VALUE}, @@ -785,6 +789,7 @@ public ZoneId getZoneId() { TEST_DB.put(pair(java.sql.Date.class, Long.class), new Object[][]{ {new java.sql.Date(Long.MIN_VALUE), Long.MIN_VALUE}, {new java.sql.Date(Integer.MIN_VALUE), (long) Integer.MIN_VALUE}, + {new java.sql.Date(now), now}, {new java.sql.Date(0), 0L}, {new java.sql.Date(Integer.MAX_VALUE), (long) Integer.MAX_VALUE}, {new java.sql.Date(Long.MAX_VALUE), Long.MAX_VALUE}, @@ -792,11 +797,14 @@ public ZoneId getZoneId() { TEST_DB.put(pair(Timestamp.class, Long.class), new Object[][]{ {new Timestamp(Long.MIN_VALUE), Long.MIN_VALUE}, {new Timestamp(Integer.MIN_VALUE), (long) Integer.MIN_VALUE}, + {new Timestamp(now), now}, {new Timestamp(0), 0L}, {new Timestamp(Integer.MAX_VALUE), (long) Integer.MAX_VALUE}, {new Timestamp(Long.MAX_VALUE), Long.MAX_VALUE}, }); TEST_DB.put(pair(Instant.class, Long.class), new Object[][]{ + {ZonedDateTime.parse("2024-02-12T11:38:00.123456789+01:00").toInstant(), 1707734280123L}, // maintains millis (best long can do) + {ZonedDateTime.parse("2024-02-12T11:38:00.123999+01:00").toInstant(), 1707734280123L}, // maintains millis (best long can do) {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").toInstant(), 1707734280000L}, }); TEST_DB.put(pair(LocalDate.class, Long.class), new Object[][]{ @@ -812,6 +820,16 @@ public ZoneId getZoneId() { zdt = zdt.withZoneSameInstant(TOKYO_Z); return zdt.toLocalDateTime(); }, 1707734280000L}, // Epoch millis in Tokyo timezone + {(Supplier) () -> { + ZonedDateTime zdt = ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00"); // maintains millis (best long can do) + zdt = zdt.withZoneSameInstant(TOKYO_Z); + return zdt.toLocalDateTime(); + }, 1707734280123L}, // Epoch millis in Tokyo timezone + {(Supplier) () -> { + ZonedDateTime zdt = ZonedDateTime.parse("2024-02-12T11:38:00.12399+01:00"); // maintains millis (best long can do) + zdt = zdt.withZoneSameInstant(TOKYO_Z); + return zdt.toLocalDateTime(); + }, 1707734280123L}, // Epoch millis in Tokyo timezone }); TEST_DB.put(pair(ZonedDateTime.class, Long.class), new Object[][]{ {ZonedDateTime.parse("2024-02-12T11:38:00+01:00"), 1707734280000L}, @@ -823,10 +841,19 @@ public ZoneId getZoneId() { cal.setTimeZone(TOKYO_TZ); cal.set(2024, Calendar.FEBRUARY, 12, 11, 38, 0); return cal; - }, 1707705480000L} + }, 1707705480000L}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TOKYO_TZ); + cal.setTimeInMillis(now); // Calendar maintains time to millisecond resolution + return cal; + }, now} }); TEST_DB.put(pair(OffsetDateTime.class, Long.class), new Object[][]{ {OffsetDateTime.parse("2024-02-12T11:38:00+01:00"), 1707734280000L}, + {OffsetDateTime.parse("2024-02-12T11:38:00.123+01:00"), 1707734280123L}, // maintains millis (best long can do) + {OffsetDateTime.parse("2024-02-12T11:38:00.12399+01:00"), 1707734280123L}, // maintains millis (best long can do) }); TEST_DB.put(pair(Year.class, Long.class), new Object[][]{ {Year.of(2024), 2024L}, @@ -1049,6 +1076,10 @@ public ZoneId getZoneId() { }); TEST_DB.put(pair(Instant.class, Double.class), new Object[][]{ {ZonedDateTime.parse("2024-02-12T11:38:00+01:00").toInstant(), 1707734280000d}, + {ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00").toInstant(), 1707734280123d}, + {ZonedDateTime.parse("2024-02-12T11:38:00.1234+01:00").toInstant(), 1707734280123.4d}, // fractional milliseconds (nano support) + {ZonedDateTime.parse("2024-02-12T11:38:00.1239+01:00").toInstant(), 1707734280123.9d}, + {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[][]{ {(Supplier) () -> { @@ -1063,10 +1094,26 @@ public ZoneId getZoneId() { zdt = zdt.withZoneSameInstant(TOKYO_Z); return zdt.toLocalDateTime(); }, 1.70773428E12}, // Epoch millis in Tokyo timezone + {(Supplier) () -> { + ZonedDateTime zdt = ZonedDateTime.parse("2024-02-12T11:38:00.123+01:00"); + zdt = zdt.withZoneSameInstant(TOKYO_Z); + return zdt.toLocalDateTime(); + }, 1.707734280123E12}, // Epoch millis in Tokyo timezone + {(Supplier) () -> { + ZonedDateTime zdt = ZonedDateTime.parse("2024-02-12T11:38:00.1239+01:00"); + zdt = zdt.withZoneSameInstant(TOKYO_Z); + return zdt.toLocalDateTime(); + }, 1.7077342801239E12}, // Epoch millis in Tokyo timezone + {(Supplier) () -> { + ZonedDateTime zdt = ZonedDateTime.parse("2024-02-12T11:38:00.123937482+01:00"); + zdt = zdt.withZoneSameInstant(TOKYO_Z); + return zdt.toLocalDateTime(); + }, 1707734280123.937482d}, // Epoch millis in Tokyo timezone }); TEST_DB.put(pair(ZonedDateTime.class, Double.class), new Object[][]{ {ZonedDateTime.parse("2024-02-12T11:38:00+01:00"), 1707734280000d}, }); + // Left off here (need to fix ZoneDateTime and Timestamp) 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}, @@ -1131,7 +1178,14 @@ public ZoneId getZoneId() { cal.setTimeZone(TOKYO_TZ); cal.set(2024, Calendar.FEBRUARY, 12, 11, 38, 0); return cal; - }, 1707705480000d} + }, 1707705480000d}, + {(Supplier) () -> { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TOKYO_TZ); + cal.setTimeInMillis(now); // Calendar maintains time to millisecond resolution + return cal; + }, (double)now} }); TEST_DB.put(pair(Number.class, Double.class), new Object[][]{ {2.5f, 2.5d} @@ -1424,7 +1478,6 @@ public ZoneId getZoneId() { ///////////////////////////////////////////////////////////// // BigInteger ///////////////////////////////////////////////////////////// - long now = System.currentTimeMillis(); TEST_DB.put(pair(Void.class, BigInteger.class), new Object[][]{ { null, null }, }); @@ -1498,18 +1551,42 @@ public ZoneId getZoneId() { { new AtomicInteger(Integer.MIN_VALUE), BigInteger.valueOf(Integer.MIN_VALUE) }, { new AtomicInteger(Integer.MAX_VALUE), BigInteger.valueOf(Integer.MAX_VALUE) }, }); - // Left off here TEST_DB.put(pair(AtomicLong.class, BigInteger.class), new Object[][]{ - {new AtomicLong(0), BigInteger.ZERO}, + { new AtomicLong(-1), BigInteger.valueOf(-1) }, + { new AtomicLong(0), BigInteger.ZERO }, + { new AtomicLong(Long.MIN_VALUE), BigInteger.valueOf(Long.MIN_VALUE) }, + { new AtomicLong(Long.MAX_VALUE), BigInteger.valueOf(Long.MAX_VALUE) }, }); TEST_DB.put(pair(Date.class, BigInteger.class), new Object[][]{ - {new Date(now), BigInteger.valueOf(now)}, + { new Date(0), BigInteger.valueOf(0), true }, + { new Date(now), BigInteger.valueOf(now), true }, + { new Date(Long.MIN_VALUE), BigInteger.valueOf(Long.MIN_VALUE), true }, + { new Date(Long.MAX_VALUE), BigInteger.valueOf(Long.MAX_VALUE), true }, }); TEST_DB.put(pair(java.sql.Date.class, BigInteger.class), new Object[][]{ - {new java.sql.Date(now), BigInteger.valueOf(now)}, + { new java.sql.Date(0), BigInteger.valueOf(0), true }, + { new java.sql.Date(now), BigInteger.valueOf(now), true }, + { new java.sql.Date(Long.MIN_VALUE), BigInteger.valueOf(Long.MIN_VALUE), true }, + { new java.sql.Date(Long.MAX_VALUE), BigInteger.valueOf(Long.MAX_VALUE), true }, }); TEST_DB.put(pair(Timestamp.class, BigInteger.class), new Object[][]{ - {new Timestamp(now), BigInteger.valueOf(now)}, + { new Timestamp(0), BigInteger.valueOf(0), true }, + { new Timestamp(now), BigInteger.valueOf(now), true }, +// { (Supplier) () -> { +// Timestamp ts = new Timestamp(now); +// ts.setNanos(1); +// return ts; +// }, (Supplier) () -> { +// Timestamp ts = new Timestamp(now); +// long milliseconds = ts.getTime(); +// int nanoseconds = ts.getNanos(); +// BigInteger nanos = BigInteger.valueOf(milliseconds).multiply(BigInteger.valueOf(1000000)) +// .add(BigInteger.valueOf(nanoseconds)); +// return nanos; +// } +// }, + { new Timestamp(Long.MIN_VALUE), BigInteger.valueOf(Long.MIN_VALUE), true }, + { new Timestamp(Long.MAX_VALUE), BigInteger.valueOf(Long.MAX_VALUE), true }, }); TEST_DB.put(pair(Instant.class, BigInteger.class), new Object[][]{ {Instant.ofEpochMilli(now), BigInteger.valueOf(now)},