Home | History | Annotate | Download | only in impl
      1 /* GENERATED SOURCE. DO NOT MODIFY. */
      2 //  2016 and later: Unicode, Inc. and others.
      3 // License & terms of use: http://www.unicode.org/copyright.html#License
      4 /*
      5 **********************************************************************
      6 * Copyright (c) 2003-2016 International Business Machines
      7 * Corporation and others.  All Rights Reserved.
      8 **********************************************************************
      9 * Author: Alan Liu
     10 * Created: September 4 2003
     11 * Since: ICU 2.8
     12 **********************************************************************
     13 */
     14 package android.icu.impl;
     15 
     16 import java.lang.ref.SoftReference;
     17 import java.text.ParsePosition;
     18 import java.util.Collections;
     19 import java.util.Locale;
     20 import java.util.MissingResourceException;
     21 import java.util.Set;
     22 import java.util.TreeSet;
     23 
     24 import android.icu.text.NumberFormat;
     25 import android.icu.util.Output;
     26 import android.icu.util.SimpleTimeZone;
     27 import android.icu.util.TimeZone;
     28 import android.icu.util.TimeZone.SystemTimeZoneType;
     29 import 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  */
     45 public final class ZoneMeta {
     46     private static final boolean ASSERT = false;
     47 
     48     private static final String ZONEINFORESNAME = "zoneinfo64";
     49     private static final String kREGIONS  = "Regions";
     50     private static final String kZONES    = "Zones";
     51     private static final String kNAMES    = "Names";
     52 
     53     private static final String kGMT_ID   = "GMT";
     54     private static final String kCUSTOM_TZ_PREFIX = "GMT";
     55 
     56     private static final String kWorld = "001";
     57 
     58     private static SoftReference<Set<String>> REF_SYSTEM_ZONES;
     59     private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES;
     60     private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES;
     61 
     62     /**
     63      * Returns an immutable set of system time zone IDs.
     64      * Etc/Unknown is excluded.
     65      * @return An immutable set of system time zone IDs.
     66      */
     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