7

I'm trying to map the values that come from the Front-End to ZoneId class like this:

Optional.ofNullable(timeZone).map(ZoneId::of).orElse(null)

For most time zones it works fine, however, for some values Java throws exception:

java.time.zone.ZoneRulesException: Unknown time-zone ID: America/Punta_Arenas

However, it is a valid time-zone according to IANA: https://www.iana.org/time-zones

Zone America/Punta_Arenas -4:43:40 - LMT 1890

I was thinking about using offset for such time-zones (just to hardcode values), but I guess there should be more convenient way to solve the issue. Is there a way Java can handle that?

Other timezones that are not supported:

  • America/Punta_Arenas
  • Asia/Atyrau
  • Asia/Famagusta
  • Asia/Yangon
  • EST
  • Europe/Saratov
  • HST
  • MST
  • ROC

My Java version: "1.8.0_121" Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

dvelopp
  • 4,095
  • 3
  • 31
  • 57
  • 2
    Which version of Java are you using exactly; the latest update of Java 8? Time zone information is updated frequently with new Java updates, it's possible that you are using a specific Java version in which the one you are referencing is not defined. – Jesper Aug 09 '17 at 14:01
  • I tried it out with Java 8 update 144 (currently the latest version) and it works as expected. – Jesper Aug 09 '17 at 14:03
  • java version "1.8.0_121" Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode) – dvelopp Aug 09 '17 at 14:04
  • Then update to the latest version. – Jesper Aug 09 '17 at 14:04
  • Is that OK to rely on Java version to parse time zones? What if prod servers uses different versions and update it there is rather difficult. They should have provided a more reliable way... – dvelopp Aug 09 '17 at 14:05
  • The 3-letter names (like EST or HST) are not supported by `ZoneId`, mainly because they are [ambiguous and not standard](https://stackoverflow.com/questions/18405262/what-are-the-standard-timezone-abbreviations/18407231#18407231) - although you can use the [`SHORT_IDS` map](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#SHORT_IDS), which has some default/retro-compatible/controversial mappings –  Aug 09 '17 at 14:05
  • 2
    You can update timezone data regardless of Java version: http://www.oracle.com/technetwork/java/javase/tzupdater-readme-136440.html –  Aug 09 '17 at 14:06
  • 1
    @Hugo, thanks, I like that way. It's more convenient than updating whole java. SHORT_IDS - good idea! – dvelopp Aug 09 '17 at 14:10
  • You can also use your own map of IDs if you want, and use it in the [`of(name, map)` method](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#of-java.lang.String-java.util.Map-) –  Aug 09 '17 at 14:19
  • @Hugo, thanks! What's about writing an answer to summarize all of this ? :) – dvelopp Aug 09 '17 at 14:50

1 Answers1

15

I've tested with Java 1.8.0_121 and some zones are really missing.

The most obvious way to fix it is to update Java's version - in Java 1.8.0_131 all the zones above are available - except the 3-letter names (EST, HST, etc), more on that below.

But I know that updates in production environments are not as easy (nor fast) as we'd like. In this case, you could use the TZUpdater tool, which can update JDK's timezone data without changing Java's version.


The only detail is that ZoneId doesn't work with the 3-letter abbreviations (EST, HST and so on). That's because those names are ambiguous and not standard.

If you want to use them, though, you can use a map of custom ID's. ZoneId comes with a built-in map:

ZoneId.of("EST", ZoneId.SHORT_IDS);

The problem is that the choices used in the SHORT_IDS map are - like any other choice - arbitrary and even controversial. If you want to use different zones for each abbreviation, just create your own map:

Map<String, String> map = new HashMap<>();
map.put("EST", "America/New_York");
... put how many names you want
System.out.println(ZoneId.of("EST", map)); // creates America/New_York

The only exceptions for 3-letter names are, of course, GMT and UTC, but in this case it's better to just use the ZoneOffset.UTC constant.


If you can't update your Java version nor run the TZUpdater tool, there's another (much more difficult) alternative.

You can extend the java.time.zone.ZoneRulesProvider class and make a provider that can create the missing ID's. Something like that:

public class MissingZonesProvider extends ZoneRulesProvider {

    private Set<String> missingIds = new HashSet<>();

    public MissingZonesProvider() {
        missingIds.add("America/Punta_Arenas");
        missingIds.add("Europe/Saratov");
        // add all others
    }

    @Override
    protected Set<String> provideZoneIds() {
        return this.missingIds;
    }

    @Override
    protected ZoneRules provideRules(String zoneId, boolean forCaching) {
        ZoneRules rules = null;
        if ("America/Punta_Arenas".equals(zoneId)) {
            rules = // create rules for America/Punta_Arenas
        }
        if ("Europe/Saratov".equals(zoneId)) {
            rules = // create rules for Europe/Saratov
        }
        // and so on

        return rules;
    }

    // returns a map with the ZoneRules, check javadoc for more details
    @Override
    protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
        TreeMap<String, ZoneRules> map = new TreeMap<>();
        ZoneRules rules = provideRules(zoneId, false);
        if (rules != null) {
            map.put(zoneId, rules);
        }
        return map;
    }
}

Create the ZoneRules is most the complicated part.

One way is to get the latest IANA files and read them. You can take a look at JDK source code to see how it creates ZoneRules from that (although I'm not sure if the file that's inside JDK is in the exact same format as IANA's files).

Anyway, this link explains how to read IANA's files. Then you can take a look at ZoneRules javadoc to know how to map IANA information to Java classes. In this answer I create a very simple ZoneRules with just 2 transition rules, so you can get a basic idea of how to do it.

Then you need to register the provider:

ZoneRulesProvider.registerProvider(new MissingZonesProvider());

And now the new zones will be available:

ZoneId.of("America/Punta_Arenas");
ZoneId.of("Europe/Saratov");
... and any other you added in the MissingZonesProvider class

There are other ways to use the provider (instead of registering), check the javadoc for more details. In the same javadoc there are also more details about how to properly implement a zone rules provider (my version above is very simple and probably it's missing some details, like the implementation of provideVersions - it should use the provider's version as a key, not the zone ID as I'm doing, etc).

Of course this provider must be discarded as soon as you update the Java version (because you can't have 2 providers that create zones with the same ID: if the new provider creates an ID that already exists, it throws an exception when you try to register it).