2 **********************************************************************
3 * Copyright (c) 2003-2013 International Business Machines
4 * Corporation and others. All Rights Reserved.
5 **********************************************************************
7 * Created: September 4 2003
9 **********************************************************************
11 package com.ibm.icu.impl;
13 import java.lang.ref.SoftReference;
14 import java.text.ParsePosition;
15 import java.util.Collections;
16 import java.util.Locale;
17 import java.util.MissingResourceException;
19 import java.util.TreeSet;
21 import com.ibm.icu.text.NumberFormat;
22 import com.ibm.icu.util.Output;
23 import com.ibm.icu.util.SimpleTimeZone;
24 import com.ibm.icu.util.TimeZone;
25 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
26 import com.ibm.icu.util.UResourceBundle;
29 * This class, not to be instantiated, implements the meta-data
30 * missing from the underlying core JDK implementation of time zones.
31 * There are two missing features: Obtaining a list of available zones
32 * for a given country (as defined by the Olson database), and
33 * obtaining a list of equivalent zones for a given zone (as defined
36 * This class uses a data class, ZoneMetaData, which is created by the
42 public final class ZoneMeta {
43 private static final boolean ASSERT = false;
45 private static final String ZONEINFORESNAME = "zoneinfo64";
46 private static final String kREGIONS = "Regions";
47 private static final String kZONES = "Zones";
48 private static final String kNAMES = "Names";
50 private static final String kGMT_ID = "GMT";
51 private static final String kCUSTOM_TZ_PREFIX = "GMT";
53 private static final String kWorld = "001";
55 private static SoftReference<Set<String>> REF_SYSTEM_ZONES;
56 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES;
57 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES;
60 * Returns an immutable set of system time zone IDs.
61 * Etc/Unknown is excluded.
62 * @return An immutable set of system time zone IDs.
64 private static synchronized Set<String> getSystemZIDs() {
65 Set<String> systemZones = null;
66 if (REF_SYSTEM_ZONES != null) {
67 systemZones = REF_SYSTEM_ZONES.get();
69 if (systemZones == null) {
70 Set<String> systemIDs = new TreeSet<String>();
71 String[] allIDs = getZoneIDs();
72 for (String id : allIDs) {
73 // exclude Etc/Unknown
74 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
79 systemZones = Collections.unmodifiableSet(systemIDs);
80 REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones);
86 * Returns an immutable set of canonical system time zone IDs.
87 * The result set is a subset of {@link #getSystemZIDs()}, but not
88 * including aliases, such as "US/Eastern".
89 * @return An immutable set of canonical system time zone IDs.
91 private static synchronized Set<String> getCanonicalSystemZIDs() {
92 Set<String> canonicalSystemZones = null;
93 if (REF_CANONICAL_SYSTEM_ZONES != null) {
94 canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get();
96 if (canonicalSystemZones == null) {
97 Set<String> canonicalSystemIDs = new TreeSet<String>();
98 String[] allIDs = getZoneIDs();
99 for (String id : allIDs) {
100 // exclude Etc/Unknown
101 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
104 String canonicalID = getCanonicalCLDRID(id);
105 if (id.equals(canonicalID)) {
106 canonicalSystemIDs.add(id);
109 canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
110 REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones);
112 return canonicalSystemZones;
116 * Returns an immutable set of canonical system time zone IDs that
117 * are associated with actual locations.
118 * The result set is a subset of {@link #getCanonicalSystemZIDs()}, but not
119 * including IDs, such as "Etc/GTM+5".
120 * @return An immutable set of canonical system time zone IDs that
121 * are associated with actual locations.
123 private static synchronized Set<String> getCanonicalSystemLocationZIDs() {
124 Set<String> canonicalSystemLocationZones = null;
125 if (REF_CANONICAL_SYSTEM_LOCATION_ZONES != null) {
126 canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get();
128 if (canonicalSystemLocationZones == null) {
129 Set<String> canonicalSystemLocationIDs = new TreeSet<String>();
130 String[] allIDs = getZoneIDs();
131 for (String id : allIDs) {
132 // exclude Etc/Unknown
133 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
136 String canonicalID = getCanonicalCLDRID(id);
137 if (id.equals(canonicalID)) {
138 String region = getRegion(id);
139 if (region != null && !region.equals(kWorld)) {
140 canonicalSystemLocationIDs.add(id);
144 canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
145 REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones);
147 return canonicalSystemLocationZones;
151 * Returns an immutable set of system IDs for the given conditions.
152 * @param type a system time zone type.
153 * @param region a region, or null.
154 * @param rawOffset a zone raw offset or null.
155 * @return An immutable set of system IDs for the given conditions.
157 public static Set<String> getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset) {
158 Set<String> baseSet = null;
161 baseSet = getSystemZIDs();
164 baseSet = getCanonicalSystemZIDs();
166 case CANONICAL_LOCATION:
167 baseSet = getCanonicalSystemLocationZIDs();
171 throw new IllegalArgumentException("Unknown SystemTimeZoneType");
174 if (region == null && rawOffset == null) {
178 if (region != null) {
179 region = region.toUpperCase(Locale.ENGLISH);
182 // Filter by region/rawOffset
183 Set<String> result = new TreeSet<String>();
184 for (String id : baseSet) {
185 if (region != null) {
186 String r = getRegion(id);
187 if (!region.equals(r)) {
191 if (rawOffset != null) {
192 // This is VERY inefficient.
193 TimeZone z = getSystemTimeZone(id);
194 if (z == null || !rawOffset.equals(z.getRawOffset())) {
200 if (result.isEmpty()) {
201 return Collections.emptySet();
204 return Collections.unmodifiableSet(result);
208 * Returns the number of IDs in the equivalency group that
209 * includes the given ID. An equivalency group contains zones
210 * that behave identically to the given zone.
212 * <p>If there are no equivalent zones, then this method returns
213 * 0. This means either the given ID is not a valid zone, or it
214 * is and there are no other equivalent zones.
215 * @param id a system time zone ID
216 * @return the number of zones in the equivalency group containing
217 * 'id', or zero if there are no equivalent zones.
218 * @see #getEquivalentID
220 public static synchronized int countEquivalentIDs(String id) {
222 UResourceBundle res = openOlsonResource(null, id);
225 UResourceBundle links = res.get("links");
226 int[] v = links.getIntVector();
228 } catch (MissingResourceException ex) {
236 * Returns an ID in the equivalency group that includes the given
237 * ID. An equivalency group contains zones that behave
238 * identically to the given zone.
240 * <p>The given index must be in the range 0..n-1, where n is the
241 * value returned by <code>countEquivalentIDs(id)</code>. For
242 * some value of 'index', the returned value will be equal to the
243 * given id. If the given id is not a valid system time zone, or
244 * if 'index' is out of range, then returns an empty string.
245 * @param id a system time zone ID
246 * @param index a value from 0 to n-1, where n is the value
247 * returned by <code>countEquivalentIDs(id)</code>
248 * @return the ID of the index-th zone in the equivalency group
249 * containing 'id', or an empty string if 'id' is not a valid
250 * system ID or 'index' is out of range
251 * @see #countEquivalentIDs
253 public static synchronized String getEquivalentID(String id, int index) {
256 UResourceBundle res = openOlsonResource(null, id);
260 UResourceBundle links = res.get("links");
261 int[] zones = links.getIntVector();
262 if (index < zones.length) {
263 zoneIdx = zones[index];
265 } catch (MissingResourceException ex) {
269 String tmp = getZoneID(zoneIdx);
279 private static String[] ZONEIDS = null;
282 * ICU frequently refers the zone ID array in zoneinfo resource
284 private static synchronized String[] getZoneIDs() {
285 if (ZONEIDS == null) {
287 UResourceBundle top = UResourceBundle.getBundleInstance(
288 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
289 UResourceBundle names = top.get(kNAMES);
290 ZONEIDS = names.getStringArray();
291 } catch (MissingResourceException ex) {
295 if (ZONEIDS == null) {
296 ZONEIDS = new String[0];
301 private static String getZoneID(int idx) {
303 String[] ids = getZoneIDs();
304 if (idx < ids.length) {
311 private static int getZoneIndex(String zid) {
314 String[] all = getZoneIDs();
315 if (all.length > 0) {
317 int limit = all.length;
319 int lastMid = Integer.MAX_VALUE;
321 int mid = (start + limit) / 2;
322 if (lastMid == mid) { /* Have we moved? */
323 break; /* We haven't moved, and it wasn't found. */
326 int r = zid.compareTo(all[mid]);
341 private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<String, String>();
342 private static ICUCache<String, String> REGION_CACHE = new SimpleCache<String, String>();
343 private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<String, Boolean>();
345 public static String getCanonicalCLDRID(TimeZone tz) {
346 if (tz instanceof OlsonTimeZone) {
347 return ((OlsonTimeZone)tz).getCanonicalID();
349 return getCanonicalCLDRID(tz.getID());
353 * Return the canonical id for this tzid defined by CLDR, which might be
354 * the id itself. If the given tzid is not known, return null.
356 * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is
359 public static String getCanonicalCLDRID(String tzid) {
360 String canonical = CANONICAL_ID_CACHE.get(tzid);
361 if (canonical == null) {
362 canonical = findCLDRCanonicalID(tzid);
363 if (canonical == null) {
364 // Resolve Olson link and try it again if necessary
366 int zoneIdx = getZoneIndex(tzid);
368 UResourceBundle top = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
369 ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
370 UResourceBundle zones = top.get(kZONES);
371 UResourceBundle zone = zones.get(zoneIdx);
372 if (zone.getType() == UResourceBundle.INT) {
373 // It's a link - resolve link and lookup
374 tzid = getZoneID(zone.getInt());
375 canonical = findCLDRCanonicalID(tzid);
377 if (canonical == null) {
381 } catch (MissingResourceException e) {
385 if (canonical != null) {
386 CANONICAL_ID_CACHE.put(tzid, canonical);
392 private static String findCLDRCanonicalID(String tzid) {
393 String canonical = null;
394 String tzidKey = tzid.replace('/', ':');
397 // First, try check if the given ID is canonical
398 UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
399 "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
400 UResourceBundle typeMap = keyTypeData.get("typeMap");
401 UResourceBundle typeKeys = typeMap.get("timezone");
403 /* UResourceBundle canonicalEntry = */ typeKeys.get(tzidKey);
404 // The given tzid is available in the canonical list
406 } catch (MissingResourceException e) {
409 if (canonical == null) {
411 UResourceBundle typeAlias = keyTypeData.get("typeAlias");
412 UResourceBundle aliasesForKey = typeAlias.get("timezone");
413 canonical = aliasesForKey.getString(tzidKey);
415 } catch (MissingResourceException e) {
422 * Return the region code for this tzid.
423 * If tzid is not a system zone ID, this method returns null.
425 public static String getRegion(String tzid) {
426 String region = REGION_CACHE.get(tzid);
427 if (region == null) {
428 int zoneIdx = getZoneIndex(tzid);
431 UResourceBundle top = UResourceBundle.getBundleInstance(
432 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
433 UResourceBundle regions = top.get(kREGIONS);
434 if (zoneIdx < regions.getSize()) {
435 region = regions.getString(zoneIdx);
437 } catch (MissingResourceException e) {
440 if (region != null) {
441 REGION_CACHE.put(tzid, region);
449 * Return the canonical country code for this tzid. If we have none, or if the time zone
450 * is not associated with a country or unknown, return null.
452 public static String getCanonicalCountry(String tzid) {
453 String country = getRegion(tzid);
454 if (country != null && country.equals(kWorld)) {
461 * Return the canonical country code for this tzid. If we have none, or if the time zone
462 * is not associated with a country or unknown, return null. When the given zone is the
463 * primary zone of the country, true is set to isPrimary.
465 public static String getCanonicalCountry(String tzid, Output<Boolean> isPrimary) {
466 isPrimary.value = Boolean.FALSE;
468 String country = getRegion(tzid);
469 if (country != null && country.equals(kWorld)) {
474 Boolean singleZone = SINGLE_COUNTRY_CACHE.get(tzid);
475 if (singleZone == null) {
476 Set<String> ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null);
477 assert(ids.size() >= 1);
478 singleZone = Boolean.valueOf(ids.size() <= 1);
479 SINGLE_COUNTRY_CACHE.put(tzid, singleZone);
483 isPrimary.value = Boolean.TRUE;
485 // Note: We may cache the primary zone map in future.
487 // Even a country has multiple zones, one of them might be
488 // dominant and treated as a primary zone.
490 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "metaZones");
491 UResourceBundle primaryZones = bundle.get("primaryZones");
492 String primaryZone = primaryZones.getString(country);
493 if (tzid.equals(primaryZone)) {
494 isPrimary.value = Boolean.TRUE;
496 // The given ID might not be a canonical ID
497 String canonicalID = getCanonicalCLDRID(tzid);
498 if (canonicalID != null && canonicalID.equals(primaryZone)) {
499 isPrimary.value = Boolean.TRUE;
502 } catch (MissingResourceException e) {
511 * Given an ID and the top-level resource of the zoneinfo resource,
512 * open the appropriate resource for the given time zone.
513 * Dereference links if necessary.
514 * @param top the top level resource of the zoneinfo resource or null.
516 * @return the corresponding zone resource or null if not found
518 public static UResourceBundle openOlsonResource(UResourceBundle top, String id)
520 UResourceBundle res = null;
521 int zoneIdx = getZoneIndex(id);
525 top = UResourceBundle.getBundleInstance(
526 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
528 UResourceBundle zones = top.get(kZONES);
529 UResourceBundle zone = zones.get(zoneIdx);
530 if (zone.getType() == UResourceBundle.INT) {
532 zone = zones.get(zone.getInt());
535 } catch (MissingResourceException e) {
544 * System time zone object cache
546 private static class SystemTimeZoneCache extends SoftCache<String, OlsonTimeZone, String> {
549 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
552 protected OlsonTimeZone createInstance(String key, String data) {
553 OlsonTimeZone tz = null;
555 UResourceBundle top = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
556 ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
557 UResourceBundle res = openOlsonResource(top, data);
559 tz = new OlsonTimeZone(top, res, data);
562 } catch (MissingResourceException e) {
569 private static final SystemTimeZoneCache SYSTEM_ZONE_CACHE = new SystemTimeZoneCache();
572 * Returns a frozen OlsonTimeZone instance for the given ID.
573 * This method returns null when the given ID is unknown.
575 public static TimeZone getSystemTimeZone(String id) {
576 return SYSTEM_ZONE_CACHE.getInstance(id, id);
579 // Maximum value of valid custom time zone hour/min
580 private static final int kMAX_CUSTOM_HOUR = 23;
581 private static final int kMAX_CUSTOM_MIN = 59;
582 private static final int kMAX_CUSTOM_SEC = 59;
585 * Custom time zone object cache
587 private static class CustomTimeZoneCache extends SoftCache<Integer, SimpleTimeZone, int[]> {
590 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
593 protected SimpleTimeZone createInstance(Integer key, int[] data) {
594 assert (data.length == 4);
595 assert (data[0] == 1 || data[0] == -1);
596 assert (data[1] >= 0 && data[1] <= kMAX_CUSTOM_HOUR);
597 assert (data[2] >= 0 && data[2] <= kMAX_CUSTOM_MIN);
598 assert (data[3] >= 0 && data[3] <= kMAX_CUSTOM_SEC);
599 String id = formatCustomID(data[1], data[2], data[3], data[0] < 0);
600 int offset = data[0] * ((data[1] * 60 + data[2]) * 60 + data[3]) * 1000;
601 SimpleTimeZone tz = new SimpleTimeZone(offset, id);
607 private static final CustomTimeZoneCache CUSTOM_ZONE_CACHE = new CustomTimeZoneCache();
610 * Parse a custom time zone identifier and return a corresponding zone.
611 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
613 * @return a frozen SimpleTimeZone with the given offset and
614 * no Daylight Savings Time, or null if the id cannot be parsed.
616 public static TimeZone getCustomTimeZone(String id){
617 int[] fields = new int[4];
618 if (parseCustomID(id, fields)) {
620 // fields[1] - hour / 5-bit
621 // fields[2] - min / 6-bit
622 // fields[3] - sec / 6-bit
623 Integer key = Integer.valueOf(
624 fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11));
625 return CUSTOM_ZONE_CACHE.getInstance(key, fields);
631 * Parse a custom time zone identifier and return the normalized
632 * custom time zone identifier for the given custom id string.
633 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
635 * @return The normalized custom id string.
637 public static String getCustomID(String id) {
638 int[] fields = new int[4];
639 if (parseCustomID(id, fields)) {
640 return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
646 * Parses the given custom time zone identifier
647 * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
649 * @param fields An array of int (length = 4) to receive the parsed
650 * offset time fields. The sign is set to fields[0] (-1 or 1),
651 * hour is set to fields[1], minute is set to fields[2] and second is
653 * @return Returns true when the given custom id is valid.
655 static boolean parseCustomID(String id, int[] fields) {
656 NumberFormat numberFormat = null;
658 if (id != null && id.length() > kGMT_ID.length() &&
659 id.toUpperCase(Locale.ENGLISH).startsWith(kGMT_ID)) {
660 ParsePosition pos = new ParsePosition(kGMT_ID.length());
666 if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
668 } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
671 pos.setIndex(pos.getIndex() + 1);
673 numberFormat = NumberFormat.getInstance();
674 numberFormat.setParseIntegerOnly(true);
676 // Look for either hh:mm, hhmm, or hh
677 int start = pos.getIndex();
679 Number n = numberFormat.parse(id, pos);
680 if (pos.getIndex() == start) {
685 if (pos.getIndex() < id.length()){
686 if (pos.getIndex() - start > 2
687 || id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
691 pos.setIndex(pos.getIndex() + 1);
692 int oldPos = pos.getIndex();
693 n = numberFormat.parse(id, pos);
694 if ((pos.getIndex() - oldPos) != 2) {
699 if (pos.getIndex() < id.length()) {
700 if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
704 pos.setIndex(pos.getIndex() + 1);
705 oldPos = pos.getIndex();
706 n = numberFormat.parse(id, pos);
707 if (pos.getIndex() != id.length()
708 || (pos.getIndex() - oldPos) != 2) {
714 // Supported formats are below -
723 int length = pos.getIndex() - start;
724 if (length <= 0 || 6 < length) {
731 // already set to hour
741 min = (hour/100) % 100;
747 if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
748 if (fields != null) {
749 if (fields.length >= 1) {
752 if (fields.length >= 2) {
755 if (fields.length >= 3) {
758 if (fields.length >= 4) {
769 * Creates a custom zone for the offset
770 * @param offset GMT offset in milliseconds
771 * @return A custom TimeZone for the offset with normalized time zone id
773 public static TimeZone getCustomTimeZone(int offset) {
774 boolean negative = false;
784 Assert.assrt("millis!=0", tmp % 1000 != 0);
792 // Note: No millisecond part included in TZID for now
793 String zid = formatCustomID(hour, min, sec, negative);
795 return new SimpleTimeZone(offset, zid);
799 * Returns the normalized custom TimeZone ID
801 static String formatCustomID(int hour, int min, int sec, boolean negative) {
802 // Create normalized time zone ID - GMT[+|-]hh:mm[:ss]
803 StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX);
804 if (hour != 0 || min != 0) {
810 // Always use US-ASCII digits
822 // Optional second field
830 return zid.toString();
834 * Returns the time zone's short ID for the zone.
835 * For example, "uslax" for zone "America/Los_Angeles".
836 * @param tz the time zone
837 * @return the short ID of the time zone, or null if the short ID is not available.
839 public static String getShortID(TimeZone tz) {
840 String canonicalID = null;
842 if (tz instanceof OlsonTimeZone) {
843 canonicalID = ((OlsonTimeZone)tz).getCanonicalID();
845 canonicalID = getCanonicalCLDRID(tz.getID());
846 if (canonicalID == null) {
849 return getShortIDFromCanonical(canonicalID);
853 * Returns the time zone's short ID for the zone ID.
854 * For example, "uslax" for zone ID "America/Los_Angeles".
855 * @param id the time zone ID
856 * @return the short ID of the time zone ID, or null if the short ID is not available.
858 public static String getShortID(String id) {
859 String canonicalID = getCanonicalCLDRID(id);
860 if (canonicalID == null) {
863 return getShortIDFromCanonical(canonicalID);
866 private static String getShortIDFromCanonical(String canonicalID) {
867 String shortID = null;
868 String tzidKey = canonicalID.replace('/', ':');
871 // First, try check if the given ID is canonical
872 UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
873 "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
874 UResourceBundle typeMap = keyTypeData.get("typeMap");
875 UResourceBundle typeKeys = typeMap.get("timezone");
876 shortID = typeKeys.getString(tzidKey);
877 } catch (MissingResourceException e) {