Skip to content

Commit

Permalink
Added more date-time conversions, improved nano resolutions on Double…
Browse files Browse the repository at this point in the history
…. Many more tests added.
  • Loading branch information
jdereg committed Feb 24, 2024
1 parent a42b6c5 commit b3bb8aa
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 10 deletions.
4 changes: 3 additions & 1 deletion src/main/java/com/cedarsoftware/util/convert/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

/**
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -80,18 +81,19 @@ 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) {
return new Date(toLong(from, 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));
}

Expand All @@ -118,6 +120,13 @@ static Map<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([email protected])
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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},
Expand All @@ -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},
Expand Down Expand Up @@ -1663,13 +1691,26 @@ 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},
{"1980-01-01T00:00Z", Instant.parse("1980-01-01T00:00:00Z")},
{"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
/////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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
/////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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();
}
}
16 changes: 16 additions & 0 deletions src/test/java/com/cedarsoftware/util/convert/ConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit b3bb8aa

Please sign in to comment.