2 **********************************************************************
3 * Copyright (c) 2003-2011 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.SimpleTimeZone;
23 import com.ibm.icu.util.TimeZone;
24 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
25 import com.ibm.icu.util.UResourceBundle;
28 * This class, not to be instantiated, implements the meta-data
29 * missing from the underlying core JDK implementation of time zones.
30 * There are two missing features: Obtaining a list of available zones
31 * for a given country (as defined by the Olson database), and
32 * obtaining a list of equivalent zones for a given zone (as defined
35 * This class uses a data class, ZoneMetaData, which is created by the
41 public final class ZoneMeta {
42 private static final boolean ASSERT = false;
44 private static final String ZONEINFORESNAME = "zoneinfo64";
45 private static final String kREGIONS = "Regions";
46 private static final String kZONES = "Zones";
47 private static final String kNAMES = "Names";
49 private static final String kGMT_ID = "GMT";
50 private static final String kCUSTOM_TZ_PREFIX = "GMT";
52 private static final String kWorld = "001";
54 private static SoftReference<Set<String>> REF_SYSTEM_ZONES;
55 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES;
56 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES;
59 * Returns an immutable set of system time zone IDs.
60 * Etc/Unknown is excluded.
61 * @return An immutable set of system time zone IDs.
63 private static synchronized Set<String> getSystemZIDs() {
64 Set<String> systemZones = null;
65 if (REF_SYSTEM_ZONES != null) {
66 systemZones = REF_SYSTEM_ZONES.get();
68 if (systemZones == null) {
69 Set<String> systemIDs = new TreeSet<String>();
70 String[] allIDs = getZoneIDs();
71 for (String id : allIDs) {
72 // exclude Etc/Unknown
73 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
78 systemZones = Collections.unmodifiableSet(systemIDs);
79 REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones);
85 * Returns an immutable set of canonical system time zone IDs.
86 * The result set is a subset of {@link #getSystemZIDs()}, but not
87 * including aliases, such as "US/Eastern".
88 * @return An immutable set of canonical system time zone IDs.
90 private static synchronized Set<String> getCanonicalSystemZIDs() {
91 Set<String> canonicalSystemZones = null;
92 if (REF_CANONICAL_SYSTEM_ZONES != null) {
93 canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get();
95 if (canonicalSystemZones == null) {
96 Set<String> canonicalSystemIDs = new TreeSet<String>();
97 String[] allIDs = getZoneIDs();
98 for (String id : allIDs) {
99 // exclude Etc/Unknown
100 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
103 String canonicalID = getCanonicalCLDRID(id);
104 if (id.equals(canonicalID)) {
105 canonicalSystemIDs.add(id);
108 canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
109 REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones);
111 return canonicalSystemZones;
115 * Returns an immutable set of canonical system time zone IDs that
116 * are associated with actual locations.
117 * The result set is a subset of {@link #getCanonicalSystemZIDs()}, but not
118 * including IDs, such as "Etc/GTM+5".
119 * @return An immutable set of canonical system time zone IDs that
120 * are associated with actual locations.
122 private static synchronized Set<String> getCanonicalSystemLocationZIDs() {
123 Set<String> canonicalSystemLocationZones = null;
124 if (REF_CANONICAL_SYSTEM_LOCATION_ZONES != null) {
125 canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get();
127 if (canonicalSystemLocationZones == null) {
128 Set<String> canonicalSystemLocationIDs = new TreeSet<String>();
129 String[] allIDs = getZoneIDs();
130 for (String id : allIDs) {
131 // exclude Etc/Unknown
132 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
135 String canonicalID = getCanonicalCLDRID(id);
136 if (id.equals(canonicalID)) {
137 String region = getRegion(id);
138 if (region != null && !region.equals(kWorld)) {
139 canonicalSystemLocationIDs.add(id);
143 canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
144 REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones);
146 return canonicalSystemLocationZones;
150 * Returns an immutable set of system IDs for the given conditions.
151 * @param type a system time zone type.
152 * @param region a region, or null.
153 * @param rawOffset a zone raw offset or null.
154 * @return An immutable set of system IDs for the given conditions.
156 public static Set<String> getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset) {
157 Set<String> baseSet = null;
160 baseSet = getSystemZIDs();
163 baseSet = getCanonicalSystemZIDs();
165 case CANONICAL_LOCATION:
166 baseSet = getCanonicalSystemLocationZIDs();
170 throw new IllegalArgumentException("Unknown SystemTimeZoneType");
173 if (region == null && rawOffset == null) {
177 if (region != null) {
178 region = region.toUpperCase(Locale.US);
181 // Filter by region/rawOffset
182 Set<String> result = new TreeSet<String>();
183 for (String id : baseSet) {
184 if (region != null) {
185 String r = getRegion(id);
186 if (!region.equals(r)) {
190 if (rawOffset != null) {
191 // This is VERY inefficient.
192 TimeZone z = getSystemTimeZone(id);
193 if (z == null || !rawOffset.equals(z.getRawOffset())) {
199 if (result.isEmpty()) {
200 return Collections.emptySet();
203 return Collections.unmodifiableSet(result);
207 * Returns the number of IDs in the equivalency group that
208 * includes the given ID. An equivalency group contains zones
209 * that behave identically to the given zone.
211 * <p>If there are no equivalent zones, then this method returns
212 * 0. This means either the given ID is not a valid zone, or it
213 * is and there are no other equivalent zones.
214 * @param id a system time zone ID
215 * @return the number of zones in the equivalency group containing
216 * 'id', or zero if there are no equivalent zones.
217 * @see #getEquivalentID
219 public static synchronized int countEquivalentIDs(String id) {
222 UResourceBundle res = openOlsonResource(null, id);
223 UResourceBundle links = res.get("links");
224 int[] v = links.getIntVector();
226 } catch (MissingResourceException ex) {
233 * Returns an ID in the equivalency group that includes the given
234 * ID. An equivalency group contains zones that behave
235 * identically to the given zone.
237 * <p>The given index must be in the range 0..n-1, where n is the
238 * value returned by <code>countEquivalentIDs(id)</code>. For
239 * some value of 'index', the returned value will be equal to the
240 * given id. If the given id is not a valid system time zone, or
241 * if 'index' is out of range, then returns an empty string.
242 * @param id a system time zone ID
243 * @param index a value from 0 to n-1, where n is the value
244 * returned by <code>countEquivalentIDs(id)</code>
245 * @return the ID of the index-th zone in the equivalency group
246 * containing 'id', or an empty string if 'id' is not a valid
247 * system ID or 'index' is out of range
248 * @see #countEquivalentIDs
250 public static synchronized String getEquivalentID(String id, int index) {
256 UResourceBundle res = openOlsonResource(null, id);
257 UResourceBundle links = res.get("links");
258 int[] zones = links.getIntVector();
259 if (index < zones.length) {
260 zoneIdx = zones[index];
262 } catch (MissingResourceException ex) {
268 String tmp = getZoneID(zoneIdx);
276 private static String[] ZONEIDS = null;
279 * ICU frequently refers the zone ID array in zoneinfo resource
281 private static synchronized String[] getZoneIDs() {
282 if (ZONEIDS == null) {
284 UResourceBundle top = UResourceBundle.getBundleInstance(
285 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
286 UResourceBundle names = top.get(kNAMES);
287 ZONEIDS = names.getStringArray();
288 } catch (MissingResourceException ex) {
292 if (ZONEIDS == null) {
293 ZONEIDS = new String[0];
298 private static String getZoneID(int idx) {
300 String[] ids = getZoneIDs();
301 if (idx < ids.length) {
308 private static int getZoneIndex(String zid) {
311 String[] all = getZoneIDs();
312 if (all.length > 0) {
314 int limit = all.length;
316 int lastMid = Integer.MAX_VALUE;
318 int mid = (start + limit) / 2;
319 if (lastMid == mid) { /* Have we moved? */
320 break; /* We haven't moved, and it wasn't found. */
323 int r = zid.compareTo(all[mid]);
338 private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<String, String>();
339 private static ICUCache<String, String> REGION_CACHE = new SimpleCache<String, String>();
340 private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<String, Boolean>();
342 public static String getCanonicalCLDRID(TimeZone tz) {
343 if (tz instanceof OlsonTimeZone) {
344 return ((OlsonTimeZone)tz).getCanonicalID();
346 return getCanonicalCLDRID(tz.getID());
350 * Return the canonical id for this tzid defined by CLDR, which might be
351 * the id itself. If the given tzid is not known, return null.
353 * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is
356 public static String getCanonicalCLDRID(String tzid) {
357 String canonical = CANONICAL_ID_CACHE.get(tzid);
358 if (canonical == null) {
359 int zoneIdx = getZoneIndex(tzid);
362 UResourceBundle top = UResourceBundle.getBundleInstance(
363 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
364 UResourceBundle zones = top.get(kZONES);
365 UResourceBundle zone = zones.get(zoneIdx);
366 if (zone.getType() == UResourceBundle.INT) {
368 String tmp = getZoneID(zone.getInt());
375 // check canonical mapping in CLDR
376 UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(
377 ICUResourceBundle.ICU_BASE_NAME, "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
378 UResourceBundle typeAlias = keyTypeData.get("typeAlias");
379 UResourceBundle aliasesForKey = typeAlias.get("timezone");
380 String cldrCanonical = aliasesForKey.getString(canonical.replace('/', ':'));
381 if (cldrCanonical != null) {
382 canonical = cldrCanonical;
384 } catch (MissingResourceException e) {
388 if (canonical != null) {
389 CANONICAL_ID_CACHE.put(tzid, canonical);
396 * Return the region code for this tzid.
397 * If tzid is not a system zone ID, this method returns null.
399 public static String getRegion(String tzid) {
400 String region = REGION_CACHE.get(tzid);
401 if (region == null) {
402 int zoneIdx = getZoneIndex(tzid);
405 UResourceBundle top = UResourceBundle.getBundleInstance(
406 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
407 UResourceBundle regions = top.get(kREGIONS);
408 if (zoneIdx < regions.getSize()) {
409 region = regions.getString(zoneIdx);
411 } catch (MissingResourceException e) {
414 if (region != null) {
415 REGION_CACHE.put(tzid, region);
423 * Return the canonical country code for this tzid. If we have none, or if the time zone
424 * is not associated with a country or unknown, return null.
426 public static String getCanonicalCountry(String tzid) {
427 String country = getRegion(tzid);
428 if (country != null && country.equals(kWorld)) {
435 * Return the country code if this is a 'single' time zone that can fallback to just
436 * the country, otherwise return null. (Note, one must also check the locale data
437 * to see that there is a localization for the country in order to implement
438 * tr#35 appendix J step 5.)
440 public static String getSingleCountry(String tzid) {
441 String country = getCanonicalCountry(tzid);
442 if (country != null) {
443 Boolean isSingle = SINGLE_COUNTRY_CACHE.get(tzid);
444 if (isSingle == null) {
445 Set<String> ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null);
446 assert(ids.size() >= 1);
447 isSingle = Boolean.valueOf(ids.size() <= 1);
448 SINGLE_COUNTRY_CACHE.put(tzid, isSingle);
458 * Given an ID and the top-level resource of the zoneinfo resource,
459 * open the appropriate resource for the given time zone.
460 * Dereference links if necessary.
461 * @param top the top level resource of the zoneinfo resource or null.
463 * @return the corresponding zone resource or null if not found
465 public static UResourceBundle openOlsonResource(UResourceBundle top, String id)
467 UResourceBundle res = null;
468 int zoneIdx = getZoneIndex(id);
472 top = UResourceBundle.getBundleInstance(
473 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
475 UResourceBundle zones = top.get(kZONES);
476 UResourceBundle zone = zones.get(zoneIdx);
477 if (zone.getType() == UResourceBundle.INT) {
479 zone = zones.get(zone.getInt());
482 } catch (MissingResourceException e) {
490 private static ICUCache<String, TimeZone> SYSTEM_ZONE_CACHE = new SimpleCache<String, TimeZone>();
493 * Lookup the given name in our system zone table. If found,
494 * instantiate a new zone of that name and return it. If not
497 public static TimeZone getSystemTimeZone(String id) {
498 TimeZone z = SYSTEM_ZONE_CACHE.get(id);
501 UResourceBundle top = UResourceBundle.getBundleInstance(
502 ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
503 UResourceBundle res = openOlsonResource(top, id);
504 z = new OlsonTimeZone(top, res, id);
505 SYSTEM_ZONE_CACHE.put(id, z);
506 }catch(Exception ex){
510 return (TimeZone)z.clone();
513 // Maximum value of valid custom time zone hour/min
514 private static final int kMAX_CUSTOM_HOUR = 23;
515 private static final int kMAX_CUSTOM_MIN = 59;
516 private static final int kMAX_CUSTOM_SEC = 59;
519 * Parse a custom time zone identifier and return a corresponding zone.
520 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
522 * @return a newly created SimpleTimeZone with the given offset and
523 * no Daylight Savings Time, or null if the id cannot be parsed.
525 public static TimeZone getCustomTimeZone(String id){
526 int[] fields = new int[4];
527 if (parseCustomID(id, fields)) {
528 String zid = formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
529 int offset = fields[0] * ((fields[1] * 60 + fields[2]) * 60 + fields[3]) * 1000;
530 return new SimpleTimeZone(offset, zid);
536 * Parse a custom time zone identifier and return the normalized
537 * custom time zone identifier for the given custom id string.
538 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
540 * @return The normalized custom id string.
542 public static String getCustomID(String id) {
543 int[] fields = new int[4];
544 if (parseCustomID(id, fields)) {
545 return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
551 * Parses the given custom time zone identifier
552 * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
554 * @param fields An array of int (length = 4) to receive the parsed
555 * offset time fields. The sign is set to fields[0] (-1 or 1),
556 * hour is set to fields[1], minute is set to fields[2] and second is
558 * @return Returns true when the given custom id is valid.
560 static boolean parseCustomID(String id, int[] fields) {
561 NumberFormat numberFormat = null;
562 String idUppercase = id.toUpperCase();
564 if (id != null && id.length() > kGMT_ID.length() &&
565 idUppercase.startsWith(kGMT_ID)) {
566 ParsePosition pos = new ParsePosition(kGMT_ID.length());
572 if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
574 } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
577 pos.setIndex(pos.getIndex() + 1);
579 numberFormat = NumberFormat.getInstance();
580 numberFormat.setParseIntegerOnly(true);
582 // Look for either hh:mm, hhmm, or hh
583 int start = pos.getIndex();
585 Number n = numberFormat.parse(id, pos);
586 if (pos.getIndex() == start) {
591 if (pos.getIndex() < id.length()){
592 if (pos.getIndex() - start > 2
593 || id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
597 pos.setIndex(pos.getIndex() + 1);
598 int oldPos = pos.getIndex();
599 n = numberFormat.parse(id, pos);
600 if ((pos.getIndex() - oldPos) != 2) {
605 if (pos.getIndex() < id.length()) {
606 if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
610 pos.setIndex(pos.getIndex() + 1);
611 oldPos = pos.getIndex();
612 n = numberFormat.parse(id, pos);
613 if (pos.getIndex() != id.length()
614 || (pos.getIndex() - oldPos) != 2) {
620 // Supported formats are below -
629 int length = pos.getIndex() - start;
630 if (length <= 0 || 6 < length) {
637 // already set to hour
647 min = (hour/100) % 100;
653 if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
654 if (fields != null) {
655 if (fields.length >= 1) {
658 if (fields.length >= 2) {
661 if (fields.length >= 3) {
664 if (fields.length >= 4) {
675 * Creates a custom zone for the offset
676 * @param offset GMT offset in milliseconds
677 * @return A custom TimeZone for the offset with normalized time zone id
679 public static TimeZone getCustomTimeZone(int offset) {
680 boolean negative = false;
687 int hour, min, sec, millis;
691 Assert.assrt("millis!=0", millis != 0);
699 // Note: No millisecond part included in TZID for now
700 String zid = formatCustomID(hour, min, sec, negative);
702 return new SimpleTimeZone(offset, zid);
706 * Returns the normalized custom TimeZone ID
708 static String formatCustomID(int hour, int min, int sec, boolean negative) {
709 // Create normalized time zone ID - GMT[+|-]hh:mm[:ss]
710 StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX);
711 if (hour != 0 || min != 0) {
717 // Always use US-ASCII digits
729 // Optional second field
737 return zid.toString();