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