]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-52_1/main/classes/core/src/com/ibm/icu/impl/ZoneMeta.java
Upgrade ICU4J.
[Dictionary.git] / jars / icu4j-52_1 / main / classes / core / src / com / ibm / icu / impl / ZoneMeta.java
1 /*
2 **********************************************************************
3 * Copyright (c) 2003-2013 International Business Machines
4 * Corporation and others.  All Rights Reserved.
5 **********************************************************************
6 * Author: Alan Liu
7 * Created: September 4 2003
8 * Since: ICU 2.8
9 **********************************************************************
10 */
11 package com.ibm.icu.impl;
12
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;
18 import java.util.Set;
19 import java.util.TreeSet;
20
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;
27
28 /**
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
34  * by Olson links).
35  *
36  * This class uses a data class, ZoneMetaData, which is created by the
37  * tool tz2icu.
38  *
39  * @author Alan Liu
40  * @since ICU 2.8
41  */
42 public final class ZoneMeta {
43     private static final boolean ASSERT = false;
44
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";
49
50     private static final String kGMT_ID   = "GMT";
51     private static final String kCUSTOM_TZ_PREFIX = "GMT";
52
53     private static final String kWorld = "001";
54
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;
58
59     /**
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.
63      */
64     private static synchronized Set<String> getSystemZIDs() {
65         Set<String> systemZones = null;
66         if (REF_SYSTEM_ZONES != null) {
67             systemZones = REF_SYSTEM_ZONES.get();
68         }
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)) {
75                     continue;
76                 }
77                 systemIDs.add(id);
78             }
79             systemZones = Collections.unmodifiableSet(systemIDs);
80             REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones);
81         }
82         return systemZones;
83     }
84
85     /**
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.
90      */
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();
95         }
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)) {
102                     continue;
103                 }
104                 String canonicalID = getCanonicalCLDRID(id);
105                 if (id.equals(canonicalID)) {
106                     canonicalSystemIDs.add(id);
107                 }
108             }
109             canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
110             REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones);
111         }
112         return canonicalSystemZones;
113     }
114
115     /**
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.
122      */
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();
127         }
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)) {
134                     continue;
135                 }
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);
141                     }
142                 }
143             }
144             canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
145             REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones);
146         }
147         return canonicalSystemLocationZones;
148     }
149
150     /**
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.
156      */
157     public static Set<String> getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset) {
158         Set<String> baseSet = null;
159         switch (type) {
160         case ANY:
161             baseSet = getSystemZIDs();
162             break;
163         case CANONICAL:
164             baseSet = getCanonicalSystemZIDs();
165             break;
166         case CANONICAL_LOCATION:
167             baseSet = getCanonicalSystemLocationZIDs();
168             break;
169         default:
170             // never occur
171             throw new IllegalArgumentException("Unknown SystemTimeZoneType");
172         }
173
174         if (region == null && rawOffset == null) {
175             return baseSet;
176         }
177
178         if (region != null) {
179             region = region.toUpperCase(Locale.ENGLISH);
180         }
181
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)) {
188                     continue;
189                 }
190             }
191             if (rawOffset != null) {
192                 // This is VERY inefficient.
193                 TimeZone z = getSystemTimeZone(id);
194                 if (z == null || !rawOffset.equals(z.getRawOffset())) {
195                     continue;
196                 }
197             }
198             result.add(id);
199         }
200         if (result.isEmpty()) {
201             return Collections.emptySet();
202         }
203
204         return Collections.unmodifiableSet(result);
205     }
206
207     /**
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.
211      *
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
219      */
220     public static synchronized int countEquivalentIDs(String id) {
221         int count = 0;
222         UResourceBundle res = openOlsonResource(null, id);
223         if (res != null) {
224             try {
225                 UResourceBundle links = res.get("links");
226                 int[] v = links.getIntVector();
227                 count = v.length;
228             } catch (MissingResourceException ex) {
229                 // throw away
230             }
231         }
232         return count;
233     }
234
235     /**
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.
239      *
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
252      */
253     public static synchronized String getEquivalentID(String id, int index) {
254         String result = "";
255         if (index >= 0) {
256             UResourceBundle res = openOlsonResource(null, id);
257             if (res != null) {
258                 int zoneIdx = -1;
259                 try {
260                     UResourceBundle links = res.get("links");
261                     int[] zones = links.getIntVector();
262                     if (index < zones.length) {
263                         zoneIdx = zones[index];
264                     }
265                 } catch (MissingResourceException ex) {
266                     // throw away
267                 }
268                 if (zoneIdx >= 0) {
269                     String tmp = getZoneID(zoneIdx);
270                     if (tmp != null) {
271                         result = tmp;
272                     }
273                 }
274             }
275         }
276         return result;
277     }
278
279     private static String[] ZONEIDS = null;
280
281     /*
282      * ICU frequently refers the zone ID array in zoneinfo resource
283      */
284     private static synchronized String[] getZoneIDs() {
285         if (ZONEIDS == null) {
286             try {
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) {
292                 // throw away..
293             }
294         }
295         if (ZONEIDS == null) {
296             ZONEIDS = new String[0];
297         }
298         return ZONEIDS;
299     }
300
301     private static String getZoneID(int idx) {
302         if (idx >= 0) {
303             String[] ids = getZoneIDs();
304             if (idx < ids.length) {
305                 return ids[idx];
306             }
307         }
308         return null;
309     }
310
311     private static int getZoneIndex(String zid) {
312         int zoneIdx = -1;
313
314         String[] all = getZoneIDs();
315         if (all.length > 0) {
316             int start = 0;
317             int limit = all.length;
318
319             int lastMid = Integer.MAX_VALUE;
320             for (;;) {
321                 int mid = (start + limit) / 2;
322                 if (lastMid == mid) {   /* Have we moved? */
323                     break;  /* We haven't moved, and it wasn't found. */
324                 }
325                 lastMid = mid;
326                 int r = zid.compareTo(all[mid]);
327                 if (r == 0) {
328                     zoneIdx = mid;
329                     break;
330                 } else if(r < 0) {
331                     limit = mid;
332                 } else {
333                     start = mid;
334                 }
335             }
336         }
337
338         return zoneIdx;
339     }
340
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>();
344
345     public static String getCanonicalCLDRID(TimeZone tz) {
346         if (tz instanceof OlsonTimeZone) {
347             return ((OlsonTimeZone)tz).getCanonicalID();
348         }
349         return getCanonicalCLDRID(tz.getID());
350     }
351
352     /**
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.
355      * 
356      * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is
357      * NOT a system ID).
358      */
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
365                 try {
366                     int zoneIdx = getZoneIndex(tzid);
367                     if (zoneIdx >= 0) {
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);
376                         }
377                         if (canonical == null) {
378                             canonical = tzid;
379                         }
380                     }
381                 } catch (MissingResourceException e) {
382                     // fall through
383                 }
384             }
385             if (canonical != null) {
386                 CANONICAL_ID_CACHE.put(tzid, canonical);
387             }
388         }
389         return canonical;
390     }
391
392     private static String findCLDRCanonicalID(String tzid) {
393         String canonical = null;
394         String tzidKey = tzid.replace('/', ':');
395
396         try {
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");
402             try {
403                 /* UResourceBundle canonicalEntry = */ typeKeys.get(tzidKey);
404                 // The given tzid is available in the canonical list
405                 canonical = tzid;
406             } catch (MissingResourceException e) {
407                 // fall through
408             }
409             if (canonical == null) {
410                 // Try alias map
411                 UResourceBundle typeAlias = keyTypeData.get("typeAlias");
412                 UResourceBundle aliasesForKey = typeAlias.get("timezone");
413                 canonical = aliasesForKey.getString(tzidKey);
414             }
415         } catch (MissingResourceException e) {
416             // fall through
417         }
418         return canonical;
419     }
420
421     /**
422      * Return the region code for this tzid.
423      * If tzid is not a system zone ID, this method returns null.
424      */
425     public static String getRegion(String tzid) {
426         String region = REGION_CACHE.get(tzid);
427         if (region == null) {
428             int zoneIdx = getZoneIndex(tzid);
429             if (zoneIdx >= 0) {
430                 try {
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);
436                     }
437                 } catch (MissingResourceException e) {
438                     // throw away
439                 }
440                 if (region != null) {
441                     REGION_CACHE.put(tzid, region);
442                 }
443             }
444         }
445         return region;
446     }
447
448     /**
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.
451      */
452     public static String getCanonicalCountry(String tzid) {
453         String country = getRegion(tzid);
454         if (country != null && country.equals(kWorld)) {
455             country = null;
456         }
457         return country;
458     }
459
460     /**
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.
464      */
465     public static String getCanonicalCountry(String tzid, Output<Boolean> isPrimary) {
466         isPrimary.value = Boolean.FALSE;
467
468         String country = getRegion(tzid);
469         if (country != null && country.equals(kWorld)) {
470             return null;
471         }
472
473         // Check the cache
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);
480         }
481
482         if (singleZone) {
483             isPrimary.value = Boolean.TRUE;
484         } else {
485             // Note: We may cache the primary zone map in future.
486
487             // Even a country has multiple zones, one of them might be
488             // dominant and treated as a primary zone.
489             try {
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;
495                 } else {
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;
500                     }
501                 }
502             } catch (MissingResourceException e) {
503                 // ignore
504             }
505         }
506
507         return country;
508     }
509
510     /**
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.
515      * @param id zone id
516      * @return the corresponding zone resource or null if not found
517      */
518     public static UResourceBundle openOlsonResource(UResourceBundle top, String id)
519     {
520         UResourceBundle res = null;
521         int zoneIdx = getZoneIndex(id);
522         if (zoneIdx >= 0) {
523             try {
524                 if (top == null) {
525                     top = UResourceBundle.getBundleInstance(
526                             ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
527                 }
528                 UResourceBundle zones = top.get(kZONES);
529                 UResourceBundle zone = zones.get(zoneIdx);
530                 if (zone.getType() == UResourceBundle.INT) {
531                     // resolve link
532                     zone = zones.get(zone.getInt());
533                 }
534                 res = zone;
535             } catch (MissingResourceException e) {
536                 res = null;
537             }
538         }
539         return res;
540     }
541
542
543     /**
544      * System time zone object cache
545      */
546     private static class SystemTimeZoneCache extends SoftCache<String, OlsonTimeZone, String> {
547
548         /* (non-Javadoc)
549          * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
550          */
551         @Override
552         protected OlsonTimeZone createInstance(String key, String data) {
553             OlsonTimeZone tz = null;
554             try {
555                 UResourceBundle top = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
556                         ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
557                 UResourceBundle res = openOlsonResource(top, data);
558                 if (res != null) {
559                     tz = new OlsonTimeZone(top, res, data);
560                     tz.freeze();
561                 }
562             } catch (MissingResourceException e) {
563                 // do nothing
564             }
565             return tz;
566         }
567     }
568
569     private static final SystemTimeZoneCache SYSTEM_ZONE_CACHE = new SystemTimeZoneCache();
570
571     /**
572      * Returns a frozen OlsonTimeZone instance for the given ID.
573      * This method returns null when the given ID is unknown.
574      */
575     public static TimeZone getSystemTimeZone(String id) {
576         return SYSTEM_ZONE_CACHE.getInstance(id, id);
577     }
578
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;
583
584     /**
585      * Custom time zone object cache
586      */
587     private static class CustomTimeZoneCache extends SoftCache<Integer, SimpleTimeZone, int[]> {
588
589         /* (non-Javadoc)
590          * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
591          */
592         @Override
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);
602             tz.freeze();
603             return tz;
604         }
605     }
606
607     private static final CustomTimeZoneCache CUSTOM_ZONE_CACHE = new CustomTimeZoneCache();
608
609     /**
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
612      * GMT[+-]hh.
613      * @return a frozen SimpleTimeZone with the given offset and
614      * no Daylight Savings Time, or null if the id cannot be parsed.
615     */
616     public static TimeZone getCustomTimeZone(String id){
617         int[] fields = new int[4];
618         if (parseCustomID(id, fields)) {
619             // fields[0] - sign
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);
626         }
627         return null;
628     }
629
630     /**
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
634      * GMT[+-]hh.
635      * @return The normalized custom id string.
636     */
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);
641         }
642         return null;
643     }
644
645     /*
646      * Parses the given custom time zone identifier
647      * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
648      * GMT[+-]hh.
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
652      * set to fields[3].
653      * @return Returns true when the given custom id is valid.
654      */
655     static boolean parseCustomID(String id, int[] fields) {
656         NumberFormat numberFormat = null;
657
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());
661             int sign = 1;
662             int hour = 0;
663             int min = 0;
664             int sec = 0;
665
666             if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
667                 sign = -1;
668             } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
669                 return false;
670             }
671             pos.setIndex(pos.getIndex() + 1);
672
673             numberFormat = NumberFormat.getInstance();
674             numberFormat.setParseIntegerOnly(true);
675
676             // Look for either hh:mm, hhmm, or hh
677             int start = pos.getIndex();
678
679             Number n = numberFormat.parse(id, pos);
680             if (pos.getIndex() == start) {
681                 return false;
682             }
683             hour = n.intValue();
684
685             if (pos.getIndex() < id.length()){
686                 if (pos.getIndex() - start > 2
687                         || id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
688                     return false;
689                 }
690                 // hh:mm
691                 pos.setIndex(pos.getIndex() + 1);
692                 int oldPos = pos.getIndex();
693                 n = numberFormat.parse(id, pos);
694                 if ((pos.getIndex() - oldPos) != 2) {
695                     // must be 2 digits
696                     return false;
697                 }
698                 min = n.intValue();
699                 if (pos.getIndex() < id.length()) {
700                     if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
701                         return false;
702                     }
703                     // [:ss]
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) {
709                         return false;
710                     }
711                     sec = n.intValue();
712                 }
713             } else {
714                 // Supported formats are below -
715                 //
716                 // HHmmss
717                 // Hmmss
718                 // HHmm
719                 // Hmm
720                 // HH
721                 // H
722
723                 int length = pos.getIndex() - start;
724                 if (length <= 0 || 6 < length) {
725                     // invalid length
726                     return false;
727                 }
728                 switch (length) {
729                     case 1:
730                     case 2:
731                         // already set to hour
732                         break;
733                     case 3:
734                     case 4:
735                         min = hour % 100;
736                         hour /= 100;
737                         break;
738                     case 5:
739                     case 6:
740                         sec = hour % 100;
741                         min = (hour/100) % 100;
742                         hour /= 10000;
743                         break;
744                 }
745             }
746
747             if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
748                 if (fields != null) {
749                     if (fields.length >= 1) {
750                         fields[0] = sign;
751                     }
752                     if (fields.length >= 2) {
753                         fields[1] = hour;
754                     }
755                     if (fields.length >= 3) {
756                         fields[2] = min;
757                     }
758                     if (fields.length >= 4) {
759                         fields[3] = sec;
760                     }
761                 }
762                 return true;
763             }
764         }
765         return false;
766     }
767
768     /**
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
772      */
773     public static TimeZone getCustomTimeZone(int offset) {
774         boolean negative = false;
775         int tmp = offset;
776         if (offset < 0) {
777             negative = true;
778             tmp = -offset;
779         }
780
781         int hour, min, sec;
782
783         if (ASSERT) {
784             Assert.assrt("millis!=0", tmp % 1000 != 0);
785         }
786         tmp /= 1000;
787         sec = tmp % 60;
788         tmp /= 60;
789         min = tmp % 60;
790         hour = tmp / 60;
791
792         // Note: No millisecond part included in TZID for now
793         String zid = formatCustomID(hour, min, sec, negative);
794
795         return new SimpleTimeZone(offset, zid);
796     }
797
798     /*
799      * Returns the normalized custom TimeZone ID
800      */
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) {
805             if(negative) {
806                 zid.append('-');
807             } else {
808                 zid.append('+');
809             }
810             // Always use US-ASCII digits
811             if (hour < 10) {
812                 zid.append('0');
813             }
814             zid.append(hour);
815             zid.append(':');
816             if (min < 10) {
817                 zid.append('0');
818             }
819             zid.append(min);
820
821             if (sec != 0) {
822                 // Optional second field
823                 zid.append(':');
824                 if (sec < 10) {
825                     zid.append('0');
826                 }
827                 zid.append(sec);
828             }
829         }
830         return zid.toString();
831     }
832
833     /**
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.
838      */
839     public static String getShortID(TimeZone tz) {
840         String canonicalID = null;
841
842         if (tz instanceof OlsonTimeZone) {
843             canonicalID = ((OlsonTimeZone)tz).getCanonicalID();
844         }
845         canonicalID = getCanonicalCLDRID(tz.getID());
846         if (canonicalID == null) {
847             return null;
848         }
849         return getShortIDFromCanonical(canonicalID);
850     }
851
852     /**
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.
857      */
858     public static String getShortID(String id) {
859         String canonicalID = getCanonicalCLDRID(id);
860         if (canonicalID == null) {
861             return null;
862         }
863         return getShortIDFromCanonical(canonicalID);
864     }
865
866     private static String getShortIDFromCanonical(String canonicalID) {
867         String shortID = null;
868         String tzidKey = canonicalID.replace('/', ':');
869
870         try {
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) {
878             // fall through
879         }
880
881         return shortID;
882     }
883
884 }