Search code examples
javajava-timeduration

java.time Duration.between produces zonedatetime exception


I'm trying to calculate the difference between an epoch miliseconds and the currenttime on local machine. I was hoping to use idiomatic java.time api with:

val eventTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochinMilis), ZoneId.systemDefault())
val now = LocalDateTime.now.atZone(ZoneId.systemDefault())
Duration.between(now, eventTime).toHours

However it produces:

java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: 2023-06-26T22:26:36.917 of type java.time.LocalDateTime
at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:566)
at java.base/java.time.ZonedDateTime.until(ZonedDateTime.java:2130)
at java.base/java.time.Duration.between(Duration.java:490)

With

now = LocalDateTime.now.atZone(ZoneId.systemDefault())
(epochinMilis/1000 - now.toEpochSecond()) / 3600

I'm getting what i want, but I would like to know what goes wrong when using the designated(?) api?


Solution

  • Your problem stems from comparing apples and oranges. In this case, you are trying to compare a LocalDateTime and a ZonedDateTime. Those two are very different things and cannot be compared directly.

    A ZonedDateTime describes a specific point in time, expressed in the terms of a certain time zone. It is unambiguous what point in time such a value means.

    A LocalDateTime describes some date and some time, without any information about which time zone they're presented in. This value is fundamentally ambiguous and open to interpretation; two people in different time zones reading the same LocalDateTime value would disagree over when exactly that point in time occurs.

    (For instance, if someone in London sees "1 PM on June 27, 2023", the exact point in time that it represents for them differs by five hours to how someone in New York would interpret the exact same value.)

    As such, you cannot directly compare a ZonedDateTime (an unambiguous point in time that everyone agrees on) with a LocalDateTime (a point in time that's relative to whoever uses the value). You can compare two ZonedDateTime with each other, and two LocalDateTime with each other; but not one to the other.

    If you nonetheless want to compare them, you need to decide how the two relate. For instance, you can use the .atZone(...) on LocalDateTime to add time zone information, converting it into a ZonedDateTime, which can then be compared with the other value.

    As an additional note, LocalDateTime has other issues that one should be aware of. For instance, during DST transitions in the fall, the same LocalDateTime value may represent two different points in time (since the period 2:00:00 AM-2:59:59 AM occurs twice in the same night). For reasons like this, a general rule of thumb is to only use LocalDateTime for presenting the value to the user, not for doing calculations or similar.

    And as a final "By the way, I technically lied to you" note, one might read the documentation for Duration.between and notice that it mentions automatically converting one of the values if they're of different types. Why did it crash if it handles this automatically? In this case, pure luck (or bad luck, depending on your viewpoint).

    The key here is that Duration.between will convert the second value to be the same type as the first value. Your first value is the ZonedDateTime and your second value is the LocalDateTime, meaning that the latter will be converted to the former. If you had reversed them, the conversation would succeed since it's trivial to convert a ZonedDateTime to a LocalDateTime; just delete the time zone information. (This is however potentially dangerous since it implicitly assumes that the time zone information was completely irrelevant in the first place.)

    The opposite isn't possible though; how do you automatically add the time zone information to convert a LocalDateTime to a ZonedDateTime? What value is the computer supposed to put there? Should it default to the UTC time zone? Should it default to the computer clock's time zone? Should it default to the time zone of the ZonedDateTime value? At best, the computer would have to blindly guess and hope it does the right thing, which is a very dangerous behavior for a method to have. In this case, it's much better that it crashes to notify you, the developer, that you have asked it a vague question and should be more specific.