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:

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)));
            });
    }
}

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:

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

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:

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)))
}

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:

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()));
    }
}

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:

-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

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

import java.time.ZoneId

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

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.

%d bloggers like this: