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*/
14package android.icu.impl;
15
16import java.lang.ref.SoftReference;
17import java.text.ParsePosition;
18import java.util.Collections;
19import java.util.Locale;
20import java.util.MissingResourceException;
21import java.util.Set;
22import java.util.TreeSet;
23
24import android.icu.text.NumberFormat;
25import android.icu.util.Output;
26import android.icu.util.SimpleTimeZone;
27import android.icu.util.TimeZone;
28import android.icu.util.TimeZone.SystemTimeZoneType;
29import android.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 Only a subset of ICU is exposed in Android
44 */
45public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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     */
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
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     */
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     */
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     */
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     */
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 android.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
552         */
553        @Override
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     */
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 android.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
593         */
594        @Override
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    */
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    */
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     */
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     */
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     */
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     */
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     */
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
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