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