Fun with Time Zones in Java 8

[Note: Revised based on suggestions in the comments.]

They say that one way to identify a software developer is to whisper the word “timezone” in their ear and see if they shudder.

That’s certainly true for me, though my reaction is based more on travel and trying to arrange conference calls across time zones than actual coding. Like most scary things, I’ve tried to avoid the whole date/time API in Java, partly because prior to Java 8 the API is a tire fire and partly because the whole issue is like the “Here be dragons” section of a map.

herebedragons

Recently, however, I’ve been teaching Java 8 upgrade classes, and making Java 8 presentations at conferences on the No Fluff, Just Stuff tour. As part of those talks, I give an overview of the new java.time package.

The new package, by the creators of JodaTime, finally (finally!) provides an alternative to java.util.Date and java.util.Calendar. New classes like java.time.LocalDate, java.time.LocalTime, java.time.LocalDateTime and java.time.ZonedDateTime are now all available and much more powerful. If you used JodaTime in the past (no pun intended, but they’re hard to avoid), you’re already familiar with them, as the same people who wrote JodaTime in the first place wrote the new package.

I’m certainly not going to review the whole thing here, but I did want to mention a couple of fun examples.

First, I’ve known for some time that there are time zones in the world that are off by half-hour offsets rather than whole hours. To pick one, Indian Standard Time is UTC+05:30. When I mentioned that in class, I also said that someone once told me that there was a time zone in the world offset by 45 minutes. At the time I thought they were pulling my leg, but now I have the machinery to find out.

Once problem, however, is that abbreviations like EST or IST are no longer valid. The Wikipedia article on Time Zones discusses the issue, which claims that “such designations can be ambiguous”, where ECT could stand for Eastern Carribean Time, Ecuador Time, or even European Central Time. Instead, the ISO 8601 standard uses either offset designators, like UTC-05:00, or “region-based IDs”, like “America/New_York”.

(Speaking of the ISO 8601 standard, since there’s an XKCD cartoon on everything, here’s the one on that: https://xkcd.com/1179/ .)

Bringing it back to Java, the API defines a class called java.time.ZoneId, which has a static method called ZoneId.of(...) that takes a designator. You use that to create a ZonedDateTime. If you use an offset as the argument, then the time in the ZonedDateTime does not change, but if you use the region, the time will automatically adjust for Daylight Savings Time rules in that region.

[As you can imagine, the whole Daylight Savings Time issue is another rabbit hole I choose not to dive into. Those rules are discussed in a class called java.util.time.zone.ZoneRules, which refers to classes like ZoneOffsetTransition, ZoneOffsetTransitionRule, and ZoneRulesProvider. You can see how the complexity just goes up and up, especially because DST rules change frequently in different locations. Yikes.]

If you know the region ID, you can create a ZoneId using the of method. I have the opposite problem, however. I want to figure out the region ID given the offset.

Fortunately, the Java Tutorial has a section on ZoneId and ZoneOffset that actually addresses this problem. For some strange reason, however, their sample code doesn’t use the Java 8 streams and lambda expressions, so I decided to rewrite it. Here’s my version:

[sourcecode language=”java”]
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;

import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.toList;

public class FunnyOffsets {
public static void main(String[] args) {

Instant instant = Instant.now();
ZonedDateTime current = instant.atZone(ZoneId.systemDefault());
System.out.printf("Current time is %s%n%n", current);

System.out.printf("%10s %20s %13s%n", "Offset", "ZoneId", "Time");
ZoneId.getAvailableZoneIds().stream()
.map(ZoneId::of)
.filter(zoneId -> {
ZoneOffset offset = instant.atZone(zoneId).getOffset();
return offset.getTotalSeconds() % (60 * 60) != 0;
})
.sorted(comparingInt(zoneId ->
instant.atZone(zoneId).getOffset().getTotalSeconds()))
.forEach(zoneId -> {
ZonedDateTime zdt = current.withZoneSameInstant(zoneId);
System.out.printf("%10s %25s %10s%n", zdt.getOffset(), zoneId,
zdt.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)));
});
}
}
[/sourcecode]

That code requires some explanation. First, the ZoneId.getAvailableZoneIds() method returns a Set of Strings containing all the region IDs. After converting to a Stream, the map(ZoneId::of) expression transforms that into a stream of ZoneId instances.

Then I want to filter that stream to return only those ZoneIds that have an offset that isn’t evenly divisible by 3600 (= 60 sec/min * 60 min/hr). To get the offset, however, you need a ZonedDateTime, so I use the current Instant and use the atZone method with each ZoneId to get a ZonedDateTime, and then call its getOffset method. That, in turn, has a getTotalSeconds method, and I can do the modulus on that. At that point, I could have just printed them, but I decided to sort them by offset first.

The sorted method on Stream takes a java.util.Comparator. I could implement the Comparator as a lambda myself, but Java 8 also added several default and static methods to that interface. One of them is Comparator.comparingInt, which takes an ToIntFunction that transforms its argument into an int. Then sorted generates a Comparator that sorts the ints, which then sorts the collection based on the results.

Believe it or not, that whole map/filter/sorted paradigm gets much easier with practice. It was harder for me to write that explanation than to figure out the method calls.

To print the results, I wanted to show the offset in each time zone as well as its region name. The ZonedDateTime class has a method called withZoneSameInstant, which converts a given time to its equivalent in another time zone.

(That’s a very convenient method that I’ve needed my entire professional career, and justifies all the time (again, no pun intended) I’ve spent on this.)

Finally, printing them out was easier if I formatted the time, for which I used the DateTimeFormatter shown. The result right now is:

[sourcecode language=”java”]
Current time is 2016-07-16T16:12:51.905-04:00[America/New_York]
Offset ZoneId Time
-09:30 Pacific/Marquesas 10:42 AM
-04:30 America/Caracas 3:42 PM
-02:30 America/St_Johns 5:42 PM
-02:30 Canada/Newfoundland 5:42 PM
+04:30 Iran 12:42 AM
+04:30 Asia/Tehran 12:42 AM
+04:30 Asia/Kabul 12:42 AM
+05:30 Asia/Kolkata 1:42 AM
+05:30 Asia/Colombo 1:42 AM
+05:30 Asia/Calcutta 1:42 AM
+05:45 Asia/Kathmandu 1:57 AM
+05:45 Asia/Katmandu 1:57 AM
+06:30 Asia/Rangoon 2:42 AM
+06:30 Indian/Cocos 2:42 AM
+08:45 Australia/Eucla 4:57 AM
+09:30 Australia/North 5:42 AM
+09:30 Australia/Yancowinna 5:42 AM
+09:30 Australia/Adelaide 5:42 AM
+09:30 Australia/Broken_Hill 5:42 AM
+09:30 Australia/South 5:42 AM
+09:30 Australia/Darwin 5:42 AM
+10:30 Australia/Lord_Howe 6:42 AM
+10:30 Australia/LHI 6:42 AM
+11:30 Pacific/Norfolk 7:42 AM
+12:45 NZ-CHAT 8:57 AM
+12:45 Pacific/Chatham 8:57 AM
[/sourcecode]
So not only are there regions with half-hour offsets, like “Canada/Newfoundland”, “Australia/Adelaide”, and “Pacific/Norfolk”, there are indeed time zones offset by 45 minutes, like “Asia/Katmandu”, “Australia/Eucla”, and “Pacific/Chatham”.

I haven’t been able to find the reasons for all the odd offsets, but they appear to be due to political compromises between two surrounding zones. Some are very recent adoptions, like the Mongolian one (“Asia/Kathmandu”), which wasn’t established until 1986.

On the guiding principle that anything I can do in Java I can do much more easily in Groovy, I decided to write a Groovy version. In this case, the Groovy JDK hasn’t done anything with the classes in java.time yet. Still, the normal Groovy simplifications lead me to this version:

[sourcecode language=”groovy”]
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle

LocalDateTime now = LocalDateTime.now();
List<ZonedDateTime> zdts =
ZoneId.availableZoneIds
.collect { now.atZone(ZoneId.of(it)) }
.findAll { it.offset.totalSeconds % (60 * 60) != 0 }
.sort { it.offset.totalSeconds }

ZonedDateTime current = now.atZone(ZoneId.systemDefault());
println "Current time is $current"
printf("%10s %20s %13s%n", "Offset", "ZoneId", "Time")
zdts.each {
ZonedDateTime zdt = current.withZoneSameInstant(it.zone)
System.out.printf("%10s %25s %10s%n", zdt.offset, it.zone,
zdt.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)))
}
[/sourcecode]

I could have used the same map/filter/sorted methods here that I used in Java, but I think this version is a bit more idiomatic. All the needed methods have been added directly to collections, so I don’t need to switch to streams first. That means I don’t need to switch back, either, so I need fewer steps. I also take advantage of the convention that property access (like offset or totalSeconds) is converted to the associated getter method (getOffset or getTotalSeconds) automatically. This time, just to show an alternative, I used the ZonedDateTime class instead of Instant and converted to a list before printing the values.

That was fun, but if you really want see how crazy time zones can get, check out this figure, from the Wikipedia article on time zones in Antarctica.

antarctica_time_zones

If that doesn’t make a developer shudder, nothing will.

I decided to print those out, too. Here’s my Java version:

[sourcecode language=”java”]
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class AntarcticaTimeZones {
public static void main(String[] args) {
Instant now = Instant.now();
ZoneId.getAvailableZoneIds().stream()
.filter(id -> id.contains("Antarctica"))
.map(id -> now.atZone(ZoneId.of(id)))
.sorted(Comparator.comparingInt(zoneId -&amp;gt;
zoneId.getOffset().getTotalSeconds()))
.collect(Collectors.toList());
.forEach(zdt ->
System.out.printf("%s: %s%n", zdt.getOffset(), zdt.getZone()));
}
}
[/sourcecode]

This time I filtered on region IDs with the word “Antarctica” and I didn’t bother with the static import for Comparator.comparingInt. The result this time is:

[sourcecode language=”bash”]
-04:00: Antarctica/Palmer
-03:00: Antarctica/Rothera
+03:00: Antarctica/Syowa
+05:00: Antarctica/Mawson
+06:00: Antarctica/Vostok
+07:00: Antarctica/Davis
+08:00: Antarctica/Casey
+10:00: Antarctica/DumontDUrville
+11:00: Antarctica/Macquarie
+12:00: Antarctica/McMurdo
+12:00: Antarctica/South_Pole
[/sourcecode]

Yeah, good luck with that. The Groovy version is naturally shorter:

[sourcecode language=”groovy”]
import java.time.ZoneId

ZoneId.availableZoneIds
.findAll { it ==~ /.*Antarctica.*/ }
.collect { now.atZone(ZoneId.of(it)) }
.sort { it.offset.totalSeconds }
[/sourcecode]

In case you’re wondering, orbiting spacecraft experience many sunrises and sunsets in a 24 hour period, so timezones are hopeless. The International Space Station (according to the Wikipedia article on time zones in space) just gives up and uses GMT. The same article says that the “common practice for lunar missions is to use the Earth-based time zone of the launch site or mission control”.

Timekeeping on Mars gets worse, because the length of the Martian day is approximately 24 hours and 39 minutes, which is why Matt Damon kept referring to a sol.

That reminds me of this quote from Men in Black:

Jay: Zed, don’t you guys ever get any sleep around here?
Zed: The twins keep us on Centaurian time, standard thirty-seven hour day. Give it a few months. You’ll get used to it… or you’ll have a psychotic episode.

I suspect that if I spend much more time (ugh, again — see how hard it is to avoid those puns?) on this, I may be vulnerable to the same problem, so I’ll take this as a good time (haha — that one was intentional) to end.

9 responses to “Fun with Time Zones in Java 8”

  1. Instead of using LocalDateTime, you can do this:

    Instant now = Instant.now();

    ZoneOffset offset = zoneId.getRules().getOffset(now);

    Hope that helps.

  2. And instead of collecting to a List, and then call forEach() on the list, you can call forEach or forEachOrdered on the stream directly.

  3. Hi Stephen,

    I’m not sure that the code using Instant is any simpler, but it’s definitely a good alternative. Thanks. 🙂

    Hi jbnizet,

    Since I’m only printing the values here, I could have used forEach. Normally, though, I’m planning to return something instead of just printing it. That’s why I got in the habit of using a collect first. Certainly could have done it the way you suggested, though. Thanks. 🙂

  4. Thanks, nice post

  5. […] Not only with the old Java API but also in real life as Ken Kousen shows in his recent blog post: Fun with Time Zones in Java 8. There are not only time zones with 1-hour offsets, but there are also some with 30- and 45-minute […]

  6. […] >> Fun with Time Zones in Java 8 [kousenit.org] […]

  7. […] stuff for understanding Java 8, like this presentation on understanding Stream performance, and some guidance on using Timezones. We published some Java 8 Top Tips with a focus on IntelliJ features (not surprisingly), and […]

  8. Great post. But you are missing one issue that I’m currently running into and no-one seems to be addressing in the web. Apparently java libraries understand timezone offsets so you can ask them to convert time between Brasilia and San Francisco (For example).

    BUT what is left out is how to handle “special scenarios”. For example, what happens if Brazil decides that next month the applicable offset for daylight savings changes, as a special rule valid for this year only? as a programmer you cannot trust Java to convert time between Brasilia and San Francisco anymore, because the preloaded offset will be wrong. How do you handle such scenario?

  9. You’re right. I’m only able to use the libraries as they are, and they currently have limited abilities to handle special cases. I would recommend talking to Stephen Colebourne (the creator of Joda time and the head of the JSR that added java.time to Java 8) and see what he suggests.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.