javadatetimejava-11java-17offsetdatetime

How to format OffsetDateTime in java 13 or above?


In Java 11, clock system uses millisecond precision, but apparently in Java 13 and above, it uses microsecond precision and this causes my tests to fail. As an example, OffsetDateTime.now() gives me this date "2021-12-10T10:58:05.309594500+01:00" while when I read this date from database "2021-12-10T10:58:05.309595+01:00". I am searching for a way that I can format the first date in a way that they should be equal. I do want having it in OffsetDateTime type not string.

Update: I have realised that this problem raised when I upgraded java version from 11 to 17 and not on local, I get this problem when gitlab runs the test.

this is the test:

@Test
    fun `can store, find and delete a failed-message`() {
        // given: a failed-message
        val failedMessage = FailedMessage(
            failedMessageId = FailedMessageId("the-subcription", "the-message-id"),
            messageAttributes = mapOf("one" to "een", "two" to "twee"),
            messagePayload = "message-payload",
            exception = "exception",
            dateTime = OffsetDateTime.now(),
            stackTrace = "stackey tracey"
        )
       
        failedMessageRepository.store(failedMessage)
      
        assertEquals(failedMessage, failedMessageRepository.find(failedMessage.failedMessageId))
}

this test gets failed due to not equal dateTime(s). and this is the log:

<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309594500+01:00, stackTrace=stackey tracey)>
 but was: 
<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309595+01:00, stackTrace=stackey tracey)>

Could you please help me?


Solution

  • An OffsetDateTime always has got nanosecond precision. It’s now method may not have depending on platform and Java version. Internally the object has. So there is no way that you can have an OffsetDateTime with fewer than 9 decimals on the seconds.

    How many decimals are printed is a different question. What is really printed when you print an OffsetDateTime is a String. If you just print the object, its toString method is implicitly called to produce the string that we see in print. OffsetDateTime.toString() always gives us as many groups of 3 decimals as are necessary for rendering the full precision. So if the decimals were .100000000, then .100 is printed. If they were .309594500, as you have seen already, all 9 decimals are present in the string. There is no way that you can change this behaviour.

    If you want a different string printed, you can use a DateTimeFormatter with the number of decimals you like.

    So what you can do:

    1. You can modify the OffsetDateTime, or really, you can create a new one like the old one only with more decimals being zero, which in turn may cause fewer decimals to be printed by toString().
    2. You can format the OffsetDateTime into a string of your liking.

    The following code snippets combines both options. For formatting only the decimals up to the last non-zero decimal I am using the following formatter:

    private static final DateTimeFormatter ODT_FORMATTER = new DateTimeFormatterBuilder()
            .appendPattern("uuuu-MM-dd'T'HH:mm.ss")
            .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
            .appendOffsetId()
            .toFormatter(Locale.ROOT);
    

    Demonstration:

        OffsetDateTime dateTime = OffsetDateTime.parse("2021-12-10T10:58:05.309594500+01:00");
    
        int nanos = dateTime.getNano();
        OffsetDateTime with7Decimals = dateTime.withNano(nanos / 100 * 100);
        System.out.println("toString(): " + with7Decimals);
        System.out.println("Formatted:  " + with7Decimals.format(ODT_FORMATTER));
        OffsetDateTime with5Decimals = dateTime.withNano(nanos / 10000 * 10000);
        System.out.println("toString(): " + with5Decimals);
        System.out.println("Formatted:  " + with5Decimals.format(ODT_FORMATTER));
    

    Output:

    toString(): 2021-12-10T10:58:05.309594500+01:00
    Formatted:  2021-12-10T10:58.05.3095945+01:00
    toString(): 2021-12-10T10:58:05.309590+01:00
    Formatted:  2021-12-10T10:58.05.30959+01:00
    

    For you tests I see no reason to use strings for comparison. Just compare the OffsetDateTime objects after setting those decimals to zero that will be lost in your database anyway.

    Edit: A workaround for your test failure? The following line is where you get the current time with a finer precision than your database can store.

            dateTime = OffsetDateTime.now(),
    

    So change it to give only the precision that your database supports:

            dateTime = OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS),
    

    This should cause the time to be saved and retrieved without any further rounding.

    Why did the problem arise? OffsetDateTime.now() has different precision on different platforms and Java versions. I wasn’t aware of a difference between Java 11 and 13 on any platform, but there is no reason why there shouldn’t be one, and there may be another one in some coming version. Also if you move between Linux, Windows and Mac you will likely experience differences even with the same Java version. The other part of the problem is the limited precision of your database, or more precisely, the data type that you use in the database for storing the time. If you are using timestamp with time zone, you are probably having the best precision that the database can offer. Apparently your database has microsecond precision (6 decimals on the seconds) and on your Java 13 OffsetDateTime.now() has at least half microseconds (500 nanos) precision. So you since Java 13 you are getting a value that the database cannot store. And apparently your database or database driver then rounded the value up in your case. Which caused the value retrieved back from the database not to be precisely the same as the one you had tried to save.