In part two of this post, we'll go over a few tips for handling time and date in Java in respect to time zones. I recommend reading part one if you haven't already.
While this post doesn't cover every aspect of working with dates and times in relation to time zones, it's meant to provide the basics. Continue reading for more info.
Human Readable Calendars With ZonedDateTime
Sometimes you do need a human representation of an instant in time. This includes month, day of week, current hour, and so on. But, here is a major complication: date and time vary across countries and regions. Instant is simple and universal, but not very useful for human beings; it's just a number. If you have business logic related to a calendar, like:
- ...must happen during office hours...
- ...up to one day...
- ...two business days...
- ...valid for up to one year...
...then you must use some calendar system. java.time.ZoneDateTime is the best alternative to the absolutely awful java.util.Calendar. As a matter of fact java.util.Date and Calendar are so broken by design that they are considered to be deprecated entirely in JDK 9. You can create ZoneDateTime from Instant only by providing a time zone. Otherwise, default system time zone is used which you have no control over. Converting
ZonedDateTime in any way without providing explicit
ZoneId is probably a bug:
Instant now = Instant.now(); System.out.println(now); ZonedDateTime dateTime = ZonedDateTime.ofInstant( now, ZoneId.of("Europe/Warsaw") ); System.out.println(dateTime);
The output is as follows:
Notice that Instant (for convenience) displays date formatted in UTC whereas ZoneDateTime uses supplied ZoneId (+2 hours during summer, more on that later).
There are many misconceptions and myths related to time and calendars. For example, some people believe that the time difference between two locations is always constant. There are at least two reasons for that not being true. First is daylight savings time, aka summer time:
LocalDate localDate = LocalDate.of(2016, Month.AUGUST, 5); LocalTime localTime = LocalTime.of(10, 21); LocalDateTime local = LocalDateTime.of(localDate, localTime); ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw")); ZonedDateTime sydney = warsaw.withZoneSameInstant(ZoneId.of("Australia/Sydney")); System.out.println(warsaw); System.out.println(sydney);
The output reveals that the difference between Warsaw and Sydney is exactly 8 hours:
Or is it? Change August to February and the difference becomes 10 hours:
That's because Warsaw does not observe DST in February (it's winter), whereas in Sydney it's summer, so they use DST (+1 hour). In August, it's vice-versa. To make things even more complex, the time to switch to DST varies and it's always during the night of the local time so there must be a moment where one country already switched but not the other, for example in October:
Nine hours of difference—another reason why time offset differs is political:
LocalDate localDate = LocalDate.of(2014, Month.FEBRUARY, 5); LocalTime localTime = LocalTime.of(10, 21); LocalDateTime local = LocalDateTime.of(localDate, localTime); ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw")); ZonedDateTime moscow = warsaw.withZoneSameInstant(ZoneId.of("Europe/Moscow")); System.out.println(warsaw); System.out.println(moscow);
The time difference between Warsaw and Moscow on February 5th, 2014 was three hours:
But, the difference on the exact same day a year later is two hours:
That's because Russia is changing their DST policy and time zone like crazy.
Another common misconception about dates is that a day is 24 hours. This is again related to daylight savings time:
LocalDate localDate = LocalDate.of(2017, Month.MARCH, 26); LocalTime localTime = LocalTime.of(1, 0); ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Europe/Warsaw")); ZonedDateTime oneDayLater = warsaw.plusDays(1); Duration duration = Duration.between(warsaw, oneDayLater); System.out.println(duration);
What do you know, the difference between 1 AM on March 26th and 27th, 2017 is... 23 hours (
PT23H). But if you change the time zone to
Australia/Sydney you'll get the familiar 24 hours because nothing special happens that day in Sydney. That special day in Sydney happens to be the 2nd of April, 2017:
LocalDate localDate = LocalDate.of(2017, Month.APRIL, 2); LocalTime localTime = LocalTime.of(1, 0); ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Australia/Sydney"));
This actually results in one day being equal to... 25 hours. But not in Brisbane (
"Australia/Brisbane"), a thousand km north of Sydney, which does not observe DST. Why is all of this important? When you make an agreement with your client that something is supposed to take one day vs. 24 hours this may actually make a huge difference on certain days. You must be precise, otherwise your system will become inconsistent twice a year. And, don't get me started on leap second.
Storing and Transmitting Time
By default, you should store and send time either as a timestamp (long value) or as an ISO 8601 which is basically what
Instant.toString() does as per the documentation. I prefer
long value as it is more compact, but you may need a more readable format in some text encoding like JSON. Also
long is timezone-agnostic so you are not pretending that the timezone you send/store has any meaning. This applies both to transmitting time and storing it in database.
There are cases where you may want to send full calendar information, including timezone. For example, when you build a chatting application, you might want to tell the client what the local time was when the message was sent if your friend lives in a different timezone. Otherwise, you know it was sent at 10 AM your time, but what was the time in your friend's location? Another example is an airline ticket booking website. You want to tell your clients when a flight departs and arrives in local time and it's only the server that knows the exact timezone at departure and destination.
The lesson to learn here is that every time you enter calendar domain you must think about time zones. There are convenience methods that use default system time zone but in cloud environments, you may not have control over that. The same applies to default character encoding, but that's a different story.
Stay tuned for part three!