1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4 **********************************************************************
5 * Copyright (c) 2003-2016 International Business Machines
6 * Corporation and others.  All Rights Reserved.
7 **********************************************************************
8 * Author: Alan Liu
9 * Created: September 4 2003
10 * Since: ICU 2.8
11 **********************************************************************
12 */
13 package com.ibm.icu.impl;
14 
15 import java.lang.ref.SoftReference;
16 import java.text.ParsePosition;
17 import java.util.Collections;
18 import java.util.Locale;
19 import java.util.MissingResourceException;
20 import java.util.Set;
21 import java.util.TreeSet;
22 
23 import com.ibm.icu.text.NumberFormat;
24 import com.ibm.icu.util.Output;
25 import com.ibm.icu.util.SimpleTimeZone;
26 import com.ibm.icu.util.TimeZone;
27 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
28 import com.ibm.icu.util.UResourceBundle;
29 
30 /**
31  * This class, not to be instantiated, implements the meta-data
32  * missing from the underlying core JDK implementation of time zones.
33  * There are two missing features: Obtaining a list of available zones
34  * for a given country (as defined by the Olson database), and
35  * obtaining a list of equivalent zones for a given zone (as defined
36  * by Olson links).
37  *
38  * This class uses a data class, ZoneMetaData, which is created by the
39  * tool tz2icu.
40  *
41  * @author Alan Liu
42  * @since ICU 2.8
43  */
44 public final class ZoneMeta {
45     private static final boolean ASSERT = false;
46 
47     private static final String ZONEINFORESNAME = "zoneinfo64";
48     private static final String kREGIONS  = "Regions";
49     private static final String kZONES    = "Zones";
50     private static final String kNAMES    = "Names";
51 
52     private static final String kGMT_ID   = "GMT";
53     private static final String kCUSTOM_TZ_PREFIX = "GMT";
54 
55     private static final String kWorld = "001";
56 
57     private static SoftReference<Set<String>> REF_SYSTEM_ZONES;
58     private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES;
59     private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES;
60 
61     /**
62      * Returns an immutable set of system time zone IDs.
63      * Etc/Unknown is excluded.
64      * @return An immutable set of system time zone IDs.
65      */
getSystemZIDs()66     private static synchronized Set<String> getSystemZIDs() {
67         Set<String> systemZones = null;
68         if (REF_SYSTEM_ZONES != null) {
69             systemZones = REF_SYSTEM_ZONES.get();
70         }
71         if (systemZones == null) {
72             Set<String> systemIDs = new TreeSet<String>();
73             String[] allIDs = getZoneIDs();
74             for (String id : allIDs) {
75                 // exclude Etc/Unknown
76                 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
77                     continue;
78                 }
79                 systemIDs.add(id);
80             }
81             systemZones = Collections.unmodifiableSet(systemIDs);
82             REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones);
83         }
84         return systemZones;
85     }
86 
87     /**
88      * Returns an immutable set of canonical system time zone IDs.
89      * The result set is a subset of {@link #getSystemZIDs()}, but not
90      * including aliases, such as "US/Eastern".
91      * @return An immutable set of canonical system time zone IDs.
92      */
getCanonicalSystemZIDs()93     private static synchronized Set<String> getCanonicalSystemZIDs() {
94         Set<String> canonicalSystemZones = null;
95         if (REF_CANONICAL_SYSTEM_ZONES != null) {
96             canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get();
97         }
98         if (canonicalSystemZones == null) {
99             Set<String> canonicalSystemIDs = new TreeSet<String>();
100             String[] allIDs = getZoneIDs();
101             for (String id : allIDs) {
102                 // exclude Etc/Unknown
103                 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
104                     continue;
105                 }
106                 String canonicalID = getCanonicalCLDRID(id);
107                 if (id.equals(canonicalID)) {
108                     canonicalSystemIDs.add(id);
109                 }
110             }
111             canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
112             REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones);
113         }
114         return canonicalSystemZones;
115     }
116 
117     /**
118      * Returns an immutable set of canonical system time zone IDs that
119      * are associated with actual locations.
120      * The result set is a subset of {@link #getCanonicalSystemZIDs()}, but not
121      * including IDs, such as "Etc/GTM+5".
122      * @return An immutable set of canonical system time zone IDs that
123      * are associated with actual locations.
124      */
getCanonicalSystemLocationZIDs()125     private static synchronized Set<String> getCanonicalSystemLocationZIDs() {
126         Set<String> canonicalSystemLocationZones = null;
127         if (REF_CANONICAL_SYSTEM_LOCATION_ZONES != null) {
128             canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get();
129         }
130         if (canonicalSystemLocationZones == null) {
131             Set<String> canonicalSystemLocationIDs = new TreeSet<String>();
132             String[] allIDs = getZoneIDs();
133             for (String id : allIDs) {
134                 // exclude Etc/Unknown
135                 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
136                     continue;
137                 }
138                 String canonicalID = getCanonicalCLDRID(id);
139                 if (id.equals(canonicalID)) {
140                     String region = getRegion(id);
141                     if (region != null && !region.equals(kWorld)) {
142                         canonicalSystemLocationIDs.add(id);
143                     }
144                 }
145             }
146             canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
147             REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones);
148         }
149         return canonicalSystemLocationZones;
150     }
151 
152     /**
153      * Returns an immutable set of system IDs for the given conditions.
154      * @param type      a system time zone type.
155      * @param region    a region, or null.
156      * @param rawOffset a zone raw offset or null.
157      * @return An immutable set of system IDs for the given conditions.
158      */
getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset)159     public static Set<String> getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset) {
160         Set<String> baseSet = null;
161         switch (type) {
162         case ANY:
163             baseSet = getSystemZIDs();
164             break;
165         case CANONICAL:
166             baseSet = getCanonicalSystemZIDs();
167             break;
168         case CANONICAL_LOCATION:
169             baseSet = getCanonicalSystemLocationZIDs();
170             break;
171         default:
172             // never occur
173             throw new IllegalArgumentException("Unknown SystemTimeZoneType");
174         }
175 
176         if (region == null && rawOffset == null) {
177             return baseSet;
178         }
179 
180         if (region != null) {
181             region = region.toUpperCase(Locale.ENGLISH);
182         }
183 
184         // Filter by region/rawOffset
185         Set<String> result = new TreeSet<String>();
186         for (String id : baseSet) {
187             if (region != null) {
188                 String r = getRegion(id);
189                 if (!region.equals(r)) {
190                     continue;
191                 }
192             }
193             if (rawOffset != null) {
194                 // This is VERY inefficient.
195                 TimeZone z = getSystemTimeZone(id);
196                 if (z == null || !rawOffset.equals(z.getRawOffset())) {
197                     continue;
198                 }
199             }
200             result.add(id);
201         }
202         if (result.isEmpty()) {
203             return Collections.emptySet();
204         }
205 
206         return Collections.unmodifiableSet(result);
207     }
208 
209     /**
210      * Returns the number of IDs in the equivalency group that
211      * includes the given ID.  An equivalency group contains zones
212      * that behave identically to the given zone.
213      *
214      * <p>If there are no equivalent zones, then this method returns
215      * 0.  This means either the given ID is not a valid zone, or it
216      * is and there are no other equivalent zones.
217      * @param id a system time zone ID
218      * @return the number of zones in the equivalency group containing
219      * 'id', or zero if there are no equivalent zones.
220      * @see #getEquivalentID
221      */
countEquivalentIDs(String id)222     public static synchronized int countEquivalentIDs(String id) {
223         int count = 0;
224         UResourceBundle res = openOlsonResource(null, id);
225         if (res != null) {
226             try {
227                 UResourceBundle links = res.get("links");
228                 int[] v = links.getIntVector();
229                 count = v.length;
230             } catch (MissingResourceException ex) {
231                 // throw away
232             }
233         }
234         return count;
235     }
236 
237     /**
238      * Returns an ID in the equivalency group that includes the given
239      * ID.  An equivalency group contains zones that behave
240      * identically to the given zone.
241      *
242      * <p>The given index must be in the range 0..n-1, where n is the
243      * value returned by <code>countEquivalentIDs(id)</code>.  For
244      * some value of 'index', the returned value will be equal to the
245      * given id.  If the given id is not a valid system time zone, or
246      * if 'index' is out of range, then returns an empty string.
247      * @param id a system time zone ID
248      * @param index a value from 0 to n-1, where n is the value
249      * returned by <code>countEquivalentIDs(id)</code>
250      * @return the ID of the index-th zone in the equivalency group
251      * containing 'id', or an empty string if 'id' is not a valid
252      * system ID or 'index' is out of range
253      * @see #countEquivalentIDs
254      */
getEquivalentID(String id, int index)255     public static synchronized String getEquivalentID(String id, int index) {
256         String result = "";
257         if (index >= 0) {
258             UResourceBundle res = openOlsonResource(null, id);
259             if (res != null) {
260                 int zoneIdx = -1;
261                 try {
262                     UResourceBundle links = res.get("links");
263                     int[] zones = links.getIntVector();
264                     if (index < zones.length) {
265                         zoneIdx = zones[index];
266                     }
267                 } catch (MissingResourceException ex) {
268                     // throw away
269                 }
270                 if (zoneIdx >= 0) {
271                     String tmp = getZoneID(zoneIdx);
272                     if (tmp != null) {
273                         result = tmp;
274                     }
275                 }
276             }
277         }
278         return result;
279     }
280 
281     private static String[] ZONEIDS = null;
282 
283     /*
284      * ICU frequently refers the zone ID array in zoneinfo resource
285      */
getZoneIDs()286     private static synchronized String[] getZoneIDs() {
287         if (ZONEIDS == null) {
288             try {
289                 UResourceBundle top = UResourceBundle.getBundleInstance(
290                         ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
291                 ZONEIDS = top.getStringArray(kNAMES);
292             } catch (MissingResourceException ex) {
293                 // throw away..
294             }
295         }
296         if (ZONEIDS == null) {
297             ZONEIDS = new String[0];
298         }
299         return ZONEIDS;
300     }
301 
getZoneID(int idx)302     private static String getZoneID(int idx) {
303         if (idx >= 0) {
304             String[] ids = getZoneIDs();
305             if (idx < ids.length) {
306                 return ids[idx];
307             }
308         }
309         return null;
310     }
311 
getZoneIndex(String zid)312     private static int getZoneIndex(String zid) {
313         int zoneIdx = -1;
314 
315         String[] all = getZoneIDs();
316         if (all.length > 0) {
317             int start = 0;
318             int limit = all.length;
319 
320             int lastMid = Integer.MAX_VALUE;
321             for (;;) {
322                 int mid = (start + limit) / 2;
323                 if (lastMid == mid) {   /* Have we moved? */
324                     break;  /* We haven't moved, and it wasn't found. */
325                 }
326                 lastMid = mid;
327                 int r = zid.compareTo(all[mid]);
328                 if (r == 0) {
329                     zoneIdx = mid;
330                     break;
331                 } else if(r < 0) {
332                     limit = mid;
333                 } else {
334                     start = mid;
335                 }
336             }
337         }
338 
339         return zoneIdx;
340     }
341 
342     private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<String, String>();
343     private static ICUCache<String, String> REGION_CACHE = new SimpleCache<String, String>();
344     private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<String, Boolean>();
345 
getCanonicalCLDRID(TimeZone tz)346     public static String getCanonicalCLDRID(TimeZone tz) {
347         if (tz instanceof OlsonTimeZone) {
348             return ((OlsonTimeZone)tz).getCanonicalID();
349         }
350         return getCanonicalCLDRID(tz.getID());
351     }
352 
353     /**
354      * Return the canonical id for this tzid defined by CLDR, which might be
355      * the id itself. If the given tzid is not known, return null.
356      *
357      * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is
358      * NOT a system ID).
359      */
getCanonicalCLDRID(String tzid)360     public static String getCanonicalCLDRID(String tzid) {
361         String canonical = CANONICAL_ID_CACHE.get(tzid);
362         if (canonical == null) {
363             canonical = findCLDRCanonicalID(tzid);
364             if (canonical == null) {
365                 // Resolve Olson link and try it again if necessary
366                 try {
367                     int zoneIdx = getZoneIndex(tzid);
368                     if (zoneIdx >= 0) {
369                         UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
370                                 ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
371                         UResourceBundle zones = top.get(kZONES);
372                         UResourceBundle zone = zones.get(zoneIdx);
373                         if (zone.getType() == UResourceBundle.INT) {
374                             // It's a link - resolve link and lookup
375                             tzid = getZoneID(zone.getInt());
376                             canonical = findCLDRCanonicalID(tzid);
377                         }
378                         if (canonical == null) {
379                             canonical = tzid;
380                         }
381                     }
382                 } catch (MissingResourceException e) {
383                     // fall through
384                 }
385             }
386             if (canonical != null) {
387                 CANONICAL_ID_CACHE.put(tzid, canonical);
388             }
389         }
390         return canonical;
391     }
392 
findCLDRCanonicalID(String tzid)393     private static String findCLDRCanonicalID(String tzid) {
394         String canonical = null;
395         String tzidKey = tzid.replace('/', ':');
396 
397         try {
398             // First, try check if the given ID is canonical
399             UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
400                     "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
401             UResourceBundle typeMap = keyTypeData.get("typeMap");
402             UResourceBundle typeKeys = typeMap.get("timezone");
403             try {
404                 /* UResourceBundle canonicalEntry = */ typeKeys.get(tzidKey);
405                 // The given tzid is available in the canonical list
406                 canonical = tzid;
407             } catch (MissingResourceException e) {
408                 // fall through
409             }
410             if (canonical == null) {
411                 // Try alias map
412                 UResourceBundle typeAlias = keyTypeData.get("typeAlias");
413                 UResourceBundle aliasesForKey = typeAlias.get("timezone");
414                 canonical = aliasesForKey.getString(tzidKey);
415             }
416         } catch (MissingResourceException e) {
417             // fall through
418         }
419         return canonical;
420     }
421 
422     /**
423      * Return the region code for this tzid.
424      * If tzid is not a system zone ID, this method returns null.
425      */
getRegion(String tzid)426     public static String getRegion(String tzid) {
427         String region = REGION_CACHE.get(tzid);
428         if (region == null) {
429             int zoneIdx = getZoneIndex(tzid);
430             if (zoneIdx >= 0) {
431                 try {
432                     UResourceBundle top = UResourceBundle.getBundleInstance(
433                             ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
434                     UResourceBundle regions = top.get(kREGIONS);
435                     if (zoneIdx < regions.getSize()) {
436                         region = regions.getString(zoneIdx);
437                     }
438                 } catch (MissingResourceException e) {
439                     // throw away
440                 }
441                 if (region != null) {
442                     REGION_CACHE.put(tzid, region);
443                 }
444             }
445         }
446         return region;
447     }
448 
449     /**
450      * Return the canonical country code for this tzid.  If we have none, or if the time zone
451      * is not associated with a country or unknown, return null.
452      */
getCanonicalCountry(String tzid)453     public static String getCanonicalCountry(String tzid) {
454         String country = getRegion(tzid);
455         if (country != null && country.equals(kWorld)) {
456             country = null;
457         }
458         return country;
459     }
460 
461     /**
462      * Return the canonical country code for this tzid.  If we have none, or if the time zone
463      * is not associated with a country or unknown, return null. When the given zone is the
464      * primary zone of the country, true is set to isPrimary.
465      */
getCanonicalCountry(String tzid, Output<Boolean> isPrimary)466     public static String getCanonicalCountry(String tzid, Output<Boolean> isPrimary) {
467         isPrimary.value = Boolean.FALSE;
468 
469         String country = getRegion(tzid);
470         if (country != null && country.equals(kWorld)) {
471             return null;
472         }
473 
474         // Check the cache
475         Boolean singleZone = SINGLE_COUNTRY_CACHE.get(tzid);
476         if (singleZone == null) {
477             Set<String> ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null);
478             assert(ids.size() >= 1);
479             singleZone = Boolean.valueOf(ids.size() <= 1);
480             SINGLE_COUNTRY_CACHE.put(tzid, singleZone);
481         }
482 
483         if (singleZone) {
484             isPrimary.value = Boolean.TRUE;
485         } else {
486             // Note: We may cache the primary zone map in future.
487 
488             // Even a country has multiple zones, one of them might be
489             // dominant and treated as a primary zone.
490             try {
491                 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones");
492                 UResourceBundle primaryZones = bundle.get("primaryZones");
493                 String primaryZone = primaryZones.getString(country);
494                 if (tzid.equals(primaryZone)) {
495                     isPrimary.value = Boolean.TRUE;
496                 } else {
497                     // The given ID might not be a canonical ID
498                     String canonicalID = getCanonicalCLDRID(tzid);
499                     if (canonicalID != null && canonicalID.equals(primaryZone)) {
500                         isPrimary.value = Boolean.TRUE;
501                     }
502                 }
503             } catch (MissingResourceException e) {
504                 // ignore
505             }
506         }
507 
508         return country;
509     }
510 
511     /**
512      * Given an ID and the top-level resource of the zoneinfo resource,
513      * open the appropriate resource for the given time zone.
514      * Dereference links if necessary.
515      * @param top the top level resource of the zoneinfo resource or null.
516      * @param id zone id
517      * @return the corresponding zone resource or null if not found
518      */
openOlsonResource(UResourceBundle top, String id)519     public static UResourceBundle openOlsonResource(UResourceBundle top, String id)
520     {
521         UResourceBundle res = null;
522         int zoneIdx = getZoneIndex(id);
523         if (zoneIdx >= 0) {
524             try {
525                 if (top == null) {
526                     top = UResourceBundle.getBundleInstance(
527                             ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
528                 }
529                 UResourceBundle zones = top.get(kZONES);
530                 UResourceBundle zone = zones.get(zoneIdx);
531                 if (zone.getType() == UResourceBundle.INT) {
532                     // resolve link
533                     zone = zones.get(zone.getInt());
534                 }
535                 res = zone;
536             } catch (MissingResourceException e) {
537                 res = null;
538             }
539         }
540         return res;
541     }
542 
543 
544     /**
545      * System time zone object cache
546      */
547     private static class SystemTimeZoneCache extends SoftCache<String, OlsonTimeZone, String> {
548 
549         /* (non-Javadoc)
550          * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
551          */
552         @Override
createInstance(String key, String data)553         protected OlsonTimeZone createInstance(String key, String data) {
554             OlsonTimeZone tz = null;
555             try {
556                 UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
557                         ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
558                 UResourceBundle res = openOlsonResource(top, data);
559                 if (res != null) {
560                     tz = new OlsonTimeZone(top, res, data);
561                     tz.freeze();
562                 }
563             } catch (MissingResourceException e) {
564                 // do nothing
565             }
566             return tz;
567         }
568     }
569 
570     private static final SystemTimeZoneCache SYSTEM_ZONE_CACHE = new SystemTimeZoneCache();
571 
572     /**
573      * Returns a frozen OlsonTimeZone instance for the given ID.
574      * This method returns null when the given ID is unknown.
575      */
getSystemTimeZone(String id)576     public static OlsonTimeZone getSystemTimeZone(String id) {
577         return SYSTEM_ZONE_CACHE.getInstance(id, id);
578     }
579 
580     // Maximum value of valid custom time zone hour/min
581     private static final int kMAX_CUSTOM_HOUR = 23;
582     private static final int kMAX_CUSTOM_MIN = 59;
583     private static final int kMAX_CUSTOM_SEC = 59;
584 
585     /**
586      * Custom time zone object cache
587      */
588     private static class CustomTimeZoneCache extends SoftCache<Integer, SimpleTimeZone, int[]> {
589 
590         /* (non-Javadoc)
591          * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
592          */
593         @Override
createInstance(Integer key, int[] data)594         protected SimpleTimeZone createInstance(Integer key, int[] data) {
595             assert (data.length == 4);
596             assert (data[0] == 1 || data[0] == -1);
597             assert (data[1] >= 0 && data[1] <= kMAX_CUSTOM_HOUR);
598             assert (data[2] >= 0 && data[2] <= kMAX_CUSTOM_MIN);
599             assert (data[3] >= 0 && data[3] <= kMAX_CUSTOM_SEC);
600             String id = formatCustomID(data[1], data[2], data[3], data[0] < 0);
601             int offset = data[0] * ((data[1] * 60 + data[2]) * 60 + data[3]) * 1000;
602             SimpleTimeZone tz = new SimpleTimeZone(offset, id);
603             tz.freeze();
604             return tz;
605         }
606     }
607 
608     private static final CustomTimeZoneCache CUSTOM_ZONE_CACHE = new CustomTimeZoneCache();
609 
610     /**
611      * Parse a custom time zone identifier and return a corresponding zone.
612      * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
613      * GMT[+-]hh.
614      * @return a frozen SimpleTimeZone with the given offset and
615      * no Daylight Savings Time, or null if the id cannot be parsed.
616     */
getCustomTimeZone(String id)617     public static SimpleTimeZone getCustomTimeZone(String id){
618         int[] fields = new int[4];
619         if (parseCustomID(id, fields)) {
620             // fields[0] - sign
621             // fields[1] - hour / 5-bit
622             // fields[2] - min  / 6-bit
623             // fields[3] - sec  / 6-bit
624             Integer key = Integer.valueOf(
625                     fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11));
626             return CUSTOM_ZONE_CACHE.getInstance(key, fields);
627         }
628         return null;
629     }
630 
631     /**
632      * Parse a custom time zone identifier and return the normalized
633      * custom time zone identifier for the given custom id string.
634      * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
635      * GMT[+-]hh.
636      * @return The normalized custom id string.
637     */
getCustomID(String id)638     public static String getCustomID(String id) {
639         int[] fields = new int[4];
640         if (parseCustomID(id, fields)) {
641             return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
642         }
643         return null;
644     }
645 
646     /*
647      * Parses the given custom time zone identifier
648      * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
649      * GMT[+-]hh.
650      * @param fields An array of int (length = 4) to receive the parsed
651      * offset time fields.  The sign is set to fields[0] (-1 or 1),
652      * hour is set to fields[1], minute is set to fields[2] and second is
653      * set to fields[3].
654      * @return Returns true when the given custom id is valid.
655      */
parseCustomID(String id, int[] fields)656     static boolean parseCustomID(String id, int[] fields) {
657         NumberFormat numberFormat = null;
658 
659         if (id != null && id.length() > kGMT_ID.length() &&
660                 id.toUpperCase(Locale.ENGLISH).startsWith(kGMT_ID)) {
661             ParsePosition pos = new ParsePosition(kGMT_ID.length());
662             int sign = 1;
663             int hour = 0;
664             int min = 0;
665             int sec = 0;
666 
667             if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
668                 sign = -1;
669             } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
670                 return false;
671             }
672             pos.setIndex(pos.getIndex() + 1);
673 
674             numberFormat = NumberFormat.getInstance();
675             numberFormat.setParseIntegerOnly(true);
676 
677             // Look for either hh:mm, hhmm, or hh
678             int start = pos.getIndex();
679 
680             Number n = numberFormat.parse(id, pos);
681             if (pos.getIndex() == start) {
682                 return false;
683             }
684             hour = n.intValue();
685 
686             if (pos.getIndex() < id.length()){
687                 if (pos.getIndex() - start > 2
688                         || id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
689                     return false;
690                 }
691                 // hh:mm
692                 pos.setIndex(pos.getIndex() + 1);
693                 int oldPos = pos.getIndex();
694                 n = numberFormat.parse(id, pos);
695                 if ((pos.getIndex() - oldPos) != 2) {
696                     // must be 2 digits
697                     return false;
698                 }
699                 min = n.intValue();
700                 if (pos.getIndex() < id.length()) {
701                     if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
702                         return false;
703                     }
704                     // [:ss]
705                     pos.setIndex(pos.getIndex() + 1);
706                     oldPos = pos.getIndex();
707                     n = numberFormat.parse(id, pos);
708                     if (pos.getIndex() != id.length()
709                             || (pos.getIndex() - oldPos) != 2) {
710                         return false;
711                     }
712                     sec = n.intValue();
713                 }
714             } else {
715                 // Supported formats are below -
716                 //
717                 // HHmmss
718                 // Hmmss
719                 // HHmm
720                 // Hmm
721                 // HH
722                 // H
723 
724                 int length = pos.getIndex() - start;
725                 if (length <= 0 || 6 < length) {
726                     // invalid length
727                     return false;
728                 }
729                 switch (length) {
730                     case 1:
731                     case 2:
732                         // already set to hour
733                         break;
734                     case 3:
735                     case 4:
736                         min = hour % 100;
737                         hour /= 100;
738                         break;
739                     case 5:
740                     case 6:
741                         sec = hour % 100;
742                         min = (hour/100) % 100;
743                         hour /= 10000;
744                         break;
745                 }
746             }
747 
748             if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
749                 if (fields != null) {
750                     if (fields.length >= 1) {
751                         fields[0] = sign;
752                     }
753                     if (fields.length >= 2) {
754                         fields[1] = hour;
755                     }
756                     if (fields.length >= 3) {
757                         fields[2] = min;
758                     }
759                     if (fields.length >= 4) {
760                         fields[3] = sec;
761                     }
762                 }
763                 return true;
764             }
765         }
766         return false;
767     }
768 
769     /**
770      * Creates a custom zone for the offset
771      * @param offset GMT offset in milliseconds
772      * @return A custom TimeZone for the offset with normalized time zone id
773      */
getCustomTimeZone(int offset)774     public static SimpleTimeZone getCustomTimeZone(int offset) {
775         boolean negative = false;
776         int tmp = offset;
777         if (offset < 0) {
778             negative = true;
779             tmp = -offset;
780         }
781 
782         int hour, min, sec;
783 
784         if (ASSERT) {
785             Assert.assrt("millis!=0", tmp % 1000 != 0);
786         }
787         tmp /= 1000;
788         sec = tmp % 60;
789         tmp /= 60;
790         min = tmp % 60;
791         hour = tmp / 60;
792 
793         // Note: No millisecond part included in TZID for now
794         String zid = formatCustomID(hour, min, sec, negative);
795 
796         return new SimpleTimeZone(offset, zid);
797     }
798 
799     /*
800      * Returns the normalized custom TimeZone ID
801      */
formatCustomID(int hour, int min, int sec, boolean negative)802     static String formatCustomID(int hour, int min, int sec, boolean negative) {
803         // Create normalized time zone ID - GMT[+|-]hh:mm[:ss]
804         StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX);
805         if (hour != 0 || min != 0) {
806             if(negative) {
807                 zid.append('-');
808             } else {
809                 zid.append('+');
810             }
811             // Always use US-ASCII digits
812             if (hour < 10) {
813                 zid.append('0');
814             }
815             zid.append(hour);
816             zid.append(':');
817             if (min < 10) {
818                 zid.append('0');
819             }
820             zid.append(min);
821 
822             if (sec != 0) {
823                 // Optional second field
824                 zid.append(':');
825                 if (sec < 10) {
826                     zid.append('0');
827                 }
828                 zid.append(sec);
829             }
830         }
831         return zid.toString();
832     }
833 
834     /**
835      * Returns the time zone's short ID for the zone.
836      * For example, "uslax" for zone "America/Los_Angeles".
837      * @param tz the time zone
838      * @return the short ID of the time zone, or null if the short ID is not available.
839      */
getShortID(TimeZone tz)840     public static String getShortID(TimeZone tz) {
841         String canonicalID = null;
842 
843         if (tz instanceof OlsonTimeZone) {
844             canonicalID = ((OlsonTimeZone)tz).getCanonicalID();
845         }
846         else {
847             canonicalID = getCanonicalCLDRID(tz.getID());
848         }
849         if (canonicalID == null) {
850             return null;
851         }
852         return getShortIDFromCanonical(canonicalID);
853     }
854 
855     /**
856      * Returns the time zone's short ID for the zone ID.
857      * For example, "uslax" for zone ID "America/Los_Angeles".
858      * @param id the time zone ID
859      * @return the short ID of the time zone ID, or null if the short ID is not available.
860      */
getShortID(String id)861     public static String getShortID(String id) {
862         String canonicalID = getCanonicalCLDRID(id);
863         if (canonicalID == null) {
864             return null;
865         }
866         return getShortIDFromCanonical(canonicalID);
867     }
868 
getShortIDFromCanonical(String canonicalID)869     private static String getShortIDFromCanonical(String canonicalID) {
870         String shortID = null;
871         String tzidKey = canonicalID.replace('/', ':');
872 
873         try {
874             // First, try check if the given ID is canonical
875             UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
876                     "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
877             UResourceBundle typeMap = keyTypeData.get("typeMap");
878             UResourceBundle typeKeys = typeMap.get("timezone");
879             shortID = typeKeys.getString(tzidKey);
880         } catch (MissingResourceException e) {
881             // fall through
882         }
883 
884         return shortID;
885     }
886 
887 }
888