Search code examples
javajava-8java-11localdatetime

LocalDateTime to milliseconds difference in Java 8 and Java 11


I'm currently in the process of upgrading a few projects from Java 8 to Java 11 where one of the unit tests for a converter failed. Basically the problem stems from the equality check failing due to a a date precision which previously passed with JDK 8.

Here's a section sample of the test, I've moved the contents of the converter for clarity:

@Test
public void testDateTime() {
    LocalDateTime expected = LocalDateTime.now().plusDays(1L);

    // converter contents
    long epochMillis = expected.atZone(ZoneId.systemDefault())
            .toInstant().toEpochMilli();
    LocalDateTime actual = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis),
            TimeZone.getDefault().toZoneId());


    assertThat(actual, equalTo(expected));
}

This results to an assetion error due to:

Expected :<2021-06-02T14:06:21.820299>
Actual   :<2021-06-02T14:06:21.820>

I can trunkate the expected with assertThat(actual, equalTo(expected.truncatedTo(ChronoUnit.MILLIS))) for them to be equal, however, this means each time a comparison is made (isAfter, isBefore, equals) to the converter class being tested, trunkating will have to be applied.

Is there a proper way to do conversions for between LocalDateTime to Long and vice versa for JDK 11 (or a documentation I might have missed perhaps :))?


Update:

As pointed out in the comments, the representation is not the same for Java 8 and 11 thus resulting to the failing test. To give more context on what is being asked by this post, here are the 2 methods being verified by the test (which i moved to the test itself to only capture what is being executed because the unit test that failed belongs to a class that uses the utility method)

public Long localDateTimeToEpochMillis(LocalDateTime ldt) {
    Instant instant = ldt.atZone(ZoneId.systemDefault()).toInstant();
        return ldt.atZone(ZoneId.systemDefault())
           .toInstant().toEpochMilli();
}

and

public LocalDateTime epochMillisToLocalDateTime(long epochMillis) {
    return LocalDateTime.ofInstant(
            Instant.ofEpochMilli(epochMillis), 
            ZoneId.systemDefault());
}

What the existng tests seems to verify is that given a long value, i should get the same LocalDateTime equivalent and this was done by using the Given (LocalDateTime converted to Long value) then back to LocalDateTime for comparison.


Solution

  • If you take a look at the difference:

    Expected :<2021-06-02T14:06:21.820299>
    Actual   :<2021-06-02T14:06:21.820>
    

    You can see that it removes anything less than a millisecond.

    This happens because you convert the LocalDateTime to milliseconds:

    .toInstant().toEpochMilli();
    

    In order to avoid that, you can use Instant#getNano:

    Gets the number of nanoseconds, later along the time-line, from the start of the second. The nanosecond-of-second value measures the total number of nanoseconds from the second returned by getEpochSecond().

    It could look like this:

    Instant instant=expected.atZone(ZoneId.systemDefault())
                    .toInstant();
    long epochMillis = instant.toEpochMilli();
    long nanos=instant.getNano()%1000000;//get nanos of Millisecond
    
    LocalDateTime actual = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis).plusNanos(nanos),
            TimeZone.getDefault().toZoneId());
    

    Why did it work in Java 8?

    As this post and JDK-8068730 Increase the precision of the implementation of java.time.Clock.systemUTC() describes, Java 8 did not capture time units smaller than a millisecond. Since Java 9, LocalDateTime.now (and similar) get the time with microseconds.