ZoneMeta.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 **********************************************************************
 * Copyright (c) 2003-2016 International Business Machines
 * Corporation and others.  All Rights Reserved.
 **********************************************************************
 * Author: Alan Liu
 * Created: September 4 2003
 * Since: ICU 2.8
 **********************************************************************
 */
package com.ibm.icu.impl;

import com.ibm.icu.util.Output;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.UResourceBundle;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeSet;

/**
 * This class, not to be instantiated, implements the meta-data missing from the underlying core JDK
 * implementation of time zones. There are two missing features: Obtaining a list of available zones
 * for a given country (as defined by the Olson database), and obtaining a list of equivalent zones
 * for a given zone (as defined by Olson links).
 *
 * <p>This class uses a data class, ZoneMetaData, which is created by the tool tz2icu.
 *
 * @author Alan Liu
 * @since ICU 2.8
 */
public final class ZoneMeta {
    private static final boolean ASSERT = false;

    private static final String ZONEINFORESNAME = "zoneinfo64";
    private static final String kREGIONS = "Regions";
    private static final String kZONES = "Zones";
    private static final String kNAMES = "Names";

    private static final String kGMT_ID = "GMT";
    private static final String kCUSTOM_TZ_PREFIX = "GMT";

    private static final String kWorld = "001";

    private static SoftReference<Set<String>> REF_SYSTEM_ZONES;
    private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES;
    private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES;

    /**
     * Returns an immutable set of system time zone IDs. Etc/Unknown is excluded.
     *
     * @return An immutable set of system time zone IDs.
     */
    private static synchronized Set<String> getSystemZIDs() {
        Set<String> systemZones = null;
        if (REF_SYSTEM_ZONES != null) {
            systemZones = REF_SYSTEM_ZONES.get();
        }
        if (systemZones == null) {
            Set<String> systemIDs = new TreeSet<>();
            String[] allIDs = getZoneIDs();
            for (String id : allIDs) {
                // exclude Etc/Unknown
                if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
                    continue;
                }
                systemIDs.add(id);
            }
            systemZones = Collections.unmodifiableSet(systemIDs);
            REF_SYSTEM_ZONES = new SoftReference<>(systemZones);
        }
        return systemZones;
    }

    /**
     * Returns an immutable set of canonical system time zone IDs. The result set is a subset of
     * {@link #getSystemZIDs()}, but not including aliases, such as "US/Eastern".
     *
     * @return An immutable set of canonical system time zone IDs.
     */
    private static synchronized Set<String> getCanonicalSystemZIDs() {
        Set<String> canonicalSystemZones = null;
        if (REF_CANONICAL_SYSTEM_ZONES != null) {
            canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get();
        }
        if (canonicalSystemZones == null) {
            Set<String> canonicalSystemIDs = new TreeSet<>();
            String[] allIDs = getZoneIDs();
            for (String id : allIDs) {
                // exclude Etc/Unknown
                if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
                    continue;
                }
                String canonicalID = getCanonicalCLDRID(id);
                if (id.equals(canonicalID)) {
                    canonicalSystemIDs.add(id);
                }
            }
            canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
            REF_CANONICAL_SYSTEM_ZONES = new SoftReference<>(canonicalSystemZones);
        }
        return canonicalSystemZones;
    }

    /**
     * Returns an immutable set of canonical system time zone IDs that are associated with actual
     * locations. The result set is a subset of {@link #getCanonicalSystemZIDs()}, but not including
     * IDs, such as "Etc/GTM+5".
     *
     * @return An immutable set of canonical system time zone IDs that are associated with actual
     *     locations.
     */
    private static synchronized Set<String> getCanonicalSystemLocationZIDs() {
        Set<String> canonicalSystemLocationZones = null;
        if (REF_CANONICAL_SYSTEM_LOCATION_ZONES != null) {
            canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get();
        }
        if (canonicalSystemLocationZones == null) {
            Set<String> canonicalSystemLocationIDs = new TreeSet<>();
            String[] allIDs = getZoneIDs();
            for (String id : allIDs) {
                // exclude Etc/Unknown
                if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) {
                    continue;
                }
                String canonicalID = getCanonicalCLDRID(id);
                if (id.equals(canonicalID)) {
                    String region = getRegion(id);
                    if (region != null && !region.equals(kWorld)) {
                        canonicalSystemLocationIDs.add(id);
                    }
                }
            }
            canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
            REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<>(canonicalSystemLocationZones);
        }
        return canonicalSystemLocationZones;
    }

    /**
     * Returns an immutable set of system IDs for the given conditions.
     *
     * @param type a system time zone type.
     * @param region a region, or null.
     * @param rawOffset a zone raw offset or null.
     * @return An immutable set of system IDs for the given conditions.
     */
    public static Set<String> getAvailableIDs(
            SystemTimeZoneType type, String region, Integer rawOffset) {
        Set<String> baseSet = null;
        switch (type) {
            case ANY:
                baseSet = getSystemZIDs();
                break;
            case CANONICAL:
                baseSet = getCanonicalSystemZIDs();
                break;
            case CANONICAL_LOCATION:
                baseSet = getCanonicalSystemLocationZIDs();
                break;
            default:
                // never occur
                throw new IllegalArgumentException("Unknown SystemTimeZoneType");
        }

        if (region == null && rawOffset == null) {
            return baseSet;
        }

        if (region != null) {
            region = region.toUpperCase(Locale.ENGLISH);
        }

        // Filter by region/rawOffset
        Set<String> result = new TreeSet<>();
        for (String id : baseSet) {
            if (region != null) {
                String r = getRegion(id);
                if (!region.equals(r)) {
                    continue;
                }
            }
            if (rawOffset != null) {
                // This is VERY inefficient.
                TimeZone z = getSystemTimeZone(id);
                if (z == null || !rawOffset.equals(z.getRawOffset())) {
                    continue;
                }
            }
            result.add(id);
        }
        if (result.isEmpty()) {
            return Collections.emptySet();
        }

        return Collections.unmodifiableSet(result);
    }

    /**
     * Returns the number of IDs in the equivalency group that includes the given ID. An equivalency
     * group contains zones that behave identically to the given zone.
     *
     * <p>If there are no equivalent zones, then this method returns 0. This means either the given
     * ID is not a valid zone, or it is and there are no other equivalent zones.
     *
     * @param id a system time zone ID
     * @return the number of zones in the equivalency group containing 'id', or zero if there are no
     *     equivalent zones.
     * @see #getEquivalentID
     */
    public static synchronized int countEquivalentIDs(String id) {
        int count = 0;
        UResourceBundle res = openOlsonResource(null, id);
        if (res != null) {
            try {
                UResourceBundle links = res.get("links");
                int[] v = links.getIntVector();
                count = v.length;
            } catch (MissingResourceException ex) {
                // throw away
            }
        }
        return count;
    }

    /**
     * Returns an ID in the equivalency group that includes the given ID. An equivalency group
     * contains zones that behave identically to the given zone.
     *
     * <p>The given index must be in the range 0..n-1, where n is the value returned by <code>
     * countEquivalentIDs(id)</code>. For some value of 'index', the returned value will be equal to
     * the given id. If the given id is not a valid system time zone, or if 'index' is out of range,
     * then returns an empty string.
     *
     * @param id a system time zone ID
     * @param index a value from 0 to n-1, where n is the value returned by <code>
     *     countEquivalentIDs(id)</code>
     * @return the ID of the index-th zone in the equivalency group containing 'id', or an empty
     *     string if 'id' is not a valid system ID or 'index' is out of range
     * @see #countEquivalentIDs
     */
    public static synchronized String getEquivalentID(String id, int index) {
        String result = "";
        if (index >= 0) {
            UResourceBundle res = openOlsonResource(null, id);
            if (res != null) {
                int zoneIdx = -1;
                try {
                    UResourceBundle links = res.get("links");
                    int[] zones = links.getIntVector();
                    if (index < zones.length) {
                        zoneIdx = zones[index];
                    }
                } catch (MissingResourceException ex) {
                    // throw away
                }
                if (zoneIdx >= 0) {
                    String tmp = getZoneID(zoneIdx);
                    if (tmp != null) {
                        result = tmp;
                    }
                }
            }
        }
        return result;
    }

    private static String[] ZONEIDS = null;

    /*
     * ICU frequently refers the zone ID array in zoneinfo resource
     */
    private static synchronized String[] getZoneIDs() {
        if (ZONEIDS == null) {
            try {
                UResourceBundle top =
                        UResourceBundle.getBundleInstance(
                                ICUData.ICU_BASE_NAME,
                                ZONEINFORESNAME,
                                ICUResourceBundle.ICU_DATA_CLASS_LOADER);
                ZONEIDS = top.getStringArray(kNAMES);
            } catch (MissingResourceException ex) {
                // throw away..
            }
        }
        if (ZONEIDS == null) {
            ZONEIDS = new String[0];
        }
        return ZONEIDS;
    }

    private static String getZoneID(int idx) {
        if (idx >= 0) {
            String[] ids = getZoneIDs();
            if (idx < ids.length) {
                return ids[idx];
            }
        }
        return null;
    }

    private static int getZoneIndex(String zid) {
        int zoneIdx = -1;

        String[] all = getZoneIDs();
        if (all.length > 0) {
            int start = 0;
            int limit = all.length;

            int lastMid = Integer.MAX_VALUE;
            for (; ; ) {
                int mid = (start + limit) / 2;
                if (lastMid == mid) {
                    /* Have we moved? */
                    break; /* We haven't moved, and it wasn't found. */
                }
                lastMid = mid;
                int r = zid.compareTo(all[mid]);
                if (r == 0) {
                    zoneIdx = mid;
                    break;
                } else if (r < 0) {
                    limit = mid;
                } else {
                    start = mid;
                }
            }
        }

        return zoneIdx;
    }

    private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<>();
    private static ICUCache<String, String> REGION_CACHE = new SimpleCache<>();
    private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<>();

    public static String getCanonicalCLDRID(TimeZone tz) {
        if (tz instanceof OlsonTimeZone) {
            return ((OlsonTimeZone) tz).getCanonicalID();
        }
        return getCanonicalCLDRID(tz.getID());
    }

    /**
     * Return the canonical id for this tzid defined by CLDR, which might be the id itself. If the
     * given tzid is not known, return null.
     *
     * <p>Note: This internal API supports all known system IDs and "Etc/Unknown" (which is NOT a
     * system ID).
     */
    public static String getCanonicalCLDRID(String tzid) {
        String canonical = CANONICAL_ID_CACHE.get(tzid);
        if (canonical == null) {
            canonical = findCLDRCanonicalID(tzid);
            if (canonical == null) {
                // Resolve Olson link and try it again if necessary
                try {
                    int zoneIdx = getZoneIndex(tzid);
                    if (zoneIdx >= 0) {
                        UResourceBundle top =
                                UResourceBundle.getBundleInstance(
                                        ICUData.ICU_BASE_NAME,
                                        ZONEINFORESNAME,
                                        ICUResourceBundle.ICU_DATA_CLASS_LOADER);
                        UResourceBundle zones = top.get(kZONES);
                        UResourceBundle zone = zones.get(zoneIdx);
                        if (zone.getType() == UResourceBundle.INT) {
                            // It's a link - resolve link and lookup
                            tzid = getZoneID(zone.getInt());
                            canonical = findCLDRCanonicalID(tzid);
                        }
                        if (canonical == null) {
                            canonical = tzid;
                        }
                    }
                } catch (MissingResourceException e) {
                    // fall through
                }
            }
            if (canonical != null) {
                CANONICAL_ID_CACHE.put(tzid, canonical);
            }
        }
        return canonical;
    }

    private static String findCLDRCanonicalID(String tzid) {
        String canonical = null;
        String tzidKey = tzid.replace('/', ':');

        try {
            // First, try check if the given ID is canonical
            UResourceBundle keyTypeData =
                    UResourceBundle.getBundleInstance(
                            ICUData.ICU_BASE_NAME,
                            "keyTypeData",
                            ICUResourceBundle.ICU_DATA_CLASS_LOADER);
            UResourceBundle typeMap = keyTypeData.get("typeMap");
            UResourceBundle typeKeys = typeMap.get("timezone");
            try {
                /* UResourceBundle canonicalEntry = */ typeKeys.get(tzidKey);
                // The given tzid is available in the canonical list
                canonical = tzid;
            } catch (MissingResourceException e) {
                // fall through
            }
            if (canonical == null) {
                // Try alias map
                UResourceBundle typeAlias = keyTypeData.get("typeAlias");
                UResourceBundle aliasesForKey = typeAlias.get("timezone");
                canonical = aliasesForKey.getString(tzidKey);
            }
        } catch (MissingResourceException e) {
            // fall through
        }
        return canonical;
    }

    /**
     * Returns primary IANA zone ID for the input zone ID. When input zone ID is not known, this
     * method returns null.
     *
     * @param tzid An input zone ID.
     * @return A primary IANA zone ID equivalent to the input zone ID.
     */
    public static String getIanaID(String tzid) {
        // First, get CLDR canonical ID
        String canonicalID = getCanonicalCLDRID(tzid);
        if (canonicalID == null) {
            return null;
        }
        // Find IANA mapping if any.
        UResourceBundle keyTypeData =
                UResourceBundle.getBundleInstance(
                        ICUData.ICU_BASE_NAME,
                        "keyTypeData",
                        ICUResourceBundle.ICU_DATA_CLASS_LOADER);
        UResourceBundle ianaMap = keyTypeData.get("ianaMap");
        UResourceBundle ianaTzMap = ianaMap.get("timezone");
        try {
            return ianaTzMap.getString(canonicalID.replace('/', ':'));
        } catch (MissingResourceException e) {
            // No IANA zone ID mapping. In this case, ianaId set by getCanonicalCLDRID()
            // is also a primary IANA id.
            return canonicalID;
        }
    }

    /**
     * Return the region code for this tzid. If tzid is not a system zone ID, this method returns
     * null.
     */
    public static String getRegion(String tzid) {
        String region = REGION_CACHE.get(tzid);
        if (region == null) {
            int zoneIdx = getZoneIndex(tzid);
            if (zoneIdx >= 0) {
                try {
                    UResourceBundle top =
                            UResourceBundle.getBundleInstance(
                                    ICUData.ICU_BASE_NAME,
                                    ZONEINFORESNAME,
                                    ICUResourceBundle.ICU_DATA_CLASS_LOADER);
                    UResourceBundle regions = top.get(kREGIONS);
                    if (zoneIdx < regions.getSize()) {
                        region = regions.getString(zoneIdx);
                    }
                } catch (MissingResourceException e) {
                    // throw away
                }
                if (region != null) {
                    REGION_CACHE.put(tzid, region);
                }
            }
        }
        return region;
    }

    /**
     * Return the canonical country code for this tzid. If we have none, or if the time zone is not
     * associated with a country or unknown, return null.
     */
    public static String getCanonicalCountry(String tzid) {
        String country = getRegion(tzid);
        if (country != null && country.equals(kWorld)) {
            country = null;
        }
        return country;
    }

    /**
     * Return the canonical country code for this tzid. If we have none, or if the time zone is not
     * associated with a country or unknown, return null. When the given zone is the primary zone of
     * the country, true is set to isPrimary.
     */
    public static String getCanonicalCountry(String tzid, Output<Boolean> isPrimary) {
        isPrimary.value = Boolean.FALSE;

        String country = getRegion(tzid);
        if (country != null && country.equals(kWorld)) {
            return null;
        }

        // Check the cache
        Boolean singleZone = SINGLE_COUNTRY_CACHE.get(tzid);
        if (singleZone == null) {
            Set<String> ids =
                    TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null);
            assert (ids.size() >= 1);
            singleZone = ids.size() <= 1;
            SINGLE_COUNTRY_CACHE.put(tzid, singleZone);
        }

        if (singleZone) {
            isPrimary.value = Boolean.TRUE;
        } else {
            // Note: We may cache the primary zone map in future.

            // Even a country has multiple zones, one of them might be
            // dominant and treated as a primary zone.
            try {
                UResourceBundle bundle =
                        UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones");
                UResourceBundle primaryZones = bundle.get("primaryZones");
                String primaryZone = primaryZones.getString(country);
                if (tzid.equals(primaryZone)) {
                    isPrimary.value = Boolean.TRUE;
                } else {
                    // The given ID might not be a canonical ID
                    String canonicalID = getCanonicalCLDRID(tzid);
                    if (canonicalID != null && canonicalID.equals(primaryZone)) {
                        isPrimary.value = Boolean.TRUE;
                    }
                }
            } catch (MissingResourceException e) {
                // ignore
            }
        }

        return country;
    }

    /**
     * Given an ID and the top-level resource of the zoneinfo resource, open the appropriate
     * resource for the given time zone. Dereference links if necessary.
     *
     * @param top the top level resource of the zoneinfo resource or null.
     * @param id zone id
     * @return the corresponding zone resource or null if not found
     */
    public static UResourceBundle openOlsonResource(UResourceBundle top, String id) {
        UResourceBundle res = null;
        int zoneIdx = getZoneIndex(id);
        if (zoneIdx >= 0) {
            try {
                if (top == null) {
                    top =
                            UResourceBundle.getBundleInstance(
                                    ICUData.ICU_BASE_NAME,
                                    ZONEINFORESNAME,
                                    ICUResourceBundle.ICU_DATA_CLASS_LOADER);
                }
                UResourceBundle zones = top.get(kZONES);
                UResourceBundle zone = zones.get(zoneIdx);
                if (zone.getType() == UResourceBundle.INT) {
                    // resolve link
                    zone = zones.get(zone.getInt());
                }
                res = zone;
            } catch (MissingResourceException e) {
                res = null;
            }
        }
        return res;
    }

    /** System time zone object cache */
    private static class SystemTimeZoneCache extends SoftCache<String, OlsonTimeZone, String> {

        /* (non-Javadoc)
         * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
         */
        @Override
        protected OlsonTimeZone createInstance(String key, String data) {
            OlsonTimeZone tz = null;
            try {
                UResourceBundle top =
                        UResourceBundle.getBundleInstance(
                                ICUData.ICU_BASE_NAME,
                                ZONEINFORESNAME,
                                ICUResourceBundle.ICU_DATA_CLASS_LOADER);
                UResourceBundle res = openOlsonResource(top, data);
                if (res != null) {
                    tz = new OlsonTimeZone(top, res, data);
                    tz.freeze();
                }
            } catch (MissingResourceException e) {
                // do nothing
            }
            return tz;
        }
    }

    private static final SystemTimeZoneCache SYSTEM_ZONE_CACHE = new SystemTimeZoneCache();

    /**
     * Returns a frozen OlsonTimeZone instance for the given ID. This method returns null when the
     * given ID is unknown.
     */
    public static OlsonTimeZone getSystemTimeZone(String id) {
        return SYSTEM_ZONE_CACHE.getInstance(id, id);
    }

    // Maximum value of valid custom time zone hour/min
    private static final int kMAX_CUSTOM_HOUR = 23;
    private static final int kMAX_CUSTOM_MIN = 59;
    private static final int kMAX_CUSTOM_SEC = 59;

    /** Custom time zone object cache */
    private static class CustomTimeZoneCache extends SoftCache<Integer, SimpleTimeZone, int[]> {

        /* (non-Javadoc)
         * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
         */
        @Override
        protected SimpleTimeZone createInstance(Integer key, int[] data) {
            assert (data.length == 4);
            assert (data[0] == 1 || data[0] == -1);
            assert (data[1] >= 0 && data[1] <= kMAX_CUSTOM_HOUR);
            assert (data[2] >= 0 && data[2] <= kMAX_CUSTOM_MIN);
            assert (data[3] >= 0 && data[3] <= kMAX_CUSTOM_SEC);
            String id = formatCustomID(data[1], data[2], data[3], data[0] < 0);
            int offset = data[0] * ((data[1] * 60 + data[2]) * 60 + data[3]) * 1000;
            SimpleTimeZone tz = new SimpleTimeZone(offset, id);
            tz.freeze();
            return tz;
        }
    }

    private static final CustomTimeZoneCache CUSTOM_ZONE_CACHE = new CustomTimeZoneCache();

    /**
     * Parse a custom time zone identifier and return a corresponding zone.
     *
     * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or GMT[+-]hh.
     * @return a frozen SimpleTimeZone with the given offset and no Daylight Savings Time, or null
     *     if the id cannot be parsed.
     */
    public static SimpleTimeZone getCustomTimeZone(String id) {
        int[] fields = new int[4];
        if (parseCustomID(id, fields)) {
            // fields[0] - sign
            // fields[1] - hour / 5-bit
            // fields[2] - min  / 6-bit
            // fields[3] - sec  / 6-bit
            Integer key = fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11);
            return CUSTOM_ZONE_CACHE.getInstance(key, fields);
        }
        return null;
    }

    /**
     * Parse a custom time zone identifier and return the normalized custom time zone identifier for
     * the given custom id string.
     *
     * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or GMT[+-]hh.
     * @return The normalized custom id string.
     */
    public static String getCustomID(String id) {
        int[] fields = new int[4];
        if (parseCustomID(id, fields)) {
            return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
        }
        return null;
    }

    /*
     * Parses the given custom time zone identifier
     * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
     * GMT[+-]hh.
     * @param fields An array of int (length = 4) to receive the parsed
     * offset time fields.  The sign is set to fields[0] (-1 or 1),
     * hour is set to fields[1], minute is set to fields[2] and second is
     * set to fields[3].
     * @return Returns true when the given custom id is valid.
     */
    static boolean parseCustomID(String id, int[] fields) {
        if (id != null
                && id.length() > kGMT_ID.length()
                && id.substring(0, 3).equalsIgnoreCase(kGMT_ID)) {
            int sign = 1;
            int hour = 0;
            int min = 0;
            int sec = 0;

            int[] pos = new int[1];
            pos[0] = kGMT_ID.length();
            if (id.charAt(pos[0]) == 0x002D /*'-'*/) {
                sign = -1;
            } else if (id.charAt(pos[0]) != 0x002B /*'+'*/) {
                return false;
            }
            pos[0]++;
            int start = pos[0];
            hour = Utility.parseNumber(id, pos, 10);
            if (pos[0] == id.length()) {
                // Handle the following cases
                // HHmmss
                // Hmmss
                // HHmm
                // Hmm
                // HH
                // H

                // Get all digits
                // Should be 1 to 6 digits.
                int length = pos[0] - start;
                switch (length) {
                    case 1: // H
                    case 2: // HH
                        // already set to hour
                        break;
                    case 3: // Hmm
                    case 4: // HHmm
                        min = hour % 100;
                        hour /= 100;
                        break;
                    case 5: // Hmmss
                    case 6: // HHmmss
                        sec = hour % 100;
                        min = (hour / 100) % 100;
                        hour /= 10000;
                        break;
                    default:
                        // invalid range
                        return false;
                }
            } else {
                // Handle the following cases
                // HH:mm:ss
                // H:mm:ss
                // HH:mm
                // H:mm
                if (pos[0] - start < 1
                        || pos[0] - start > 2
                        || id.charAt(pos[0]) != 0x003A /*':'*/) {
                    return false;
                }
                pos[0]++; // skip : after H
                if (id.length() == pos[0]) {
                    return false;
                }
                start = pos[0];
                min = Utility.parseNumber(id, pos, 10);
                if (pos[0] - start != 2) {
                    return false;
                }
                if (id.length() > pos[0]) {
                    if (id.charAt(pos[0]) != 0x003A /*':'*/) {
                        return false;
                    }
                    pos[0]++; // skip : after mm
                    start = pos[0];
                    sec = Utility.parseNumber(id, pos, 10);
                    if (pos[0] - start != 2 || id.length() > pos[0]) {
                        return false;
                    }
                }
            }

            if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
                if (fields != null) {
                    if (fields.length >= 1) {
                        fields[0] = sign;
                    }
                    if (fields.length >= 2) {
                        fields[1] = hour;
                    }
                    if (fields.length >= 3) {
                        fields[2] = min;
                    }
                    if (fields.length >= 4) {
                        fields[3] = sec;
                    }
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Creates a custom zone for the offset
     *
     * @param offset GMT offset in milliseconds
     * @return A custom TimeZone for the offset with normalized time zone id
     */
    public static SimpleTimeZone getCustomTimeZone(int offset) {
        boolean negative = false;
        int tmp = offset;
        if (offset < 0) {
            negative = true;
            tmp = -offset;
        }

        int hour, min, sec;

        if (ASSERT) {
            Assert.assrt("millis!=0", tmp % 1000 != 0);
        }
        tmp /= 1000;
        sec = tmp % 60;
        tmp /= 60;
        min = tmp % 60;
        hour = tmp / 60;

        // Note: No millisecond part included in TZID for now
        String zid = formatCustomID(hour, min, sec, negative);

        return new SimpleTimeZone(offset, zid);
    }

    /*
     * Returns the normalized custom TimeZone ID
     */
    static String formatCustomID(int hour, int min, int sec, boolean negative) {
        // Create normalized time zone ID - GMT[+|-]hh:mm[:ss]
        StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX);
        if (hour != 0 || min != 0) {
            if (negative) {
                zid.append('-');
            } else {
                zid.append('+');
            }
            // Always use US-ASCII digits
            if (hour < 10) {
                zid.append('0');
            }
            zid.append(hour);
            zid.append(':');
            if (min < 10) {
                zid.append('0');
            }
            zid.append(min);

            if (sec != 0) {
                // Optional second field
                zid.append(':');
                if (sec < 10) {
                    zid.append('0');
                }
                zid.append(sec);
            }
        }
        return zid.toString();
    }

    /**
     * Returns the time zone's short ID for the zone. For example, "uslax" for zone
     * "America/Los_Angeles".
     *
     * @param tz the time zone
     * @return the short ID of the time zone, or null if the short ID is not available.
     */
    public static String getShortID(TimeZone tz) {
        String canonicalID = null;

        if (tz instanceof OlsonTimeZone) {
            canonicalID = ((OlsonTimeZone) tz).getCanonicalID();
        } else {
            canonicalID = getCanonicalCLDRID(tz.getID());
        }
        if (canonicalID == null) {
            return null;
        }
        return getShortIDFromCanonical(canonicalID);
    }

    /**
     * Returns the time zone's short ID for the zone ID. For example, "uslax" for zone ID
     * "America/Los_Angeles".
     *
     * @param id the time zone ID
     * @return the short ID of the time zone ID, or null if the short ID is not available.
     */
    public static String getShortID(String id) {
        String canonicalID = getCanonicalCLDRID(id);
        if (canonicalID == null) {
            return null;
        }
        return getShortIDFromCanonical(canonicalID);
    }

    private static String getShortIDFromCanonical(String canonicalID) {
        String shortID = null;
        String tzidKey = canonicalID.replace('/', ':');

        try {
            // First, try check if the given ID is canonical
            UResourceBundle keyTypeData =
                    UResourceBundle.getBundleInstance(
                            ICUData.ICU_BASE_NAME,
                            "keyTypeData",
                            ICUResourceBundle.ICU_DATA_CLASS_LOADER);
            UResourceBundle typeMap = keyTypeData.get("typeMap");
            UResourceBundle typeKeys = typeMap.get("timezone");
            shortID = typeKeys.getString(tzidKey);
        } catch (MissingResourceException e) {
            // fall through
        }

        return shortID;
    }
}