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