1 /*
2  *******************************************************************************
3  * Copyright (C) 2011-2014, International Business Machines Corporation and    *
4  * others. All Rights Reserved.                                                *
5  *******************************************************************************
6  */
7 package com.ibm.icu.text;
8 
9 import java.io.IOException;
10 import java.io.InvalidObjectException;
11 import java.io.ObjectInputStream;
12 import java.io.ObjectOutputStream;
13 import java.io.ObjectStreamField;
14 import java.io.Serializable;
15 import java.text.AttributedCharacterIterator;
16 import java.text.AttributedString;
17 import java.text.FieldPosition;
18 import java.text.ParseException;
19 import java.text.ParsePosition;
20 import java.util.ArrayList;
21 import java.util.BitSet;
22 import java.util.Collection;
23 import java.util.Date;
24 import java.util.EnumSet;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.MissingResourceException;
29 import java.util.Set;
30 
31 import com.ibm.icu.impl.ICUResourceBundle;
32 import com.ibm.icu.impl.SoftCache;
33 import com.ibm.icu.impl.TZDBTimeZoneNames;
34 import com.ibm.icu.impl.TextTrieMap;
35 import com.ibm.icu.impl.TimeZoneGenericNames;
36 import com.ibm.icu.impl.TimeZoneGenericNames.GenericMatchInfo;
37 import com.ibm.icu.impl.TimeZoneGenericNames.GenericNameType;
38 import com.ibm.icu.impl.TimeZoneNamesImpl;
39 import com.ibm.icu.impl.ZoneMeta;
40 import com.ibm.icu.lang.UCharacter;
41 import com.ibm.icu.text.TimeZoneNames.MatchInfo;
42 import com.ibm.icu.text.TimeZoneNames.NameType;
43 import com.ibm.icu.util.Calendar;
44 import com.ibm.icu.util.Freezable;
45 import com.ibm.icu.util.Output;
46 import com.ibm.icu.util.TimeZone;
47 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
48 import com.ibm.icu.util.ULocale;
49 
50 /**
51  * <code>TimeZoneFormat</code> supports time zone display name formatting and parsing.
52  * An instance of TimeZoneFormat works as a subformatter of {@link SimpleDateFormat},
53  * but you can also directly get a new instance of <code>TimeZoneFormat</code> and
54  * formatting/parsing time zone display names.
55  * <p>
56  * ICU implements the time zone display names defined by <a href="http://www.unicode.org/reports/tr35/">UTS#35
57  * Unicode Locale Data Markup Language (LDML)</a>. {@link TimeZoneNames} represents the
58  * time zone display name data model and this class implements the algorithm for actual
59  * formatting and parsing.
60  *
61  * @see SimpleDateFormat
62  * @see TimeZoneNames
63  * @stable ICU 49
64  */
65 public class TimeZoneFormat extends UFormat implements Freezable<TimeZoneFormat>, Serializable {
66 
67     private static final long serialVersionUID = 2281246852693575022L;
68 
69     private static final int ISO_Z_STYLE_FLAG = 0x0080;
70     private static final int ISO_LOCAL_STYLE_FLAG = 0x0100;
71 
72     /**
73      * Time zone display format style enum used by format/parse APIs in <code>TimeZoneFormat</code>.
74      *
75      * @see TimeZoneFormat#format(Style, TimeZone, long)
76      * @see TimeZoneFormat#format(Style, TimeZone, long, Output)
77      * @see TimeZoneFormat#parse(Style, String, ParsePosition, Output)
78      * @stable ICU 49
79      */
80     public enum Style {
81         /**
82          * Generic location format, such as "United States Time (New York)" and "Italy Time".
83          * This style is equivalent to the LDML date format pattern "VVVV".
84          * @stable ICU 49
85          */
86         GENERIC_LOCATION (0x0001),
87         /**
88          * Generic long non-location format, such as "Eastern Time".
89          * This style is equivalent to the LDML date format pattern "vvvv".
90          * @stable ICU 49
91          */
92         GENERIC_LONG (0x0002),
93         /**
94          * Generic short non-location format, such as "ET".
95          * This style is equivalent to the LDML date format pattern "v".
96          * @stable ICU 49
97          */
98         GENERIC_SHORT (0x0004),
99         /**
100          * Specific long format, such as "Eastern Standard Time".
101          * This style is equivalent to the LDML date format pattern "zzzz".
102          * @stable ICU 49
103          */
104         SPECIFIC_LONG (0x0008),
105         /**
106          * Specific short format, such as "EST", "PDT".
107          * This style is equivalent to the LDML date format pattern "z".
108          * @stable ICU 49
109          */
110         SPECIFIC_SHORT (0x0010),
111         /**
112          * Localized GMT offset format, such as "GMT-05:00", "UTC+0100"
113          * This style is equivalent to the LDML date format pattern "OOOO" and "ZZZZ"
114          * @stable ICU 49
115          */
116         LOCALIZED_GMT (0x0020),
117         /**
118          * Short localized GMT offset format, such as "GMT-5", "UTC+1:30"
119          * This style is equivalent to the LDML date format pattern "O".
120          * @stable ICU 51
121          */
122         LOCALIZED_GMT_SHORT (0x0040),
123         /**
124          * Short ISO 8601 local time difference (basic format) or the UTC indicator.
125          * For example, "-05", "+0530", and "Z"(UTC).
126          * This style is equivalent to the LDML date format pattern "X".
127          * @stable ICU 51
128          */
129         ISO_BASIC_SHORT (ISO_Z_STYLE_FLAG),
130         /**
131          * Short ISO 8601 locale time difference (basic format).
132          * For example, "-05" and "+0530".
133          * This style is equivalent to the LDML date format pattern "x".
134          * @stable ICU 51
135          */
136         ISO_BASIC_LOCAL_SHORT (ISO_LOCAL_STYLE_FLAG),
137         /**
138          * Fixed width ISO 8601 local time difference (basic format) or the UTC indicator.
139          * For example, "-0500", "+0530", and "Z"(UTC).
140          * This style is equivalent to the LDML date format pattern "XX".
141          * @stable ICU 51
142          */
143         ISO_BASIC_FIXED (ISO_Z_STYLE_FLAG),
144         /**
145          * Fixed width ISO 8601 local time difference (basic format).
146          * For example, "-0500" and "+0530".
147          * This style is equivalent to the LDML date format pattern "xx".
148          * @stable ICU 51
149          */
150         ISO_BASIC_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG),
151         /**
152          * ISO 8601 local time difference (basic format) with optional seconds field, or the UTC indicator.
153          * For example, "-0500", "+052538", and "Z"(UTC).
154          * This style is equivalent to the LDML date format pattern "XXXX".
155          * @stable ICU 51
156          */
157         ISO_BASIC_FULL (ISO_Z_STYLE_FLAG),
158         /**
159          * ISO 8601 local time difference (basic format) with optional seconds field.
160          * For example, "-0500" and "+052538".
161          * This style is equivalent to the LDML date format pattern "xxxx".
162          * @stable ICU 51
163          */
164         ISO_BASIC_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG),
165         /**
166          * Fixed width ISO 8601 local time difference (extended format) or the UTC indicator.
167          * For example, "-05:00", "+05:30", and "Z"(UTC).
168          * This style is equivalent to the LDML date format pattern "XXX".
169          * @stable ICU 51
170          */
171         ISO_EXTENDED_FIXED (ISO_Z_STYLE_FLAG),
172         /**
173          * Fixed width ISO 8601 local time difference (extended format).
174          * For example, "-05:00" and "+05:30".
175          * This style is equivalent to the LDML date format pattern "xxx" and "ZZZZZ".
176          * @stable ICU 51
177          */
178         ISO_EXTENDED_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG),
179         /**
180          * ISO 8601 local time difference (extended format) with optional seconds field, or the UTC indicator.
181          * For example, "-05:00", "+05:25:38", and "Z"(UTC).
182          * This style is equivalent to the LDML date format pattern "XXXXX".
183          * @stable ICU 51
184          */
185         ISO_EXTENDED_FULL (ISO_Z_STYLE_FLAG),
186         /**
187          * ISO 8601 local time difference (extended format) with optional seconds field.
188          * For example, "-05:00" and "+05:25:38".
189          * This style is equivalent to the LDML date format pattern "xxxxx".
190          * @stable ICU 51
191          */
192         ISO_EXTENDED_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG),
193         /**
194          * Time Zone ID, such as "America/Los_Angeles".
195          * @stable ICU 51
196          */
197         ZONE_ID (0x0200),
198         /**
199          * Short Time Zone ID (BCP 47 Unicode location extension, time zone type value), such as "uslax".
200          * @stable ICU 51
201          */
202         ZONE_ID_SHORT (0x0400),
203         /**
204          * Exemplar location, such as "Los Angeles" and "Paris".
205          * @stable ICU 51
206          */
207         EXEMPLAR_LOCATION (0x0800);
208 
209         final int flag;
210 
Style(int flag)211         private Style(int flag) {
212             this.flag = flag;
213         }
214     }
215 
216     /**
217      * Offset pattern type enum.
218      *
219      * @see TimeZoneFormat#getGMTOffsetPattern(GMTOffsetPatternType)
220      * @see TimeZoneFormat#setGMTOffsetPattern(GMTOffsetPatternType, String)
221      * @stable ICU 49
222      */
223     public enum GMTOffsetPatternType {
224         /**
225          * Positive offset with hours and minutes fields
226          * @stable ICU 49
227          */
228         POSITIVE_HM ("+H:mm", "Hm", true),
229         /**
230          * Positive offset with hours, minutes and seconds fields
231          * @stable ICU 49
232          */
233         POSITIVE_HMS ("+H:mm:ss", "Hms", true),
234         /**
235          * Negative offset with hours and minutes fields
236          * @stable ICU 49
237          */
238         NEGATIVE_HM ("-H:mm", "Hm", false),
239         /**
240          * Negative offset with hours, minutes and seconds fields
241          * @stable ICU 49
242          */
243         NEGATIVE_HMS ("-H:mm:ss", "Hms", false),
244         /**
245          * Positive offset with hours field
246          * @stable ICU 51
247          */
248         POSITIVE_H ("+H", "H", true),
249         /**
250          * Negative offset with hours field
251          * @stable ICU 51
252          */
253         NEGATIVE_H ("-H", "H", false);
254 
255         private String _defaultPattern;
256         private String _required;
257         private boolean _isPositive;
258 
GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive)259         private GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive) {
260             _defaultPattern = defaultPattern;
261             _required = required;
262             _isPositive = isPositive;
263         }
264 
defaultPattern()265         private String defaultPattern() {
266             return _defaultPattern;
267         }
268 
required()269         private String required() {
270             return _required;
271         }
272 
isPositive()273         private boolean isPositive() {
274             return _isPositive;
275         }
276     }
277 
278     /**
279      * Time type enum used for receiving time type (standard time, daylight time or unknown)
280      * in <code>TimeZoneFormat</code> APIs.
281      *
282      * @stable ICU 49
283      */
284     public enum TimeType {
285         /**
286          * Unknown
287          * @stable ICU 49
288          */
289         UNKNOWN,
290         /**
291          * Standard time
292          * @stable ICU 49
293          */
294         STANDARD,
295         /**
296          * Daylight saving time
297          * @stable ICU 49
298          */
299         DAYLIGHT;
300     }
301 
302     /**
303      * Parse option enum, used for specifying optional parse behavior.
304      * @stable ICU 49
305      */
306     public enum ParseOption {
307         /**
308          * When a time zone display name is not found within a set of display names
309          * used for the specified style, look for the name from display names used
310          * by other styles.
311          * @stable ICU 49
312          */
313         ALL_STYLES,
314         /**
315          * When parsing a time zone display name in {@link Style#SPECIFIC_SHORT},
316          * look for the IANA tz database compatible zone abbreviations in addition
317          * to the localized names coming from the {@link TimeZoneNames} currently
318          * used by the {@link TimeZoneFormat}.
319          * @draft ICU 54
320          * @provisional This API might change or be removed in a future release.
321          */
322         TZ_DATABASE_ABBREVIATIONS;
323     }
324 
325     /*
326      * fields to be serialized
327      */
328     private ULocale _locale;
329     private TimeZoneNames _tznames;
330     private String _gmtPattern;
331     private String[] _gmtOffsetPatterns;
332     private String[] _gmtOffsetDigits;
333     private String _gmtZeroFormat;
334     private boolean _parseAllStyles;
335     private boolean _parseTZDBNames;
336 
337     /*
338      * Transient fields
339      */
340     private transient volatile TimeZoneGenericNames _gnames;
341 
342     private transient String _gmtPatternPrefix;
343     private transient String _gmtPatternSuffix;
344     private transient Object[][] _gmtOffsetPatternItems;
345     // cache if offset hours and minutes are abutting
346     private transient boolean _abuttingOffsetHoursAndMinutes;
347 
348     private transient String _region;
349 
350     private volatile transient boolean _frozen;
351 
352     private transient volatile TimeZoneNames _tzdbNames;
353 
354     /*
355      * Static final fields
356      */
357     private static final String TZID_GMT = "Etc/GMT"; // canonical tzid for GMT
358 
359     private static final String[] ALT_GMT_STRINGS = {"GMT", "UTC", "UT"};
360 
361     private static final String DEFAULT_GMT_PATTERN = "GMT{0}";
362     private static final String DEFAULT_GMT_ZERO = "GMT";
363     private static final String[] DEFAULT_GMT_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
364     private static final char DEFAULT_GMT_OFFSET_SEP = ':';
365     private static final String ASCII_DIGITS = "0123456789";
366     private static final String ISO8601_UTC = "Z";
367 
368     private static final String UNKNOWN_ZONE_ID = "Etc/Unknown";
369     private static final String UNKNOWN_SHORT_ZONE_ID = "unk";
370     private static final String UNKNOWN_LOCATION = "Unknown";
371 
372     // Order of GMT offset pattern parsing, *_HMS must be evaluated first
373     // because *_HM is most likely a substring of *_HMS
374     private static final GMTOffsetPatternType[] PARSE_GMT_OFFSET_TYPES = {
375         GMTOffsetPatternType.POSITIVE_HMS, GMTOffsetPatternType.NEGATIVE_HMS,
376         GMTOffsetPatternType.POSITIVE_HM, GMTOffsetPatternType.NEGATIVE_HM,
377         GMTOffsetPatternType.POSITIVE_H, GMTOffsetPatternType.NEGATIVE_H,
378     };
379 
380     private static final int MILLIS_PER_HOUR = 60 * 60 * 1000;
381     private static final int MILLIS_PER_MINUTE = 60 * 1000;
382     private static final int MILLIS_PER_SECOND = 1000;
383 
384     // Maximum offset (exclusive) in millisecond supported by offset formats
385     private static final int MAX_OFFSET = 24 * MILLIS_PER_HOUR;
386 
387     // Maximum values for GMT offset fields
388     private static final int MAX_OFFSET_HOUR = 23;
389     private static final int MAX_OFFSET_MINUTE = 59;
390     private static final int MAX_OFFSET_SECOND = 59;
391 
392     private static final int UNKNOWN_OFFSET = Integer.MAX_VALUE;
393 
394     private static TimeZoneFormatCache _tzfCache = new TimeZoneFormatCache();
395 
396     // The filter used for searching all specific names and exemplar location names
397     private static final EnumSet<NameType> ALL_SIMPLE_NAME_TYPES = EnumSet.of(
398         NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT,
399         NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT,
400         NameType.EXEMPLAR_LOCATION
401     );
402 
403     // The filter used for searching all generic names
404     private static final EnumSet<GenericNameType> ALL_GENERIC_NAME_TYPES = EnumSet.of(
405         GenericNameType.LOCATION, GenericNameType.LONG, GenericNameType.SHORT
406     );
407 
408     private static volatile TextTrieMap<String> ZONE_ID_TRIE;
409     private static volatile TextTrieMap<String> SHORT_ZONE_ID_TRIE;
410 
411     /**
412      * The protected constructor for subclassing.
413      * @param locale the locale
414      * @stable ICU 49
415      */
TimeZoneFormat(ULocale locale)416     protected TimeZoneFormat(ULocale locale) {
417         _locale = locale;
418         _tznames = TimeZoneNames.getInstance(locale);
419         // TimeZoneGenericNames _gnames will be instantiated lazily
420 
421         String gmtPattern = null;
422         String hourFormats = null;
423         _gmtZeroFormat = DEFAULT_GMT_ZERO;
424 
425         try {
426             ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance(
427                     ICUResourceBundle.ICU_ZONE_BASE_NAME, locale);
428             try {
429                 gmtPattern = bundle.getStringWithFallback("zoneStrings/gmtFormat");
430             } catch (MissingResourceException e) {
431                 // fall through
432             }
433             try {
434                 hourFormats = bundle.getStringWithFallback("zoneStrings/hourFormat");
435             } catch (MissingResourceException e) {
436                 // fall through
437             }
438             try {
439                 _gmtZeroFormat = bundle.getStringWithFallback("zoneStrings/gmtZeroFormat");
440             } catch (MissingResourceException e) {
441                 // fall through
442             }
443         } catch (MissingResourceException e) {
444             // fall through
445         }
446 
447         if (gmtPattern == null) {
448             gmtPattern = DEFAULT_GMT_PATTERN;
449         }
450         initGMTPattern(gmtPattern);
451 
452         String[] gmtOffsetPatterns = new String[GMTOffsetPatternType.values().length];
453         if (hourFormats != null) {
454             String[] hourPatterns = hourFormats.split(";", 2);
455             gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[0]);
456             gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()] = hourPatterns[0];
457             gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[0]);
458             gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[1]);
459             gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()] = hourPatterns[1];
460             gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[1]);
461         } else {
462             for (GMTOffsetPatternType patType : GMTOffsetPatternType.values()) {
463                 gmtOffsetPatterns[patType.ordinal()] = patType.defaultPattern();
464             }
465         }
466         initGMTOffsetPatterns(gmtOffsetPatterns);
467 
468         _gmtOffsetDigits = DEFAULT_GMT_DIGITS;
469         NumberingSystem ns = NumberingSystem.getInstance(locale);
470         if (!ns.isAlgorithmic()) {
471             // we do not support algorithmic numbering system for GMT offset for now
472             _gmtOffsetDigits = toCodePoints(ns.getDescription());
473         }
474     }
475 
476     /**
477      * Returns a frozen instance of <code>TimeZoneFormat</code> for the given locale.
478      * <p><b>Note</b>: The instance returned by this method is frozen. If you want to
479      * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a
480      * thawed copy first.
481      *
482      * @param locale the locale.
483      * @return a frozen instance of <code>TimeZoneFormat</code> for the given locale.
484      * @stable ICU 49
485      */
getInstance(ULocale locale)486     public static TimeZoneFormat getInstance(ULocale locale) {
487         if (locale == null) {
488             throw new NullPointerException("locale is null");
489         }
490         return _tzfCache.getInstance(locale, locale);
491     }
492 
493     /**
494      * Returns a frozen instance of <code>TimeZoneFormat</code> for the given JDK locale.
495      * <p><b>Note</b>: The instance returned by this method is frozen. If you want to
496      * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a
497      * thawed copy first.
498      *
499      * @param locale the JDK locale.
500      * @return a frozen instance of <code>TimeZoneFormat</code> for the given locale.
501      * @draft ICU 54
502      * @provisional This API might change or be removed in a future release.
503      */
getInstance(Locale locale)504     public static TimeZoneFormat getInstance(Locale locale) {
505         return getInstance(ULocale.forLocale(locale));
506     }
507 
508     /**
509      * Returns the time zone display name data used by this instance.
510      *
511      * @return the time zone display name data.
512      * @see #setTimeZoneNames(TimeZoneNames)
513      * @stable ICU 49
514      */
getTimeZoneNames()515     public TimeZoneNames getTimeZoneNames() {
516         return _tznames;
517     }
518 
519     /**
520      * Private method returning the instance of TimeZoneGenericNames
521      * used by this object. The instance of TimeZoneGenericNames might
522      * not be available until the first use (lazy instantiation) because
523      * it is only required for handling generic names (that are not used
524      * by DateFormat's default patterns) and it requires relatively heavy
525      * one time initialization.
526      * @return the instance of TimeZoneGenericNames used by this object.
527      */
getTimeZoneGenericNames()528     private TimeZoneGenericNames getTimeZoneGenericNames() {
529         if (_gnames == null) { // _gnames is volatile
530             synchronized(this) {
531                 if (_gnames == null) {
532                     _gnames = TimeZoneGenericNames.getInstance(_locale);
533                 }
534             }
535         }
536         return _gnames;
537     }
538 
539     /**
540      * Private method returning the instance of TZDBTimeZoneNames.
541      * The instance if used only for parsing when {@link ParseOption#TZ_DATABASE_ABBREVIATIONS}
542      * is enabled.
543      * @return an instance of TZDBTimeZoneNames.
544      */
getTZDBTimeZoneNames()545     private TimeZoneNames getTZDBTimeZoneNames() {
546         if (_tzdbNames == null) {
547             synchronized(this) {
548                 if (_tzdbNames == null) {
549                     _tzdbNames = new TZDBTimeZoneNames(_locale);
550                 }
551             }
552         }
553         return _tzdbNames;
554     }
555 
556     /**
557      * Sets the time zone display name data to this instance.
558      *
559      * @param tznames the time zone display name data.
560      * @return this object.
561      * @throws UnsupportedOperationException when this object is frozen.
562      * @see #getTimeZoneNames()
563      * @stable ICU 49
564      */
setTimeZoneNames(TimeZoneNames tznames)565     public TimeZoneFormat setTimeZoneNames(TimeZoneNames tznames) {
566         if (isFrozen()) {
567             throw new UnsupportedOperationException("Attempt to modify frozen object");
568         }
569        _tznames = tznames;
570        // TimeZoneGenericNames must be changed to utilize the new TimeZoneNames instance.
571        _gnames = new TimeZoneGenericNames(_locale, _tznames);
572        return this;
573     }
574 
575     /**
576      * Returns the localized GMT format pattern.
577      *
578      * @return the localized GMT format pattern.
579      * @see #setGMTPattern(String)
580      * @stable ICU 49
581      */
getGMTPattern()582     public String getGMTPattern() {
583         return _gmtPattern;
584     }
585 
586     /**
587      * Sets the localized GMT format pattern. The pattern must contain
588      * a single argument {0}, for example "GMT {0}".
589      *
590      * @param pattern the localized GMT format pattern string
591      * @return this object.
592      * @throws IllegalArgumentException when the pattern string does not contain "{0}"
593      * @throws UnsupportedOperationException when this object is frozen.
594      * @see #getGMTPattern()
595      * @stable ICU 49
596      */
setGMTPattern(String pattern)597     public TimeZoneFormat setGMTPattern(String pattern) {
598         if (isFrozen()) {
599             throw new UnsupportedOperationException("Attempt to modify frozen object");
600         }
601         initGMTPattern(pattern);
602         return this;
603     }
604 
605     /**
606      * Returns the offset pattern used for localized GMT format.
607      *
608      * @param type the offset pattern enum
609      * @see #setGMTOffsetPattern(GMTOffsetPatternType, String)
610      * @stable ICU 49
611      */
getGMTOffsetPattern(GMTOffsetPatternType type)612     public String getGMTOffsetPattern(GMTOffsetPatternType type) {
613         return _gmtOffsetPatterns[type.ordinal()];
614     }
615 
616     /**
617      * Sets the offset pattern for the given offset type.
618      *
619      * @param type the offset pattern.
620      * @param pattern the pattern string.
621      * @return this object.
622      * @throws IllegalArgumentException when the pattern string does not have required time field letters.
623      * @throws UnsupportedOperationException when this object is frozen.
624      * @see #getGMTOffsetPattern(GMTOffsetPatternType)
625      * @stable ICU 49
626      */
setGMTOffsetPattern(GMTOffsetPatternType type, String pattern)627     public TimeZoneFormat setGMTOffsetPattern(GMTOffsetPatternType type, String pattern) {
628         if (isFrozen()) {
629             throw new UnsupportedOperationException("Attempt to modify frozen object");
630         }
631         if (pattern == null) {
632             throw new NullPointerException("Null GMT offset pattern");
633         }
634 
635         Object[] parsedItems = parseOffsetPattern(pattern, type.required());
636 
637         _gmtOffsetPatterns[type.ordinal()] = pattern;
638         _gmtOffsetPatternItems[type.ordinal()] = parsedItems;
639         checkAbuttingHoursAndMinutes();
640 
641         return this;
642     }
643 
644     /**
645      * Returns the decimal digit characters used for localized GMT format in a single string
646      * containing from 0 to 9 in the ascending order.
647      *
648      * @return the decimal digits for localized GMT format.
649      * @see #setGMTOffsetDigits(String)
650      * @stable ICU 49
651      */
getGMTOffsetDigits()652     public String getGMTOffsetDigits() {
653         StringBuilder buf = new StringBuilder(_gmtOffsetDigits.length);
654         for (String digit : _gmtOffsetDigits) {
655             buf.append(digit);
656         }
657         return buf.toString();
658     }
659 
660     /**
661      * Sets the decimal digit characters used for localized GMT format.
662      *
663      * @param digits a string contains the decimal digit characters from 0 to 9 n the ascending order.
664      * @return this object.
665      * @throws IllegalArgumentException when the string did not contain ten characters.
666      * @throws UnsupportedOperationException when this object is frozen.
667      * @see #getGMTOffsetDigits()
668      * @stable ICU 49
669      */
setGMTOffsetDigits(String digits)670     public TimeZoneFormat setGMTOffsetDigits(String digits) {
671         if (isFrozen()) {
672             throw new UnsupportedOperationException("Attempt to modify frozen object");
673         }
674         if (digits == null) {
675             throw new NullPointerException("Null GMT offset digits");
676         }
677         String[] digitArray = toCodePoints(digits);
678         if (digitArray.length != 10) {
679             throw new IllegalArgumentException("Length of digits must be 10");
680         }
681         _gmtOffsetDigits = digitArray;
682         return this;
683     }
684 
685     /**
686      * Returns the localized GMT format string for GMT(UTC) itself (GMT offset is 0).
687      *
688      * @return the localized GMT string string for GMT(UTC) itself.
689      * @see #setGMTZeroFormat(String)
690      * @stable ICU 49
691      */
getGMTZeroFormat()692     public String getGMTZeroFormat() {
693         return _gmtZeroFormat;
694     }
695 
696     /**
697      * Sets the localized GMT format string for GMT(UTC) itself (GMT offset is 0).
698      *
699      * @param gmtZeroFormat the localized GMT format string for GMT(UTC).
700      * @return this object.
701      * @throws UnsupportedOperationException when this object is frozen.
702      * @see #getGMTZeroFormat()
703      * @stable ICU 49
704      */
setGMTZeroFormat(String gmtZeroFormat)705     public TimeZoneFormat setGMTZeroFormat(String gmtZeroFormat) {
706         if (isFrozen()) {
707             throw new UnsupportedOperationException("Attempt to modify frozen object");
708         }
709         if (gmtZeroFormat == null) {
710             throw new NullPointerException("Null GMT zero format");
711         }
712         if (gmtZeroFormat.length() == 0) {
713             throw new IllegalArgumentException("Empty GMT zero format");
714         }
715         _gmtZeroFormat = gmtZeroFormat;
716         return this;
717     }
718 
719     /**
720      * Sets the default parse options.
721      * <p>
722      * <b>Note:</b> By default, an instance of <code>TimeZoneFormat></code>
723      * created by {#link {@link #getInstance(ULocale)} has no parse options set.
724      *
725      * @param options the default parse options.
726      * @return this object.
727      * @see ParseOption
728      * @stable ICU 49
729      */
setDefaultParseOptions(EnumSet<ParseOption> options)730     public TimeZoneFormat setDefaultParseOptions(EnumSet<ParseOption> options) {
731         _parseAllStyles = options.contains(ParseOption.ALL_STYLES);
732         _parseTZDBNames = options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS);
733         return this;
734     }
735 
736     /**
737      * Returns the default parse options used by this <code>TimeZoneFormat</code> instance.
738      * @return the default parse options.
739      * @see ParseOption
740      * @stable ICU 49
741      */
getDefaultParseOptions()742     public EnumSet<ParseOption> getDefaultParseOptions() {
743         if (_parseAllStyles && _parseTZDBNames) {
744             return EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS);
745         } else if (_parseAllStyles) {
746             return EnumSet.of(ParseOption.ALL_STYLES);
747         } else if (_parseTZDBNames) {
748             return EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS);
749         }
750         return EnumSet.noneOf(ParseOption.class);
751     }
752 
753     /**
754      * Returns the ISO 8601 basic time zone string for the given offset.
755      * For example, "-08", "-0830" and "Z"
756      *
757      * @param offset the offset from GMT(UTC) in milliseconds.
758      * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0.
759      * @param isShort true if shortest form is used.
760      * @param ignoreSeconds true if non-zero offset seconds is appended.
761      * @return the ISO 8601 basic format.
762      * @throws IllegalArgumentException if the specified offset is out of supported range
763      * (-24 hours &lt; offset &lt; +24 hours).
764      * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean)
765      * @see #parseOffsetISO8601(String, ParsePosition)
766      * @stable ICU 51
767      */
formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds)768     public final String formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
769         return formatOffsetISO8601(offset, true, useUtcIndicator, isShort, ignoreSeconds);
770     }
771 
772     /**
773      * Returns the ISO 8601 extended time zone string for the given offset.
774      * For example, "-08:00", "-08:30" and "Z"
775      *
776      * @param offset the offset from GMT(UTC) in milliseconds.
777      * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0.
778      * @param isShort true if shortest form is used.
779      * @param ignoreSeconds true if non-zero offset seconds is appended.
780      * @return the ISO 8601 extended format.
781      * @throws IllegalArgumentException if the specified offset is out of supported range
782      * (-24 hours &lt; offset &lt; +24 hours).
783      * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean)
784      * @see #parseOffsetISO8601(String, ParsePosition)
785      * @stable ICU 51
786      */
formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds)787     public final String formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
788         return formatOffsetISO8601(offset, false, useUtcIndicator, isShort, ignoreSeconds);
789     }
790 
791     /**
792      * Returns the localized GMT(UTC) offset format for the given offset.
793      * The localized GMT offset is defined by;
794      * <ul>
795      * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()})
796      * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)})
797      * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()})
798      * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()})
799      * </ul>
800      * This format always uses 2 digit hours and minutes. When the given offset has non-zero
801      * seconds, 2 digit seconds field will be appended. For example,
802      * GMT+05:00 and GMT+05:28:06.
803      * @param offset the offset from GMT(UTC) in milliseconds.
804      * @return the localized GMT format string
805      * @see #parseOffsetLocalizedGMT(String, ParsePosition)
806      * @throws IllegalArgumentException if the specified offset is out of supported range
807      * (-24 hours &lt; offset &lt; +24 hours).
808      * @stable ICU 49
809      */
formatOffsetLocalizedGMT(int offset)810     public String formatOffsetLocalizedGMT(int offset) {
811         return formatOffsetLocalizedGMT(offset, false);
812     }
813 
814     /**
815      * Returns the short localized GMT(UTC) offset format for the given offset.
816      * The short localized GMT offset is defined by;
817      * <ul>
818      * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()})
819      * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)})
820      * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()})
821      * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()})
822      * </ul>
823      * This format uses the shortest representation of offset. The hours field does not
824      * have leading zero and lower fields with zero will be truncated. For example,
825      * GMT+5 and GMT+530.
826      * @param offset the offset from GMT(UTC) in milliseconds.
827      * @return the short localized GMT format string
828      * @see #parseOffsetLocalizedGMT(String, ParsePosition)
829      * @throws IllegalArgumentException if the specified offset is out of supported range
830      * (-24 hours &lt; offset &lt; +24 hours).
831      * @stable ICU 51
832      */
formatOffsetShortLocalizedGMT(int offset)833     public String formatOffsetShortLocalizedGMT(int offset) {
834         return formatOffsetLocalizedGMT(offset, true);
835     }
836 
837     /**
838      * Returns the display name of the time zone at the given date for
839      * the style.
840      *
841      * <p><b>Note</b>: A style may have fallback styles defined. For example,
842      * when <code>GENERIC_LONG</code> is requested, but there is no display name
843      * data available for <code>GENERIC_LONG</code> style, the implementation
844      * may use <code>GENERIC_LOCATION</code> or <code>LOCALIZED_GMT</code>.
845      * See UTS#35 UNICODE LOCALE DATA MARKUP LANGUAGE (LDML)
846      * <a href="http://www.unicode.org/reports/tr35/#Time_Zone_Fallback">Appendix J: Time Zone Display Name</a>
847      * for the details.
848      *
849      * @param style the style enum (e.g. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...)
850      * @param tz the time zone.
851      * @param date the date.
852      * @return the display name of the time zone.
853      * @see Style
854      * @see #format(Style, TimeZone, long, Output)
855      * @stable ICU 49
856      */
format(Style style, TimeZone tz, long date)857     public final String format(Style style, TimeZone tz, long date) {
858         return format(style, tz, date, null);
859     }
860 
861     /**
862      * Returns the display name of the time zone at the given date for
863      * the style. This method takes an extra argument <code>Output&lt;TimeType&gt; timeType</code>
864      * in addition to the argument list of {@link #format(Style, TimeZone, long)}.
865      * The argument is used for receiving the time type (standard time
866      * or daylight saving time, or unknown) actually used for the display name.
867      *
868      * @param style the style enum (e.g. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...)
869      * @param tz the time zone.
870      * @param date the date.
871      * @param timeType the output argument for receiving the time type (standard/daylight/unknown)
872      * used for the display name, or specify null if the information is not necessary.
873      * @return the display name of the time zone.
874      * @see Style
875      * @see #format(Style, TimeZone, long)
876      * @stable ICU 49
877      */
format(Style style, TimeZone tz, long date, Output<TimeType> timeType)878     public String format(Style style, TimeZone tz, long date, Output<TimeType> timeType) {
879         String result = null;
880 
881         if (timeType != null) {
882             timeType.value = TimeType.UNKNOWN;
883         }
884 
885         boolean noOffsetFormatFallback = false;
886 
887         switch (style) {
888         case GENERIC_LOCATION:
889             result = getTimeZoneGenericNames().getGenericLocationName(ZoneMeta.getCanonicalCLDRID(tz));
890             break;
891         case GENERIC_LONG:
892             result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.LONG, date);
893             break;
894         case GENERIC_SHORT:
895             result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.SHORT, date);
896             break;
897         case SPECIFIC_LONG:
898             result = formatSpecific(tz, NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, date, timeType);
899             break;
900         case SPECIFIC_SHORT:
901             result = formatSpecific(tz, NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, date, timeType);
902             break;
903 
904         case ZONE_ID:
905             result = tz.getID();
906             noOffsetFormatFallback = true;
907             break;
908         case ZONE_ID_SHORT:
909             result = ZoneMeta.getShortID(tz);
910             if (result == null) {
911                 result = UNKNOWN_SHORT_ZONE_ID;
912             }
913             noOffsetFormatFallback = true;
914             break;
915         case EXEMPLAR_LOCATION:
916             result = formatExemplarLocation(tz);
917             noOffsetFormatFallback = true;
918             break;
919 
920         default:
921             // will be handled below
922             break;
923         }
924 
925         if (result == null && !noOffsetFormatFallback) {
926             int[] offsets = {0, 0};
927             tz.getOffset(date, false, offsets);
928             int offset = offsets[0] + offsets[1];
929 
930             switch (style) {
931             case GENERIC_LOCATION:
932             case GENERIC_LONG:
933             case SPECIFIC_LONG:
934             case LOCALIZED_GMT:
935                 result = formatOffsetLocalizedGMT(offset);
936                 break;
937 
938             case GENERIC_SHORT:
939             case SPECIFIC_SHORT:
940             case LOCALIZED_GMT_SHORT:
941                 result = formatOffsetShortLocalizedGMT(offset);
942                 break;
943 
944             case ISO_BASIC_SHORT:
945                 result = formatOffsetISO8601Basic(offset, true, true, true);
946                 break;
947 
948             case ISO_BASIC_LOCAL_SHORT:
949                 result = formatOffsetISO8601Basic(offset, false, true, true);
950                 break;
951 
952             case ISO_BASIC_FIXED:
953                 result = formatOffsetISO8601Basic(offset, true, false, true);
954                 break;
955 
956             case ISO_BASIC_LOCAL_FIXED:
957                 result = formatOffsetISO8601Basic(offset, false, false, true);
958                 break;
959 
960             case ISO_BASIC_FULL:
961                 result = formatOffsetISO8601Basic(offset, true, false, false);
962                 break;
963 
964             case ISO_BASIC_LOCAL_FULL:
965                 result = formatOffsetISO8601Basic(offset, false, false, false);
966                 break;
967 
968             case ISO_EXTENDED_FIXED:
969                 result = formatOffsetISO8601Extended(offset, true, false, true);
970                 break;
971 
972             case ISO_EXTENDED_LOCAL_FIXED:
973                 result = formatOffsetISO8601Extended(offset, false, false, true);
974                 break;
975 
976             case ISO_EXTENDED_FULL:
977                 result = formatOffsetISO8601Extended(offset, true, false, false);
978                 break;
979 
980             case ISO_EXTENDED_LOCAL_FULL:
981                 result = formatOffsetISO8601Extended(offset, false, false, false);
982                 break;
983 
984             default:
985                 // Other cases are handled earlier and never comes into this
986                 // switch statement.
987                 assert false;
988                 break;
989             }
990             // time type
991             if (timeType != null) {
992                 timeType.value = (offsets[1] != 0) ? TimeType.DAYLIGHT : TimeType.STANDARD;
993             }
994         }
995 
996         assert(result != null);
997 
998         return result;
999     }
1000 
1001     /**
1002      * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601
1003      * basic or extended time zone string. When the given string is not an ISO 8601 time
1004      * zone string, this method sets the current position as the error index
1005      * to <code>ParsePosition pos</code> and returns 0.
1006      *
1007      * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-0800", "-08:00", and "Z")
1008      * at the position.
1009      * @param pos the position.
1010      * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style
1011      * time zone string.
1012      * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean)
1013      * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean)
1014      * @stable ICU 49
1015      */
parseOffsetISO8601(String text, ParsePosition pos)1016     public final int parseOffsetISO8601(String text, ParsePosition pos) {
1017         return parseOffsetISO8601(text, pos, false, null);
1018     }
1019 
1020     /**
1021      * Returns offset from GMT(UTC) in milliseconds for the given localized GMT
1022      * offset format string. When the given string cannot be parsed, this method
1023      * sets the current position as the error index to <code>ParsePosition pos</code>
1024      * and returns 0.
1025      *
1026      * @param text the text contains a localized GMT offset string at the position.
1027      * @param pos the position.
1028      * @return the offset from GMT(UTC) in milliseconds for the given localized GMT
1029      * offset format string.
1030      * @see #formatOffsetLocalizedGMT(int)
1031      * @stable ICU 49
1032      */
parseOffsetLocalizedGMT(String text, ParsePosition pos)1033     public int parseOffsetLocalizedGMT(String text, ParsePosition pos) {
1034         return parseOffsetLocalizedGMT(text, pos, false, null);
1035     }
1036 
1037     /**
1038      * Returns offset from GMT(UTC) in milliseconds for the given short localized GMT
1039      * offset format string. When the given string cannot be parsed, this method
1040      * sets the current position as the error index to <code>ParsePosition pos</code>
1041      * and returns 0.
1042      *
1043      * @param text the text contains a short localized GMT offset string at the position.
1044      * @param pos the position.
1045      * @return the offset from GMT(UTC) in milliseconds for the given short localized GMT
1046      * offset format string.
1047      * @see #formatOffsetShortLocalizedGMT(int)
1048      * @stable ICU 51
1049      */
parseOffsetShortLocalizedGMT(String text, ParsePosition pos)1050     public int parseOffsetShortLocalizedGMT(String text, ParsePosition pos) {
1051         return parseOffsetLocalizedGMT(text, pos, true, null);
1052     }
1053 
1054     /**
1055      * Returns a <code>TimeZone</code> by parsing the time zone string according to
1056      * the parse position, the style and the parse options.
1057      *
1058      * @param text the text contains a time zone string at the position.
1059      * @param style the format style.
1060      * @param pos the position.
1061      * @param options the parse options.
1062      * @param timeType The output argument for receiving the time type (standard/daylight/unknown),
1063      * or specify null if the information is not necessary.
1064      * @return A <code>TimeZone</code>, or null if the input could not be parsed.
1065      * @see Style
1066      * @see #format(Style, TimeZone, long, Output)
1067      * @stable ICU 49
1068      */
parse(Style style, String text, ParsePosition pos, EnumSet<ParseOption> options, Output<TimeType> timeType)1069     public TimeZone parse(Style style, String text, ParsePosition pos, EnumSet<ParseOption> options, Output<TimeType> timeType) {
1070         if (timeType == null) {
1071             timeType = new Output<TimeType>(TimeType.UNKNOWN);
1072         } else {
1073             timeType.value = TimeType.UNKNOWN;
1074         }
1075 
1076         int startIdx = pos.getIndex();
1077         int maxPos = text.length();
1078         int offset;
1079 
1080         // Styles using localized GMT format as fallback
1081         boolean fallbackLocalizedGMT =
1082                 (style == Style.SPECIFIC_LONG || style == Style.GENERIC_LONG || style == Style.GENERIC_LOCATION);
1083         boolean fallbackShortLocalizedGMT =
1084                 (style == Style.SPECIFIC_SHORT || style == Style.GENERIC_SHORT);
1085 
1086         int evaluated = 0;  // bit flags representing already evaluated styles
1087         ParsePosition tmpPos = new ParsePosition(startIdx);
1088 
1089         int parsedOffset = UNKNOWN_OFFSET;  // stores successfully parsed offset for later use
1090         int parsedPos = -1;                 // stores successfully parsed offset position for later use
1091 
1092         // Try localized GMT format first if necessary
1093         if (fallbackLocalizedGMT || fallbackShortLocalizedGMT) {
1094             Output<Boolean> hasDigitOffset = new Output<Boolean>(false);
1095             offset = parseOffsetLocalizedGMT(text, tmpPos, fallbackShortLocalizedGMT, hasDigitOffset);
1096             if (tmpPos.getErrorIndex() == -1) {
1097                 // Even when the input text was successfully parsed as a localized GMT format text,
1098                 // we may still need to evaluate the specified style if -
1099                 //   1) GMT zero format was used, and
1100                 //   2) The input text was not completely processed
1101                 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
1102                     pos.setIndex(tmpPos.getIndex());
1103                     return getTimeZoneForOffset(offset);
1104                 }
1105                 parsedOffset = offset;
1106                 parsedPos = tmpPos.getIndex();
1107             }
1108             // Note: For now, no distinction between long/short localized GMT format in the parser.
1109             // This might be changed in future.
1110 //            evaluated |= (fallbackLocalizedGMT ? Style.LOCALIZED_GMT.flag : Style.LOCALIZED_GMT_SHORT.flag);
1111             evaluated |= (Style.LOCALIZED_GMT.flag | Style.LOCALIZED_GMT_SHORT.flag);
1112         }
1113 
1114         boolean parseTZDBAbbrev = (options == null) ?
1115                 getDefaultParseOptions().contains(ParseOption.TZ_DATABASE_ABBREVIATIONS)
1116                 : options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS);
1117 
1118         // Try the specified style
1119         switch (style) {
1120             case LOCALIZED_GMT:
1121             {
1122                 tmpPos.setIndex(startIdx);
1123                 tmpPos.setErrorIndex(-1);
1124 
1125                 offset = parseOffsetLocalizedGMT(text, tmpPos);
1126                 if (tmpPos.getErrorIndex() == -1) {
1127                     pos.setIndex(tmpPos.getIndex());
1128                     return getTimeZoneForOffset(offset);
1129                 }
1130                 // Note: For now, no distinction between long/short localized GMT format in the parser.
1131                 // This might be changed in future.
1132                 evaluated |= Style.LOCALIZED_GMT_SHORT.flag;
1133                 break;
1134             }
1135             case LOCALIZED_GMT_SHORT:
1136             {
1137                 tmpPos.setIndex(startIdx);
1138                 tmpPos.setErrorIndex(-1);
1139 
1140                 offset = parseOffsetShortLocalizedGMT(text, tmpPos);
1141                 if (tmpPos.getErrorIndex() == -1) {
1142                     pos.setIndex(tmpPos.getIndex());
1143                     return getTimeZoneForOffset(offset);
1144                 }
1145                 // Note: For now, no distinction between long/short localized GMT format in the parser.
1146                 // This might be changed in future.
1147                 evaluated |= Style.LOCALIZED_GMT.flag;
1148                 break;
1149             }
1150 
1151             case ISO_BASIC_SHORT:
1152             case ISO_BASIC_FIXED:
1153             case ISO_BASIC_FULL:
1154             case ISO_EXTENDED_FIXED:
1155             case ISO_EXTENDED_FULL:
1156             {
1157                 tmpPos.setIndex(startIdx);
1158                 tmpPos.setErrorIndex(-1);
1159 
1160                 offset = parseOffsetISO8601(text, tmpPos);
1161                 if (tmpPos.getErrorIndex() == -1) {
1162                     pos.setIndex(tmpPos.getIndex());
1163                     return getTimeZoneForOffset(offset);
1164                 }
1165                 break;
1166             }
1167 
1168             case ISO_BASIC_LOCAL_SHORT:
1169             case ISO_BASIC_LOCAL_FIXED:
1170             case ISO_BASIC_LOCAL_FULL:
1171             case ISO_EXTENDED_LOCAL_FIXED:
1172             case ISO_EXTENDED_LOCAL_FULL:
1173             {
1174                 tmpPos.setIndex(startIdx);
1175                 tmpPos.setErrorIndex(-1);
1176 
1177                 // Exclude the case of UTC Indicator "Z" here
1178                 Output<Boolean> hasDigitOffset = new Output<Boolean>(false);
1179                 offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset);
1180                 if (tmpPos.getErrorIndex() == -1 && hasDigitOffset.value) {
1181                     pos.setIndex(tmpPos.getIndex());
1182                     return getTimeZoneForOffset(offset);
1183                 }
1184                 break;
1185             }
1186 
1187             case SPECIFIC_LONG:
1188             case SPECIFIC_SHORT:
1189             {
1190                 // Specific styles
1191                 EnumSet<NameType> nameTypes = null;
1192                 if (style == Style.SPECIFIC_LONG) {
1193                     nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT);
1194                 } else {
1195                     assert style == Style.SPECIFIC_SHORT;
1196                     nameTypes = EnumSet.of(NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT);
1197                 }
1198                 Collection<MatchInfo> specificMatches = _tznames.find(text, startIdx, nameTypes);
1199                 if (specificMatches != null) {
1200                     MatchInfo specificMatch = null;
1201                     for (MatchInfo match : specificMatches) {
1202                         if (startIdx + match.matchLength() > parsedPos) {
1203                             specificMatch = match;
1204                             parsedPos = startIdx + match.matchLength();
1205                         }
1206                     }
1207                     if (specificMatch != null) {
1208                         timeType.value = getTimeType(specificMatch.nameType());
1209                         pos.setIndex(parsedPos);
1210                         return TimeZone.getTimeZone(getTimeZoneID(specificMatch.tzID(), specificMatch.mzID()));
1211                     }
1212                 }
1213 
1214                 if (parseTZDBAbbrev && style == Style.SPECIFIC_SHORT) {
1215                     assert nameTypes.contains(NameType.SHORT_STANDARD);
1216                     assert nameTypes.contains(NameType.SHORT_DAYLIGHT);
1217 
1218                     Collection<MatchInfo> tzdbNameMatches =
1219                             getTZDBTimeZoneNames().find(text, startIdx, nameTypes);
1220                     if (tzdbNameMatches != null) {
1221                         MatchInfo tzdbNameMatch = null;
1222                         for (MatchInfo match : tzdbNameMatches) {
1223                             if (startIdx + match.matchLength() > parsedPos) {
1224                                 tzdbNameMatch = match;
1225                                 parsedPos = startIdx + match.matchLength();
1226                             }
1227                         }
1228                         if (tzdbNameMatch != null) {
1229                             timeType.value = getTimeType(tzdbNameMatch.nameType());
1230                             pos.setIndex(parsedPos);
1231                             return TimeZone.getTimeZone(getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID()));
1232                         }
1233                     }
1234                 }
1235                 break;
1236             }
1237             case GENERIC_LONG:
1238             case GENERIC_SHORT:
1239             case GENERIC_LOCATION:
1240             {
1241                 EnumSet<GenericNameType> genericNameTypes = null;
1242                 switch (style) {
1243                 case GENERIC_LOCATION:
1244                     genericNameTypes = EnumSet.of(GenericNameType.LOCATION);
1245                     break;
1246                 case GENERIC_LONG:
1247                     genericNameTypes = EnumSet.of(GenericNameType.LONG, GenericNameType.LOCATION);
1248                     break;
1249                 case GENERIC_SHORT:
1250                     genericNameTypes = EnumSet.of(GenericNameType.SHORT, GenericNameType.LOCATION);
1251                     break;
1252                 default:
1253                     // style cannot be other than above cases
1254                     assert false;
1255                     break;
1256                 }
1257                 GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, genericNameTypes);
1258                 if (bestGeneric != null && (startIdx + bestGeneric.matchLength() > parsedPos)) {
1259                     timeType.value = bestGeneric.timeType();
1260                     pos.setIndex(startIdx + bestGeneric.matchLength());
1261                     return TimeZone.getTimeZone(bestGeneric.tzID());
1262                 }
1263                 break;
1264             }
1265             case ZONE_ID:
1266             {
1267                 tmpPos.setIndex(startIdx);
1268                 tmpPos.setErrorIndex(-1);
1269 
1270                 String id = parseZoneID(text, tmpPos);
1271                 if (tmpPos.getErrorIndex() == -1) {
1272                     pos.setIndex(tmpPos.getIndex());
1273                     return TimeZone.getTimeZone(id);
1274                 }
1275                 break;
1276             }
1277             case ZONE_ID_SHORT:
1278             {
1279                 tmpPos.setIndex(startIdx);
1280                 tmpPos.setErrorIndex(-1);
1281 
1282                 String id = parseShortZoneID(text, tmpPos);
1283                 if (tmpPos.getErrorIndex() == -1) {
1284                     pos.setIndex(tmpPos.getIndex());
1285                     return TimeZone.getTimeZone(id);
1286                 }
1287                 break;
1288             }
1289             case EXEMPLAR_LOCATION:
1290             {
1291                 tmpPos.setIndex(startIdx);
1292                 tmpPos.setErrorIndex(-1);
1293 
1294                 String id = parseExemplarLocation(text, tmpPos);
1295                 if (tmpPos.getErrorIndex() == -1) {
1296                     pos.setIndex(tmpPos.getIndex());
1297                     return TimeZone.getTimeZone(id);
1298                 }
1299                 break;
1300             }
1301         }
1302         evaluated |= style.flag;
1303 
1304         if (parsedPos > startIdx) {
1305             // When the specified style is one of SPECIFIC_XXX or GENERIC_XXX, we tried to parse the input
1306             // as localized GMT format earlier. If parsedOffset is positive, it means it was successfully
1307             // parsed as localized GMT format, but offset digits were not detected (more specifically, GMT
1308             // zero format). Then, it tried to find a match within the set of display names, but could not
1309             // find a match. At this point, we can safely assume the input text contains the localized
1310             // GMT format.
1311             assert parsedOffset != UNKNOWN_OFFSET;
1312             pos.setIndex(parsedPos);
1313             return getTimeZoneForOffset(parsedOffset);
1314         }
1315 
1316 
1317         // Failed to parse the input text as the time zone format in the specified style.
1318         // Check the longest match among other styles below.
1319         String parsedID = null;                     // stores successfully parsed zone ID for later use
1320         TimeType parsedTimeType = TimeType.UNKNOWN; // stores successfully parsed time type for later use
1321         assert parsedPos < 0;
1322         assert parsedOffset == UNKNOWN_OFFSET;
1323 
1324         // ISO 8601
1325         if (parsedPos < maxPos &&
1326                 ((evaluated & ISO_Z_STYLE_FLAG) == 0 || (evaluated & ISO_LOCAL_STYLE_FLAG) == 0)) {
1327             tmpPos.setIndex(startIdx);
1328             tmpPos.setErrorIndex(-1);
1329 
1330             Output<Boolean> hasDigitOffset = new Output<Boolean>(false);
1331             offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset);
1332             if (tmpPos.getErrorIndex() == -1) {
1333                 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
1334                     pos.setIndex(tmpPos.getIndex());
1335                     return getTimeZoneForOffset(offset);
1336                 }
1337                 // Note: When ISO 8601 format contains offset digits, it should not
1338                 // collide with other formats. However, ISO 8601 UTC format "Z" (single letter)
1339                 // may collide with other names. In this case, we need to evaluate other names.
1340                 if (parsedPos < tmpPos.getIndex()) {
1341                     parsedOffset = offset;
1342                     parsedID = null;
1343                     parsedTimeType = TimeType.UNKNOWN;
1344                     parsedPos = tmpPos.getIndex();
1345                     assert parsedPos == startIdx + 1;   // only when "Z" is used
1346                 }
1347             }
1348         }
1349 
1350 
1351         // Localized GMT format
1352         if (parsedPos < maxPos &&
1353                 (evaluated & Style.LOCALIZED_GMT.flag) == 0) {
1354             tmpPos.setIndex(startIdx);
1355             tmpPos.setErrorIndex(-1);
1356 
1357             Output<Boolean> hasDigitOffset = new Output<Boolean>(false);
1358             offset = parseOffsetLocalizedGMT(text, tmpPos, false, hasDigitOffset);
1359             if (tmpPos.getErrorIndex() == -1) {
1360                 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
1361                     pos.setIndex(tmpPos.getIndex());
1362                     return getTimeZoneForOffset(offset);
1363                 }
1364                 // Evaluate other names - see the comment earlier in this method.
1365                 if (parsedPos < tmpPos.getIndex()) {
1366                     parsedOffset = offset;
1367                     parsedID = null;
1368                     parsedTimeType = TimeType.UNKNOWN;
1369                     parsedPos = tmpPos.getIndex();
1370                 }
1371             }
1372         }
1373 
1374         if (parsedPos < maxPos &&
1375                 (evaluated & Style.LOCALIZED_GMT_SHORT.flag) == 0) {
1376             tmpPos.setIndex(startIdx);
1377             tmpPos.setErrorIndex(-1);
1378 
1379             Output<Boolean> hasDigitOffset = new Output<Boolean>(false);
1380             offset = parseOffsetLocalizedGMT(text, tmpPos, true, hasDigitOffset);
1381             if (tmpPos.getErrorIndex() == -1) {
1382                 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
1383                     pos.setIndex(tmpPos.getIndex());
1384                     return getTimeZoneForOffset(offset);
1385                 }
1386                 // Evaluate other names - see the comment earlier in this method.
1387                 if (parsedPos < tmpPos.getIndex()) {
1388                     parsedOffset = offset;
1389                     parsedID = null;
1390                     parsedTimeType = TimeType.UNKNOWN;
1391                     parsedPos = tmpPos.getIndex();
1392                 }
1393             }
1394         }
1395 
1396         // When ParseOption.ALL_STYLES is available, we also try to look all possible display names and IDs.
1397         // For example, when style is GENERIC_LONG, "EST" (SPECIFIC_SHORT) is never
1398         // used for America/New_York. With parseAllStyles true, this code parses "EST"
1399         // as America/New_York.
1400 
1401         // Note: Adding all possible names into the trie used by the implementation is quite heavy operation,
1402         // which we want to avoid normally (note that we cache the trie, so this is applicable to the
1403         // first time only as long as the cache does not expire).
1404 
1405         boolean parseAllStyles = (options == null) ?
1406                 getDefaultParseOptions().contains(ParseOption.ALL_STYLES)
1407                 : options.contains(ParseOption.ALL_STYLES);
1408 
1409         if (parseAllStyles) {
1410             // Try all specific names and exemplar location names
1411             if (parsedPos < maxPos) {
1412                 Collection<MatchInfo> specificMatches = _tznames.find(text, startIdx, ALL_SIMPLE_NAME_TYPES);
1413                 MatchInfo specificMatch = null;
1414                 int matchPos = -1;
1415                 if (specificMatches != null) {
1416                     for (MatchInfo match : specificMatches) {
1417                         if (startIdx + match.matchLength() > matchPos) {
1418                             specificMatch = match;
1419                             matchPos = startIdx + match.matchLength();
1420                         }
1421                     }
1422                 }
1423                 if (parsedPos < matchPos) {
1424                     parsedPos = matchPos;
1425                     parsedID = getTimeZoneID(specificMatch.tzID(), specificMatch.mzID());
1426                     parsedTimeType = getTimeType(specificMatch.nameType());
1427                     parsedOffset = UNKNOWN_OFFSET;
1428                 }
1429             }
1430             if (parseTZDBAbbrev && parsedPos < maxPos && (evaluated & Style.SPECIFIC_SHORT.flag) == 0) {
1431                 Collection<MatchInfo> tzdbNameMatches =
1432                         getTZDBTimeZoneNames().find(text, startIdx, ALL_SIMPLE_NAME_TYPES);
1433                 MatchInfo tzdbNameMatch = null;
1434                 int matchPos = -1;
1435                 if (tzdbNameMatches != null) {
1436                     for (MatchInfo match : tzdbNameMatches) {
1437                         if (startIdx + match.matchLength() > matchPos) {
1438                             tzdbNameMatch = match;
1439                             matchPos = startIdx + match.matchLength();
1440                         }
1441                     }
1442                     if (parsedPos < matchPos) {
1443                         parsedPos = matchPos;
1444                         parsedID = getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID());
1445                         parsedTimeType = getTimeType(tzdbNameMatch.nameType());
1446                         parsedOffset = UNKNOWN_OFFSET;
1447                     }
1448                 }
1449 
1450             }
1451             // Try generic names
1452             if (parsedPos < maxPos) {
1453                 GenericMatchInfo genericMatch = getTimeZoneGenericNames().findBestMatch(text, startIdx, ALL_GENERIC_NAME_TYPES);
1454                 if (genericMatch != null && parsedPos < startIdx + genericMatch.matchLength()) {
1455                     parsedPos = startIdx + genericMatch.matchLength();
1456                     parsedID = genericMatch.tzID();
1457                     parsedTimeType = genericMatch.timeType();
1458                     parsedOffset = UNKNOWN_OFFSET;
1459                 }
1460             }
1461 
1462             // Try time zone ID
1463             if (parsedPos < maxPos && (evaluated & Style.ZONE_ID.flag) == 0) {
1464                 tmpPos.setIndex(startIdx);
1465                 tmpPos.setErrorIndex(-1);
1466 
1467                 String id = parseZoneID(text, tmpPos);
1468                 if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) {
1469                     parsedPos = tmpPos.getIndex();
1470                     parsedID = id;
1471                     parsedTimeType = TimeType.UNKNOWN;
1472                     parsedOffset = UNKNOWN_OFFSET;
1473                 }
1474             }
1475             // Try short time zone ID
1476             if (parsedPos < maxPos && (evaluated & Style.ZONE_ID_SHORT.flag) == 0) {
1477                 tmpPos.setIndex(startIdx);
1478                 tmpPos.setErrorIndex(-1);
1479 
1480                 String id = parseShortZoneID(text, tmpPos);
1481                 if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) {
1482                     parsedPos = tmpPos.getIndex();
1483                     parsedID = id;
1484                     parsedTimeType = TimeType.UNKNOWN;
1485                     parsedOffset = UNKNOWN_OFFSET;
1486                 }
1487             }
1488         }
1489 
1490         if (parsedPos > startIdx) {
1491             // Parsed successfully
1492             TimeZone parsedTZ = null;
1493             if (parsedID != null) {
1494                 parsedTZ = TimeZone.getTimeZone(parsedID);
1495             } else {
1496                 assert parsedOffset != UNKNOWN_OFFSET;
1497                 parsedTZ = getTimeZoneForOffset(parsedOffset);
1498             }
1499             timeType.value = parsedTimeType;
1500             pos.setIndex(parsedPos);
1501             return parsedTZ;
1502         }
1503 
1504         pos.setErrorIndex(startIdx);
1505         return null;
1506     }
1507 
1508     /**
1509      * Returns a <code>TimeZone</code> by parsing the time zone string according to
1510      * the parse position, the style and the default parse options.
1511      * <p>
1512      * <b>Note</b>: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output)
1513      * parse(style, text, pos, null, timeType)}.
1514      *
1515      * @param text the text contains a time zone string at the position.
1516      * @param style the format style
1517      * @param pos the position.
1518      * @param timeType The output argument for receiving the time type (standard/daylight/unknown),
1519      * or specify null if the information is not necessary.
1520      * @return A <code>TimeZone</code>, or null if the input could not be parsed.
1521      * @see Style
1522      * @see #parse(Style, String, ParsePosition, EnumSet, Output)
1523      * @see #format(Style, TimeZone, long, Output)
1524      * @see #setDefaultParseOptions(EnumSet)
1525      * @stable ICU 49
1526      */
1527     public TimeZone parse(Style style, String text, ParsePosition pos, Output<TimeType> timeType) {
1528         return parse(style, text, pos, null, timeType);
1529     }
1530 
1531     /**
1532      * Returns a <code>TimeZone</code> by parsing the time zone string according to
1533      * the given parse position.
1534      * <p>
1535      * <b>Note</b>: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output)
1536      * parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), timeType)}.
1537      *
1538      * @param text the text contains a time zone string at the position.
1539      * @param pos the position.
1540      * @return A <code>TimeZone</code>, or null if the input could not be parsed.
1541      * @see #parse(Style, String, ParsePosition, EnumSet, Output)
1542      * @stable ICU 49
1543      */
1544     public final TimeZone parse(String text, ParsePosition pos) {
1545         return parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), null);
1546     }
1547 
1548     /**
1549      * Returns a <code>TimeZone</code> for the given text.
1550      * <p>
1551      * <b>Note</b>: The behavior of this method is equivalent to {@link #parse(String, ParsePosition)}.
1552      * @param text the time zone string
1553      * @return A <code>TimeZone</code>.
1554      * @throws ParseException when the input could not be parsed as a time zone string.
1555      * @see #parse(String, ParsePosition)
1556      * @stable ICU 49
1557      */
1558     public final TimeZone parse(String text) throws ParseException {
1559         ParsePosition pos = new ParsePosition(0);
1560         TimeZone tz = parse(text, pos);
1561         if (pos.getErrorIndex() >= 0) {
1562             throw new ParseException("Unparseable time zone: \"" + text + "\"" , 0);
1563         }
1564         assert(tz != null);
1565         return tz;
1566     }
1567 
1568     /**
1569      * {@inheritDoc}
1570      *
1571      * @stable ICU 49
1572      */
1573     @Override
1574     public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
1575         TimeZone tz = null;
1576         long date = System.currentTimeMillis();
1577 
1578         if (obj instanceof TimeZone) {
1579             tz = (TimeZone)obj;
1580         } else if (obj instanceof Calendar) {
1581             tz = ((Calendar)obj).getTimeZone();
1582             date = ((Calendar)obj).getTimeInMillis();
1583         } else {
1584             throw new IllegalArgumentException("Cannot format given Object (" +
1585                     obj.getClass().getName() + ") as a time zone");
1586         }
1587         assert(tz != null);
1588         String result = formatOffsetLocalizedGMT(tz.getOffset(date));
1589         toAppendTo.append(result);
1590 
1591         if (pos.getFieldAttribute() == DateFormat.Field.TIME_ZONE
1592                 || pos.getField() == DateFormat.TIMEZONE_FIELD) {
1593             pos.setBeginIndex(0);
1594             pos.setEndIndex(result.length());
1595         }
1596         return toAppendTo;
1597     }
1598 
1599     /**
1600      * {@inheritDoc}
1601      *
1602      * @stable ICU 49
1603      */
1604     @Override
1605     public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
1606         StringBuffer toAppendTo = new StringBuffer();
1607         FieldPosition pos = new FieldPosition(0);
1608         toAppendTo = format(obj, toAppendTo, pos);
1609 
1610         // supporting only DateFormat.Field.TIME_ZONE
1611         AttributedString as = new AttributedString(toAppendTo.toString());
1612         as.addAttribute(DateFormat.Field.TIME_ZONE, DateFormat.Field.TIME_ZONE);
1613 
1614         return as.getIterator();
1615     }
1616 
1617     /**
1618      * {@inheritDoc}
1619      *
1620      * @stable ICU 49
1621      */
1622     @Override
1623     public Object parseObject(String source, ParsePosition pos) {
1624         return parse(source, pos);
1625     }
1626 
1627     /**
1628      * Private method used for localized GMT formatting.
1629      * @param offset the zone's UTC offset
1630      * @param isShort true if the short localized GMT format is desired
1631      * @return the localized GMT string
1632      */
1633     private String formatOffsetLocalizedGMT(int offset, boolean isShort) {
1634         if (offset == 0) {
1635             return _gmtZeroFormat;
1636         }
1637 
1638         StringBuilder buf = new StringBuilder();
1639         boolean positive = true;
1640         if (offset < 0) {
1641             offset = -offset;
1642             positive = false;
1643         }
1644 
1645         int offsetH = offset / MILLIS_PER_HOUR;
1646         offset = offset % MILLIS_PER_HOUR;
1647         int offsetM = offset / MILLIS_PER_MINUTE;
1648         offset = offset % MILLIS_PER_MINUTE;
1649         int offsetS = offset / MILLIS_PER_SECOND;
1650 
1651         if (offsetH > MAX_OFFSET_HOUR || offsetM > MAX_OFFSET_MINUTE || offsetS > MAX_OFFSET_SECOND) {
1652             throw new IllegalArgumentException("Offset out of range :" + offset);
1653         }
1654 
1655         Object[] offsetPatternItems;
1656         if (positive) {
1657             if (offsetS != 0) {
1658                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HMS.ordinal()];
1659             } else if (offsetM != 0 || !isShort) {
1660                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HM.ordinal()];
1661             } else {
1662                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_H.ordinal()];
1663             }
1664         } else {
1665             if (offsetS != 0) {
1666                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()];
1667             } else if (offsetM != 0 || !isShort) {
1668                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HM.ordinal()];
1669             } else {
1670                 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_H.ordinal()];
1671             }
1672         }
1673 
1674         // Building the GMT format string
1675         buf.append(_gmtPatternPrefix);
1676 
1677         for (Object item : offsetPatternItems) {
1678             if (item instanceof String) {
1679                 // pattern literal
1680                 buf.append((String)item);
1681             } else if (item instanceof GMTOffsetField) {
1682                 // Hour/minute/second field
1683                 GMTOffsetField field = (GMTOffsetField)item;
1684                 switch (field.getType()) {
1685                 case 'H':
1686                     appendOffsetDigits(buf, offsetH, (isShort ? 1 : 2));
1687                     break;
1688                 case 'm':
1689                     appendOffsetDigits(buf, offsetM, 2);
1690                     break;
1691                 case 's':
1692                     appendOffsetDigits(buf, offsetS, 2);
1693                     break;
1694                 }
1695             }
1696         }
1697         buf.append(_gmtPatternSuffix);
1698         return buf.toString();
1699     }
1700 
1701     /**
1702      * Numeric offset field combinations
1703      */
1704     private enum OffsetFields {
1705         H, HM, HMS
1706     }
1707 
1708     private String formatOffsetISO8601(int offset, boolean isBasic, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
1709         int absOffset = offset < 0 ? -offset : offset;
1710         if (useUtcIndicator && (absOffset < MILLIS_PER_SECOND || (ignoreSeconds && absOffset < MILLIS_PER_MINUTE))) {
1711             return ISO8601_UTC;
1712         }
1713         OffsetFields minFields = isShort ? OffsetFields.H : OffsetFields.HM;
1714         OffsetFields maxFields = ignoreSeconds ? OffsetFields.HM : OffsetFields.HMS;
1715         Character sep = isBasic ? null : ':';
1716 
1717         // Note: OffsetFields.HMS as maxFields is an ICU extension. ISO 8601 specification does
1718         // not support seconds field.
1719 
1720         if (absOffset >= MAX_OFFSET) {
1721             throw new IllegalArgumentException("Offset out of range :" + offset);
1722         }
1723 
1724         int[] fields = new int[3];
1725         fields[0] = absOffset / MILLIS_PER_HOUR;
1726         absOffset = absOffset % MILLIS_PER_HOUR;
1727         fields[1] = absOffset / MILLIS_PER_MINUTE;
1728         absOffset = absOffset % MILLIS_PER_MINUTE;
1729         fields[2] = absOffset / MILLIS_PER_SECOND;
1730 
1731         assert(fields[0] >= 0 && fields[0] <= MAX_OFFSET_HOUR);
1732         assert(fields[1] >= 0 && fields[1] <= MAX_OFFSET_MINUTE);
1733         assert(fields[2] >= 0 && fields[2] <= MAX_OFFSET_SECOND);
1734 
1735         int lastIdx = maxFields.ordinal();
1736         while (lastIdx > minFields.ordinal()) {
1737             if (fields[lastIdx] != 0) {
1738                 break;
1739             }
1740             lastIdx--;
1741         }
1742 
1743         StringBuilder buf = new StringBuilder();
1744         char sign = '+';
1745         if (offset < 0) {
1746             // if all output fields are 0s, do not use negative sign
1747             for (int idx = 0; idx <= lastIdx; idx++) {
1748                 if (fields[idx] != 0) {
1749                     sign = '-';
1750                     break;
1751                 }
1752             }
1753         }
1754         buf.append(sign);
1755 
1756         for (int idx = 0; idx <= lastIdx; idx++) {
1757             if (sep != null && idx != 0) {
1758                 buf.append(sep);
1759             }
1760             if (fields[idx] < 10) {
1761                 buf.append('0');
1762             }
1763             buf.append(fields[idx]);
1764         }
1765         return buf.toString();
1766     }
1767 
1768     /**
1769      * Private method returning the time zone's specific format string.
1770      *
1771      * @param tz the time zone
1772      * @param stdType the name type used for standard time
1773      * @param dstType the name type used for daylight time
1774      * @param date the date
1775      * @param timeType when null, actual time type is set
1776      * @return the time zone's specific format name string
1777      */
1778     private String formatSpecific(TimeZone tz, NameType stdType, NameType dstType, long date, Output<TimeType> timeType) {
1779         assert(stdType == NameType.LONG_STANDARD || stdType == NameType.SHORT_STANDARD);
1780         assert(dstType == NameType.LONG_DAYLIGHT || dstType == NameType.SHORT_DAYLIGHT);
1781 
1782         boolean isDaylight = tz.inDaylightTime(new Date(date));
1783         String name = isDaylight?
1784                 getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), dstType, date) :
1785                 getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), stdType, date);
1786 
1787         if (name != null && timeType != null) {
1788             timeType.value = isDaylight ? TimeType.DAYLIGHT : TimeType.STANDARD;
1789         }
1790         return name;
1791     }
1792 
1793     /**
1794      * Private method returning the time zone's exemplar location string.
1795      * This method will never return null.
1796      *
1797      * @param tz the time zone
1798      * @return the time zone's exemplar location name.
1799      */
1800     private String formatExemplarLocation(TimeZone tz) {
1801         String location = getTimeZoneNames().getExemplarLocationName(ZoneMeta.getCanonicalCLDRID(tz));
1802         if (location == null) {
1803             // Use "unknown" location
1804             location = getTimeZoneNames().getExemplarLocationName(UNKNOWN_ZONE_ID);
1805             if (location == null) {
1806                 // last resort
1807                 location = UNKNOWN_LOCATION;
1808             }
1809         }
1810         return location;
1811     }
1812 
1813     /**
1814      * Private method returns a time zone ID. If tzID is not null, the value of tzID is returned.
1815      * If tzID is null, then this method look up a time zone ID for the current region. This is a
1816      * small helper method used by the parse implementation method
1817      *
1818      * @param tzID
1819      *            the time zone ID or null
1820      * @param mzID
1821      *            the meta zone ID or null
1822      * @return A time zone ID
1823      * @throws IllegalArgumentException
1824      *             when both tzID and mzID are null
1825      */
1826     private String getTimeZoneID(String tzID, String mzID) {
1827         String id = tzID;
1828         if (id == null) {
1829             assert (mzID != null);
1830             id = _tznames.getReferenceZoneID(mzID, getTargetRegion());
1831             if (id == null) {
1832                 throw new IllegalArgumentException("Invalid mzID: " + mzID);
1833             }
1834         }
1835         return id;
1836     }
1837 
1838     /**
1839      * Private method returning the target region. The target regions is determined by
1840      * the locale of this instance. When a generic name is coming from
1841      * a meta zone, this region is used for checking if the time zone
1842      * is a reference zone of the meta zone.
1843      *
1844      * @return the target region
1845      */
1846     private synchronized String getTargetRegion() {
1847         if (_region == null) {
1848             _region = _locale.getCountry();
1849             if (_region.length() == 0) {
1850                 ULocale tmp = ULocale.addLikelySubtags(_locale);
1851                 _region = tmp.getCountry();
1852                 if (_region.length() == 0) {
1853                     _region = "001";
1854                 }
1855             }
1856         }
1857         return _region;
1858     }
1859 
1860     /**
1861      * Returns the time type for the given name type
1862      * @param nameType the name type
1863      * @return the time type (unknown/standard/daylight)
1864      */
1865     private TimeType getTimeType(NameType nameType) {
1866         switch (nameType) {
1867         case LONG_STANDARD:
1868         case SHORT_STANDARD:
1869             return TimeType.STANDARD;
1870 
1871         case LONG_DAYLIGHT:
1872         case SHORT_DAYLIGHT:
1873             return TimeType.DAYLIGHT;
1874 
1875         default:
1876             return TimeType.UNKNOWN;
1877         }
1878     }
1879 
1880     /**
1881      * Parses the localized GMT pattern string and initialize
1882      * localized gmt pattern fields including {{@link #_gmtPatternTokens}.
1883      * This method must be also called at deserialization time.
1884      *
1885      * @param gmtPattern the localized GMT pattern string such as "GMT {0}"
1886      * @throws IllegalArgumentException when the pattern string does not contain "{0}"
1887      */
1888     private void initGMTPattern(String gmtPattern) {
1889         // This implementation not perfect, but sufficient practically.
1890         int idx = gmtPattern.indexOf("{0}");
1891         if (idx < 0) {
1892             throw new IllegalArgumentException("Bad localized GMT pattern: " + gmtPattern);
1893         }
1894         _gmtPattern = gmtPattern;
1895         _gmtPatternPrefix = unquote(gmtPattern.substring(0, idx));
1896         _gmtPatternSuffix = unquote(gmtPattern.substring(idx + 3));
1897     }
1898 
1899     /**
1900      * Unquotes the message format style pattern.
1901      *
1902      * @param s the pattern
1903      * @return the unquoted pattern string
1904      */
1905     private static String unquote(String s) {
1906         if (s.indexOf('\'') < 0) {
1907             return s;
1908         }
1909         boolean isPrevQuote = false;
1910         boolean inQuote = false;
1911         StringBuilder buf = new StringBuilder();
1912         for (int i = 0; i < s.length(); i++) {
1913             char c = s.charAt(i);
1914             if (c == '\'') {
1915                 if (isPrevQuote) {
1916                     buf.append(c);
1917                     isPrevQuote = false;
1918                 } else {
1919                     isPrevQuote = true;
1920                 }
1921                 inQuote = !inQuote;
1922             } else {
1923                 isPrevQuote = false;
1924                 buf.append(c);
1925             }
1926         }
1927         return buf.toString();
1928     }
1929 
1930     /**
1931      * Initialize localized GMT format offset hour/min/sec patterns.
1932      * This method parses patterns into optimized run-time format.
1933      * This method must be called at deserialization time.
1934      *
1935      * @param gmtOffsetPatterns patterns, String[4]
1936      * @throws IllegalArgumentException when patterns are not valid
1937      */
1938     private void initGMTOffsetPatterns(String[] gmtOffsetPatterns) {
1939         int size = GMTOffsetPatternType.values().length;
1940         if (gmtOffsetPatterns.length < size) {
1941             throw new IllegalArgumentException("Insufficient number of elements in gmtOffsetPatterns");
1942         }
1943         Object[][] gmtOffsetPatternItems = new Object[size][];
1944         for (GMTOffsetPatternType t : GMTOffsetPatternType.values()) {
1945             int idx = t.ordinal();
1946             // Note: parseOffsetPattern will validate the given pattern and throws
1947             // IllegalArgumentException when pattern is not valid
1948             Object[] parsedItems = parseOffsetPattern(gmtOffsetPatterns[idx], t.required());
1949             gmtOffsetPatternItems[idx] = parsedItems;
1950         }
1951 
1952         _gmtOffsetPatterns = new String[size];
1953         System.arraycopy(gmtOffsetPatterns, 0, _gmtOffsetPatterns, 0, size);
1954         _gmtOffsetPatternItems = gmtOffsetPatternItems;
1955         checkAbuttingHoursAndMinutes();
1956     }
1957 
1958     private void checkAbuttingHoursAndMinutes() {
1959         _abuttingOffsetHoursAndMinutes = false;
1960         for (Object[] items : _gmtOffsetPatternItems) {
1961             boolean afterH = false;
1962             for (Object item : items) {
1963                 if (item instanceof GMTOffsetField) {
1964                     GMTOffsetField fld = (GMTOffsetField)item;
1965                     if (afterH) {
1966                         _abuttingOffsetHoursAndMinutes = true;
1967                     } else if (fld.getType() == 'H') {
1968                         afterH = true;
1969                     }
1970                 } else if (afterH) {
1971                     break;
1972                 }
1973             }
1974         }
1975     }
1976 
1977     /**
1978      * Used for representing localized GMT time fields in the parsed pattern object.
1979      * @see TimeZoneFormat#parseOffsetPattern(String, String)
1980      */
1981     private static class GMTOffsetField {
1982         final char _type;
1983         final int _width;
1984 
1985         GMTOffsetField(char type, int width) {
1986             _type = type;
1987             _width = width;
1988         }
1989 
1990         char getType() {
1991             return _type;
1992         }
1993 
1994         @SuppressWarnings("unused")
1995         int getWidth() {
1996             return _width;
1997         }
1998 
1999         static boolean isValid(char type, int width) {
2000             return (width == 1 ||  width == 2);
2001         }
2002     }
2003 
2004     /**
2005      * Parse the GMT offset pattern into runtime optimized format
2006      *
2007      * @param pattern the offset pattern string
2008      * @param letters the required pattern letters such as "Hm"
2009      * @return An array of Object. Each array entry is either String (representing
2010      * pattern literal) or GMTOffsetField (hour/min/sec field)
2011      */
2012     private static Object[] parseOffsetPattern(String pattern, String letters) {
2013         boolean isPrevQuote = false;
2014         boolean inQuote = false;
2015         StringBuilder text = new StringBuilder();
2016         char itemType = 0;  // 0 for string literal, otherwise time pattern character
2017         int itemLength = 1;
2018         boolean invalidPattern = false;
2019 
2020         List<Object> items = new ArrayList<Object>();
2021         BitSet checkBits = new BitSet(letters.length());
2022 
2023         for (int i = 0; i < pattern.length(); i++) {
2024             char ch = pattern.charAt(i);
2025             if (ch == '\'') {
2026                 if (isPrevQuote) {
2027                     text.append('\'');
2028                     isPrevQuote = false;
2029                 } else {
2030                     isPrevQuote = true;
2031                     if (itemType != 0) {
2032                         if (GMTOffsetField.isValid(itemType, itemLength)) {
2033                             items.add(new GMTOffsetField(itemType, itemLength));
2034                         } else {
2035                             invalidPattern = true;
2036                             break;
2037                         }
2038                         itemType = 0;
2039                     }
2040                 }
2041                 inQuote = !inQuote;
2042             } else {
2043                 isPrevQuote = false;
2044                 if (inQuote) {
2045                     text.append(ch);
2046                 } else {
2047                     int patFieldIdx = letters.indexOf(ch);
2048                     if (patFieldIdx >= 0) {
2049                         // an offset time pattern character
2050                         if (ch == itemType) {
2051                             itemLength++;
2052                         } else {
2053                             if (itemType == 0) {
2054                                 if (text.length() > 0) {
2055                                     items.add(text.toString());
2056                                     text.setLength(0);
2057                                 }
2058                             } else {
2059                                 if (GMTOffsetField.isValid(itemType, itemLength)) {
2060                                     items.add(new GMTOffsetField(itemType, itemLength));
2061                                 } else {
2062                                     invalidPattern = true;
2063                                     break;
2064                                 }
2065                             }
2066                             itemType = ch;
2067                             itemLength = 1;
2068                             checkBits.set(patFieldIdx);
2069                         }
2070                     } else {
2071                         // a string literal
2072                         if (itemType != 0) {
2073                             if (GMTOffsetField.isValid(itemType, itemLength)) {
2074                                 items.add(new GMTOffsetField(itemType, itemLength));
2075                             } else {
2076                                 invalidPattern = true;
2077                                 break;
2078                             }
2079                             itemType = 0;
2080                         }
2081                         text.append(ch);
2082                     }
2083                 }
2084             }
2085         }
2086         // handle last item
2087         if (!invalidPattern) {
2088             if (itemType == 0) {
2089                 if (text.length() > 0) {
2090                     items.add(text.toString());
2091                     text.setLength(0);
2092                 }
2093             } else {
2094                 if (GMTOffsetField.isValid(itemType, itemLength)) {
2095                     items.add(new GMTOffsetField(itemType, itemLength));
2096                 } else {
2097                     invalidPattern = true;
2098                 }
2099             }
2100         }
2101 
2102         if (invalidPattern || checkBits.cardinality() != letters.length()) {
2103             throw new IllegalStateException("Bad localized GMT offset pattern: " + pattern);
2104         }
2105 
2106         return items.toArray(new Object[items.size()]);
2107     }
2108 
2109     /**
2110      * Appends seconds field to the offset pattern with hour/minute
2111      *
2112      * @param offsetHM the offset pattern including hours and minutes fields
2113      * @return the offset pattern including hours, minutes and seconds fields
2114      */
2115     //TODO This code will be obsoleted once we add hour-minute-second pattern data in CLDR
2116     private static String expandOffsetPattern(String offsetHM) {
2117         int idx_mm = offsetHM.indexOf("mm");
2118         if (idx_mm < 0) {
2119             throw new RuntimeException("Bad time zone hour pattern data");
2120         }
2121         String sep = ":";
2122         int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H");
2123         if (idx_H >= 0) {
2124             sep = offsetHM.substring(idx_H + 1, idx_mm);
2125         }
2126         return offsetHM.substring(0, idx_mm + 2) + sep + "ss" + offsetHM.substring(idx_mm + 2);
2127     }
2128 
2129     /**
2130      * Truncates minutes field from the offset pattern with hour/minute
2131      *
2132      * @param offsetHM the offset pattern including hours and minutes fields
2133      * @return the offset pattern including only hours field
2134      */
2135     //TODO This code will be obsoleted once we add hour pattern data in CLDR
2136     private static String truncateOffsetPattern(String offsetHM) {
2137         int idx_mm = offsetHM.indexOf("mm");
2138         if (idx_mm < 0) {
2139             throw new RuntimeException("Bad time zone hour pattern data");
2140         }
2141         int idx_HH = offsetHM.substring(0, idx_mm).lastIndexOf("HH");
2142         if (idx_HH >= 0) {
2143             return offsetHM.substring(0, idx_HH + 2);
2144         }
2145         int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H");
2146         if (idx_H >= 0) {
2147             return offsetHM.substring(0, idx_H + 1);
2148         }
2149         throw new RuntimeException("Bad time zone hour pattern data");
2150     }
2151 
2152     /**
2153      * Appends localized digits to the buffer.
2154      * <p>
2155      * Note: This code assumes that the input number is 0 - 59
2156      *
2157      * @param buf the target buffer
2158      * @param n the integer number
2159      * @param minDigits the minimum digits width
2160      */
2161     private void appendOffsetDigits(StringBuilder buf, int n, int minDigits) {
2162         assert(n >= 0 && n < 60);
2163         int numDigits = n >= 10 ? 2 : 1;
2164         for (int i = 0; i < minDigits - numDigits; i++) {
2165             buf.append(_gmtOffsetDigits[0]);
2166         }
2167         if (numDigits == 2) {
2168             buf.append(_gmtOffsetDigits[n / 10]);
2169         }
2170         buf.append(_gmtOffsetDigits[n % 10]);
2171     }
2172 
2173     /**
2174      * Creates an instance of TimeZone for the given offset
2175      * @param offset the offset
2176      * @return A TimeZone with the given offset
2177      */
2178     private TimeZone getTimeZoneForOffset(int offset) {
2179         if (offset == 0) {
2180             // when offset is 0, we should use "Etc/GMT"
2181             return TimeZone.getTimeZone(TZID_GMT);
2182         }
2183         return ZoneMeta.getCustomTimeZone(offset);
2184     }
2185 
2186     /**
2187      * Returns offset from GMT(UTC) in milliseconds for the given localized GMT
2188      * offset format string. When the given string cannot be parsed, this method
2189      * sets the current position as the error index to <code>ParsePosition pos</code>
2190      * and returns 0.
2191      *
2192      * @param text the text contains a localized GMT offset string at the position.
2193      * @param pos the position.
2194      * @param isShort true if this parser to try the short format first
2195      * @param hasDigitOffset receiving if the parsed zone string contains offset digits.
2196      * @return the offset from GMT(UTC) in milliseconds for the given localized GMT
2197      * offset format string.
2198      */
2199     private int parseOffsetLocalizedGMT(String text, ParsePosition pos, boolean isShort, Output<Boolean> hasDigitOffset) {
2200         int start = pos.getIndex();
2201         int offset = 0;
2202         int[] parsedLength = {0};
2203 
2204         if (hasDigitOffset != null) {
2205             hasDigitOffset.value = false;
2206         }
2207 
2208         offset = parseOffsetLocalizedGMTPattern(text, start, isShort, parsedLength);
2209 
2210         // For now, parseOffsetLocalizedGMTPattern handles both long and short
2211         // formats, no matter isShort is true or false. This might be changed in future
2212         // when strict parsing is necessary, or different set of patterns are used for
2213         // short/long formats.
2214 //        if (parsedLength[0] == 0) {
2215 //            offset = parseOffsetLocalizedGMTPattern(text, start, !isShort, parsedLength);
2216 //        }
2217 
2218         if (parsedLength[0] > 0) {
2219             if (hasDigitOffset != null) {
2220                 hasDigitOffset.value = true;
2221             }
2222             pos.setIndex(start + parsedLength[0]);
2223             return offset;
2224         }
2225 
2226         // Try the default patterns
2227         offset = parseOffsetDefaultLocalizedGMT(text, start, parsedLength);
2228         if (parsedLength[0] > 0) {
2229             if (hasDigitOffset != null) {
2230                 hasDigitOffset.value = true;
2231             }
2232             pos.setIndex(start + parsedLength[0]);
2233             return offset;
2234         }
2235 
2236         // Check if this is a GMT zero format
2237         if (text.regionMatches(true, start, _gmtZeroFormat, 0, _gmtZeroFormat.length())) {
2238             pos.setIndex(start + _gmtZeroFormat.length());
2239             return 0;
2240         }
2241 
2242         // Check if this is a default GMT zero format
2243         for (String defGMTZero : ALT_GMT_STRINGS) {
2244             if (text.regionMatches(true, start, defGMTZero, 0, defGMTZero.length())) {
2245                 pos.setIndex(start + defGMTZero.length());
2246                 return 0;
2247             }
2248         }
2249 
2250         // Nothing matched
2251         pos.setErrorIndex(start);
2252         return 0;
2253     }
2254 
2255     /**
2256      * Parse localized GMT format generated by the pattern used by this formatter, except
2257      * GMT Zero format.
2258      * @param text the input text
2259      * @param start the start index
2260      * @param isShort true if the short localized GMT format is parsed.
2261      * @param parsedLen the parsed length, or 0 on failure.
2262      * @return the parsed offset in milliseconds.
2263      */
2264     private int parseOffsetLocalizedGMTPattern(String text, int start, boolean isShort, int[] parsedLen) {
2265         int idx = start;
2266         int offset = 0;
2267         boolean parsed = false;
2268 
2269         do {
2270             // Prefix part
2271             int len = _gmtPatternPrefix.length();
2272             if (len > 0 && !text.regionMatches(true, idx, _gmtPatternPrefix, 0, len)) {
2273                 // prefix match failed
2274                 break;
2275             }
2276             idx += len;
2277 
2278             // Offset part
2279             int[] offsetLen = new int[1];
2280             offset = parseOffsetFields(text, idx, false, offsetLen);
2281             if (offsetLen[0] == 0) {
2282                 // offset field match failed
2283                 break;
2284             }
2285             idx += offsetLen[0];
2286 
2287             // Suffix part
2288             len = _gmtPatternSuffix.length();
2289             if (len > 0 && !text.regionMatches(true, idx, _gmtPatternSuffix, 0, len)) {
2290                 // no suffix match
2291                 break;
2292             }
2293             idx += len;
2294             parsed = true;
2295         } while (false);
2296 
2297         parsedLen[0] = parsed ? idx - start : 0;
2298         return offset;
2299     }
2300 
2301     /**
2302      * Parses localized GMT offset fields into offset.
2303      *
2304      * @param text the input text
2305      * @param start the start index
2306      * @param isShort true if this is a short format - currently not used
2307      * @param parsedLen the parsed length, or 0 on failure.
2308      * @return the parsed offset in milliseconds.
2309      */
2310     private int parseOffsetFields(String text, int start, boolean isShort, int[] parsedLen) {
2311         int outLen = 0;
2312         int offset = 0;
2313         int sign = 1;
2314 
2315         if (parsedLen != null && parsedLen.length >= 1) {
2316             parsedLen[0] = 0;
2317         }
2318 
2319         int offsetH, offsetM, offsetS;
2320         offsetH = offsetM = offsetS = 0;
2321 
2322         int[] fields = {0, 0, 0};
2323         for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) {
2324             Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()];
2325             assert items != null;
2326 
2327             outLen = parseOffsetFieldsWithPattern(text, start, items, false, fields);
2328             if (outLen > 0) {
2329                 sign = gmtPatType.isPositive() ? 1 : -1;
2330                 offsetH = fields[0];
2331                 offsetM = fields[1];
2332                 offsetS = fields[2];
2333                 break;
2334             }
2335         }
2336         if (outLen > 0 && _abuttingOffsetHoursAndMinutes) {
2337             // When hours field is abutting minutes field,
2338             // the parse result above may not be appropriate.
2339             // For example, "01020" is parsed as 01:02 above,
2340             // but it should be parsed as 00:10:20.
2341             int tmpLen = 0;
2342             int tmpSign = 1;
2343             for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) {
2344                 Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()];
2345                 assert items != null;
2346 
2347                 // forcing parse to use single hour digit
2348                 tmpLen = parseOffsetFieldsWithPattern(text, start, items, true, fields);
2349                 if (tmpLen > 0) {
2350                     tmpSign = gmtPatType.isPositive() ? 1 : -1;
2351                     break;
2352                 }
2353             }
2354             if (tmpLen > outLen) {
2355                 // Better parse result with single hour digit
2356                 outLen = tmpLen;
2357                 sign = tmpSign;
2358                 offsetH = fields[0];
2359                 offsetM = fields[1];
2360                 offsetS = fields[2];
2361             }
2362         }
2363 
2364         if (parsedLen != null && parsedLen.length >= 1) {
2365             parsedLen[0] = outLen;
2366         }
2367 
2368         if (outLen > 0) {
2369             offset = ((((offsetH * 60) + offsetM) * 60) + offsetS) * 1000 * sign;
2370         }
2371 
2372         return offset;
2373     }
2374 
2375     /**
2376      * Parses localized GMT offset fields with the given pattern
2377      *
2378      * @param text the input text
2379      * @param start the start index
2380      * @param patternItems the pattern (already itemized)
2381      * @param forceSingleHourDigit true if hours field is parsed as a single digit
2382      * @param fields receives the parsed hours/minutes/seconds
2383      * @return parsed length
2384      */
2385     private int parseOffsetFieldsWithPattern(String text, int start, Object[] patternItems, boolean forceSingleHourDigit, int fields[]) {
2386         assert (fields != null && fields.length >= 3);
2387         fields[0] = fields[1] = fields[2] = 0;
2388 
2389         boolean failed = false;
2390         int offsetH, offsetM, offsetS;
2391         offsetH = offsetM = offsetS = 0;
2392         int idx = start;
2393         int[] tmpParsedLen = {0};
2394         for (int i = 0; i < patternItems.length; i++) {
2395             if (patternItems[i] instanceof String) {
2396                 String patStr = (String)patternItems[i];
2397                 int len = patStr.length();
2398                 if (!text.regionMatches(true, idx, patStr, 0, len)) {
2399                     failed = true;
2400                     break;
2401                 }
2402                 idx += len;
2403             } else {
2404                 assert(patternItems[i] instanceof GMTOffsetField);
2405                 GMTOffsetField field = (GMTOffsetField)patternItems[i];
2406                 char fieldType = field.getType();
2407                 if (fieldType == 'H') {
2408                     int maxDigits = forceSingleHourDigit ? 1 : 2;
2409                     offsetH = parseOffsetFieldWithLocalizedDigits(text, idx, 1, maxDigits, 0, MAX_OFFSET_HOUR, tmpParsedLen);
2410                 } else if (fieldType == 'm') {
2411                     offsetM = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_MINUTE, tmpParsedLen);
2412                 } else if (fieldType == 's') {
2413                     offsetS = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_SECOND, tmpParsedLen);
2414                 }
2415 
2416                 if (tmpParsedLen[0] == 0) {
2417                     failed = true;
2418                     break;
2419                 }
2420                 idx += tmpParsedLen[0];
2421             }
2422         }
2423 
2424         if (failed) {
2425             return 0;
2426         }
2427 
2428         fields[0] = offsetH;
2429         fields[1] = offsetM;
2430         fields[2] = offsetS;
2431 
2432         return idx - start;
2433     }
2434 
2435     /**
2436      * Parses the input text using the default format patterns (e.g. "UTC{0}").
2437      * @param text the input text
2438      * @param start the start index
2439      * @param parsedLen the parsed length, or 0 on failure
2440      * @return the parsed offset in milliseconds.
2441      */
2442     private int parseOffsetDefaultLocalizedGMT(String text, int start, int[] parsedLen) {
2443         int idx = start;
2444         int offset = 0;
2445         int parsed = 0;
2446         do {
2447             // check global default GMT alternatives
2448             int gmtLen = 0;
2449             for (String gmt : ALT_GMT_STRINGS) {
2450                 int len = gmt.length();
2451                 if (text.regionMatches(true, idx, gmt, 0, len)) {
2452                     gmtLen = len;
2453                     break;
2454                 }
2455             }
2456             if (gmtLen == 0) {
2457                 break;
2458             }
2459             idx += gmtLen;
2460 
2461             // offset needs a sign char and a digit at minimum
2462             if (idx + 1 >= text.length()) {
2463                 break;
2464             }
2465 
2466             // parse sign
2467             int sign = 1;
2468             char c = text.charAt(idx);
2469             if (c == '+') {
2470                 sign = 1;
2471             } else if (c == '-') {
2472                 sign = -1;
2473             } else {
2474                 break;
2475             }
2476             idx++;
2477 
2478             // offset part
2479             // try the default pattern with the separator first
2480             int[] lenWithSep = {0};
2481             int offsetWithSep = parseDefaultOffsetFields(text, idx, DEFAULT_GMT_OFFSET_SEP, lenWithSep);
2482             if (lenWithSep[0] == text.length() - idx) {
2483                 // maximum match
2484                 offset = offsetWithSep * sign;
2485                 idx += lenWithSep[0];
2486             } else {
2487                 // try abutting field pattern
2488                 int[] lenAbut = {0};
2489                 int offsetAbut = parseAbuttingOffsetFields(text, idx, lenAbut);
2490 
2491                 if (lenWithSep[0] > lenAbut[0]) {
2492                     offset = offsetWithSep * sign;
2493                     idx += lenWithSep[0];
2494                 } else {
2495                     offset = offsetAbut * sign;
2496                     idx += lenAbut[0];
2497                 }
2498             }
2499             parsed = idx - start;
2500         } while (false);
2501 
2502         parsedLen[0] = parsed;
2503         return offset;
2504     }
2505 
2506     /**
2507      * Parses the input GMT offset fields with the default offset pattern.
2508      * @param text the input text
2509      * @param start the start index
2510      * @param separator the separator character, e.g. ':'
2511      * @param parsedLen the parsed length, or 0 on failure.
2512      * @return the parsed offset in milliseconds.
2513      */
2514     private int parseDefaultOffsetFields(String text, int start, char separator, int[] parsedLen) {
2515         int max = text.length();
2516         int idx = start;
2517         int[] len = {0};
2518         int hour = 0, min = 0, sec = 0;
2519 
2520         do {
2521             hour = parseOffsetFieldWithLocalizedDigits(text, idx, 1, 2, 0, MAX_OFFSET_HOUR, len);
2522             if (len[0] == 0) {
2523                 break;
2524             }
2525             idx += len[0];
2526 
2527             if (idx + 1 < max && text.charAt(idx) == separator) {
2528                 min = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_MINUTE, len);
2529                 if (len[0] == 0) {
2530                     break;
2531                 }
2532                 idx += (1 + len[0]);
2533 
2534                 if (idx + 1 < max && text.charAt(idx) == separator) {
2535                     sec = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_SECOND, len);
2536                     if (len[0] == 0) {
2537                         break;
2538                     }
2539                     idx += (1 + len[0]);
2540                 }
2541             }
2542         } while (false);
2543 
2544         if (idx == start) {
2545             parsedLen[0] = 0;
2546             return 0;
2547         }
2548 
2549         parsedLen[0] = idx - start;
2550         return hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND;
2551     }
2552 
2553     /**
2554      * Parses abutting localized GMT offset fields (such as 0800) into offset.
2555      * @param text the input text
2556      * @param start the start index
2557      * @param parsedLen the parsed length, or 0 on failure
2558      * @return the parsed offset in milliseconds.
2559      */
2560     private int parseAbuttingOffsetFields(String text, int start, int[] parsedLen) {
2561         final int MAXDIGITS = 6;
2562         int[] digits = new int[MAXDIGITS];
2563         int[] parsed = new int[MAXDIGITS];  // accumulative offsets
2564 
2565         // Parse digits into int[]
2566         int idx = start;
2567         int[] len = {0};
2568         int numDigits = 0;
2569         for (int i = 0; i < MAXDIGITS; i++) {
2570             digits[i] = parseSingleLocalizedDigit(text, idx, len);
2571             if (digits[i] < 0) {
2572                 break;
2573             }
2574             idx += len[0];
2575             parsed[i] = idx - start;
2576             numDigits++;
2577         }
2578 
2579         if (numDigits == 0) {
2580             parsedLen[0] = 0;
2581             return 0;
2582         }
2583 
2584         int offset = 0;
2585         while (numDigits > 0) {
2586             int hour = 0;
2587             int min = 0;
2588             int sec = 0;
2589 
2590             assert(numDigits > 0 && numDigits <= 6);
2591             switch (numDigits) {
2592             case 1: // H
2593                 hour = digits[0];
2594                 break;
2595             case 2: // HH
2596                 hour = digits[0] * 10 + digits[1];
2597                 break;
2598             case 3: // Hmm
2599                 hour = digits[0];
2600                 min = digits[1] * 10 + digits[2];
2601                 break;
2602             case 4: // HHmm
2603                 hour = digits[0] * 10 + digits[1];
2604                 min = digits[2] * 10 + digits[3];
2605                 break;
2606             case 5: // Hmmss
2607                 hour = digits[0];
2608                 min = digits[1] * 10 + digits[2];
2609                 sec = digits[3] * 10 + digits[4];
2610                 break;
2611             case 6: // HHmmss
2612                 hour = digits[0] * 10 + digits[1];
2613                 min = digits[2] * 10 + digits[3];
2614                 sec = digits[4] * 10 + digits[5];
2615                 break;
2616             }
2617             if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) {
2618                 // found a valid combination
2619                 offset = hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND;
2620                 parsedLen[0] = parsed[numDigits - 1];
2621                 break;
2622             }
2623             numDigits--;
2624         }
2625         return offset;
2626     }
2627 
2628     /**
2629      * Reads an offset field value. This method will stop parsing when
2630      * 1) number of digits reaches <code>maxDigits</code>
2631      * 2) just before already parsed number exceeds <code>maxVal</code>
2632      *
2633      * @param text the text
2634      * @param start the start offset
2635      * @param minDigits the minimum number of required digits
2636      * @param maxDigits the maximum number of digits
2637      * @param minVal the minimum value
2638      * @param maxVal the maximum value
2639      * @param parsedLen the actual parsed length is set to parsedLen[0], must not be null.
2640      * @return the integer value parsed
2641      */
2642     private int parseOffsetFieldWithLocalizedDigits(String text, int start, int minDigits, int maxDigits,
2643             int minVal, int maxVal, int[] parsedLen) {
2644 
2645         parsedLen[0] = 0;
2646 
2647         int decVal = 0;
2648         int numDigits = 0;
2649         int idx = start;
2650         int[] digitLen = {0};
2651         while (idx < text.length() && numDigits < maxDigits) {
2652             int digit = parseSingleLocalizedDigit(text, idx, digitLen);
2653             if (digit < 0) {
2654                 break;
2655             }
2656             int tmpVal = decVal * 10 + digit;
2657             if (tmpVal > maxVal) {
2658                 break;
2659             }
2660             decVal = tmpVal;
2661             numDigits++;
2662             idx += digitLen[0];
2663         }
2664 
2665         // Note: maxVal is checked in the while loop
2666         if (numDigits < minDigits || decVal < minVal) {
2667             decVal = -1;
2668             numDigits = 0;
2669         } else {
2670             parsedLen[0] = idx - start;
2671         }
2672 
2673 
2674         return decVal;
2675     }
2676 
2677     /**
2678      * Reads a single decimal digit, either localized digits used by this object
2679      * or any Unicode numeric character.
2680      * @param text the text
2681      * @param start the start index
2682      * @param len the actual length read from the text
2683      * the start index is not a decimal number.
2684      * @return the integer value of the parsed digit, or -1 on failure.
2685      */
2686     private int parseSingleLocalizedDigit(String text, int start, int[] len) {
2687         int digit = -1;
2688         len[0] = 0;
2689         if (start < text.length()) {
2690             int cp = Character.codePointAt(text, start);
2691 
2692             // First, try digits configured for this instance
2693             for (int i = 0; i < _gmtOffsetDigits.length; i++) {
2694                 if (cp == _gmtOffsetDigits[i].codePointAt(0)) {
2695                     digit = i;
2696                     break;
2697                 }
2698             }
2699             // If failed, check if this is a Unicode digit
2700             if (digit < 0) {
2701                 digit = UCharacter.digit(cp);
2702             }
2703 
2704             if (digit >= 0) {
2705                 len[0] = Character.charCount(cp);
2706             }
2707         }
2708         return digit;
2709     }
2710 
2711     /**
2712      * Break input String into String[]. Each array element represents
2713      * a code point. This method is used for parsing localized digit
2714      * characters and support characters in Unicode supplemental planes.
2715      *
2716      * @param str the string
2717      * @return the array of code points in String[]
2718      */
2719     private static String[] toCodePoints(String str) {
2720         int len = str.codePointCount(0, str.length());
2721         String[] codePoints = new String[len];
2722 
2723         for (int i = 0, offset = 0; i < len; i++) {
2724             int code = str.codePointAt(offset);
2725             int codeLen = Character.charCount(code);
2726             codePoints[i] = str.substring(offset, offset + codeLen);
2727             offset += codeLen;
2728         }
2729         return codePoints;
2730     }
2731 
2732 
2733     /**
2734      * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601 time zone string
2735      * (basic format, extended format, or UTC indicator). When the given string is not an ISO 8601 time
2736      * zone string, this method sets the current position as the error index
2737      * to <code>ParsePosition pos</code> and returns 0.
2738      *
2739      * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-08:00", "Z")
2740      * at the position.
2741      * @param pos the position.
2742      * @param extendedOnly <code>true</code> if parsing the text as ISO 8601 extended offset format (e.g. "-08:00"),
2743      *                     or <code>false</code> to evaluate the text as basic format.
2744      * @param hasDigitOffset receiving if the parsed zone string contains offset digits.
2745      * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style
2746      * time zone string.
2747      */
2748     private static int parseOffsetISO8601(String text, ParsePosition pos, boolean extendedOnly, Output<Boolean> hasDigitOffset) {
2749         if (hasDigitOffset != null) {
2750             hasDigitOffset.value = false;
2751         }
2752         int start = pos.getIndex();
2753         if (start >= text.length()) {
2754             pos.setErrorIndex(start);
2755             return 0;
2756         }
2757 
2758         char firstChar = text.charAt(start);
2759         if (Character.toUpperCase(firstChar) == ISO8601_UTC.charAt(0)) {
2760             // "Z" - indicates UTC
2761             pos.setIndex(start + 1);
2762             return 0;
2763         }
2764 
2765         int sign;
2766         if (firstChar == '+') {
2767             sign = 1;
2768         } else if (firstChar == '-') {
2769             sign = -1;
2770         } else {
2771             // Not an ISO 8601 offset string
2772             pos.setErrorIndex(start);
2773             return 0;
2774         }
2775         ParsePosition posOffset = new ParsePosition(start + 1);
2776         int offset = parseAsciiOffsetFields(text, posOffset, ':', OffsetFields.H, OffsetFields.HMS);
2777         if (posOffset.getErrorIndex() == -1 && !extendedOnly && (posOffset.getIndex() - start <= 3)) {
2778             // If the text is successfully parsed as extended format with the options above, it can be also parsed
2779             // as basic format. For example, "0230" can be parsed as offset 2:00 (only first digits are valid for
2780             // extended format), but it can be parsed as offset 2:30 with basic format. We use longer result.
2781             ParsePosition posBasic = new ParsePosition(start + 1);
2782             int tmpOffset = parseAbuttingAsciiOffsetFields(text, posBasic, OffsetFields.H, OffsetFields.HMS, false);
2783             if (posBasic.getErrorIndex() == -1 && posBasic.getIndex() > posOffset.getIndex()) {
2784                 offset = tmpOffset;
2785                 posOffset.setIndex(posBasic.getIndex());
2786             }
2787         }
2788 
2789         if (posOffset.getErrorIndex() != -1) {
2790             pos.setErrorIndex(start);
2791             return 0;
2792         }
2793 
2794         pos.setIndex(posOffset.getIndex());
2795         if (hasDigitOffset != null) {
2796             hasDigitOffset.value = true;
2797         }
2798         return sign * offset;
2799     }
2800 
2801     /**
2802      * Parses offset represented by contiguous ASCII digits
2803      * <p>
2804      * Note: This method expects the input position is already at the start of
2805      * ASCII digits and does not parse sign (+/-).
2806      *
2807      * @param text The text contains a sequence of ASCII digits
2808      * @param pos The parse position
2809      * @param minFields The minimum Fields to be parsed
2810      * @param maxFields The maximum Fields to be parsed
2811      * @param fixedHourWidth true if hours field must be width of 2
2812      * @return Parsed offset, 0 or positive number.
2813      */
2814     private static int parseAbuttingAsciiOffsetFields(String text, ParsePosition pos,
2815             OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWidth) {
2816         int start = pos.getIndex();
2817 
2818         int minDigits = 2 * (minFields.ordinal() + 1) - (fixedHourWidth ? 0 : 1);
2819         int maxDigits = 2 * (maxFields.ordinal() + 1);
2820 
2821         int[] digits = new int[maxDigits];
2822         int numDigits = 0;
2823         int idx = start;
2824         while (numDigits < digits.length && idx < text.length()) {
2825             int digit = ASCII_DIGITS.indexOf(text.charAt(idx));
2826             if (digit < 0) {
2827                 break;
2828             }
2829             digits[numDigits] = digit;
2830             numDigits++;
2831             idx++;
2832         }
2833 
2834         if (fixedHourWidth && ((numDigits & 1) != 0)) {
2835             // Fixed digits, so the number of digits must be even number. Truncating.
2836             numDigits--;
2837         }
2838 
2839         if (numDigits < minDigits) {
2840             pos.setErrorIndex(start);
2841             return 0;
2842         }
2843 
2844         int hour = 0, min = 0, sec = 0;
2845         boolean bParsed = false;
2846         while (numDigits >= minDigits) {
2847             switch (numDigits) {
2848             case 1: //H
2849                 hour = digits[0];
2850                 break;
2851             case 2: //HH
2852                 hour = digits[0] * 10 + digits[1];
2853                 break;
2854             case 3: //Hmm
2855                 hour = digits[0];
2856                 min = digits[1] * 10 + digits[2];
2857                 break;
2858             case 4: //HHmm
2859                 hour = digits[0] * 10 + digits[1];
2860                 min = digits[2] * 10 + digits[3];
2861                 break;
2862             case 5: //Hmmss
2863                 hour = digits[0];
2864                 min = digits[1] * 10 + digits[2];
2865                 sec = digits[3] * 10 + digits[4];
2866                 break;
2867             case 6: //HHmmss
2868                 hour = digits[0] * 10 + digits[1];
2869                 min = digits[2] * 10 + digits[3];
2870                 sec = digits[4] * 10 + digits[5];
2871                 break;
2872             }
2873 
2874             if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) {
2875                 // Successfully parsed
2876                 bParsed = true;
2877                 break;
2878             }
2879 
2880             // Truncating
2881             numDigits -= (fixedHourWidth ? 2 : 1);
2882             hour = min = sec = 0;
2883         }
2884 
2885         if (!bParsed) {
2886             pos.setErrorIndex(start);
2887             return 0;
2888         }
2889         pos.setIndex(start + numDigits);
2890         return ((((hour * 60) + min) * 60) + sec) * 1000;
2891     }
2892 
2893     /**
2894      * Parses offset represented by ASCII digits and separators.
2895      * <p>
2896      * Note: This method expects the input position is already at the start of
2897      * ASCII digits and does not parse sign (+/-).
2898      *
2899      * @param text The text
2900      * @param pos The parse position
2901      * @param sep The separator character
2902      * @param minFields The minimum Fields to be parsed
2903      * @param maxFields The maximum Fields to be parsed
2904      * @return Parsed offset, 0 or positive number.
2905      */
2906     private static int parseAsciiOffsetFields(String text, ParsePosition pos, char sep,
2907             OffsetFields minFields, OffsetFields maxFields) {
2908         int start = pos.getIndex();
2909         int[] fieldVal = {0, 0, 0};
2910         int[] fieldLen = {0, -1, -1};
2911         for (int idx = start, fieldIdx = 0; idx < text.length() && fieldIdx <= maxFields.ordinal(); idx++) {
2912             char c = text.charAt(idx);
2913             if (c == sep) {
2914                 if (fieldIdx == 0) {
2915                     if (fieldLen[0] == 0) {
2916                         // no hours field
2917                         break;
2918                     }
2919                     // 1 digit hour, move to next field
2920                     fieldIdx++;
2921                 } else {
2922                     if (fieldLen[fieldIdx] != -1) {
2923                         // premature minutes or seconds field
2924                         break;
2925                     }
2926                     fieldLen[fieldIdx] = 0;
2927                 }
2928                 continue;
2929             } else if (fieldLen[fieldIdx] == -1) {
2930                 // no separator after 2 digit field
2931                 break;
2932             }
2933             int digit = ASCII_DIGITS.indexOf(c);
2934             if (digit < 0) {
2935                 // not a digit
2936                 break;
2937             }
2938             fieldVal[fieldIdx] = fieldVal[fieldIdx] * 10 + digit;
2939             fieldLen[fieldIdx]++;
2940             if (fieldLen[fieldIdx] >= 2) {
2941                 // parsed 2 digits, move to next field
2942                 fieldIdx++;
2943             }
2944         }
2945 
2946         int offset = 0;
2947         int parsedLen = 0;
2948         OffsetFields parsedFields = null;
2949         do {
2950             // hour
2951             if (fieldLen[0] == 0) {
2952                 break;
2953             }
2954             if (fieldVal[0] > MAX_OFFSET_HOUR) {
2955                 offset = (fieldVal[0] / 10) * MILLIS_PER_HOUR;
2956                 parsedFields = OffsetFields.H;
2957                 parsedLen = 1;
2958                 break;
2959             }
2960             offset = fieldVal[0] * MILLIS_PER_HOUR;
2961             parsedLen = fieldLen[0];
2962             parsedFields = OffsetFields.H;
2963 
2964             // minute
2965             if (fieldLen[1] != 2 || fieldVal[1] > MAX_OFFSET_MINUTE) {
2966                 break;
2967             }
2968             offset += fieldVal[1] * MILLIS_PER_MINUTE;
2969             parsedLen += (1 + fieldLen[1]);
2970             parsedFields = OffsetFields.HM;
2971 
2972             // second
2973             if (fieldLen[2] != 2 || fieldVal[2] > MAX_OFFSET_SECOND) {
2974                 break;
2975             }
2976             offset += fieldVal[2] * MILLIS_PER_SECOND;
2977             parsedLen += (1 + fieldLen[2]);
2978             parsedFields = OffsetFields.HMS;
2979         } while (false);
2980 
2981         if (parsedFields == null || parsedFields.ordinal() < minFields.ordinal()) {
2982             pos.setErrorIndex(start);
2983             return 0;
2984         }
2985 
2986         pos.setIndex(start + parsedLen);
2987         return offset;
2988     }
2989 
2990     /**
2991      * Parse a zone ID.
2992      * @param text the text contains a time zone ID string at the position.
2993      * @param pos the position.
2994      * @return The zone ID parsed.
2995      */
2996     private static String parseZoneID(String text, ParsePosition pos) {
2997         String resolvedID = null;
2998         if (ZONE_ID_TRIE == null) {
2999             synchronized (TimeZoneFormat.class) {
3000                 if (ZONE_ID_TRIE == null) {
3001                     // Build zone ID trie
3002                     TextTrieMap<String> trie = new TextTrieMap<String>(true);
3003                     String[] ids = TimeZone.getAvailableIDs();
3004                     for (String id : ids) {
3005                         trie.put(id, id);
3006                     }
3007                     ZONE_ID_TRIE = trie;
3008                 }
3009             }
3010         }
3011 
3012         int[] matchLen = new int[] {0};
3013         Iterator<String> itr = ZONE_ID_TRIE.get(text, pos.getIndex(), matchLen);
3014         if (itr != null) {
3015             resolvedID = itr.next();
3016             pos.setIndex(pos.getIndex() + matchLen[0]);
3017         } else {
3018             // TODO
3019             // We many need to handle rule based custom zone ID (See ZoneMeta.parseCustomID),
3020             // such as GM+05:00. However, the public parse method in this class also calls
3021             // parseOffsetLocalizedGMT and custom zone IDs are likely supported by the parser,
3022             // so we might not need to handle them here.
3023             pos.setErrorIndex(pos.getIndex());
3024         }
3025         return resolvedID;
3026     }
3027 
3028     /**
3029      * Parse a short zone ID.
3030      * @param text the text contains a time zone ID string at the position.
3031      * @param pos the position.
3032      * @return The zone ID for the parsed short zone ID.
3033      */
3034     private static String parseShortZoneID(String text, ParsePosition pos) {
3035         String resolvedID = null;
3036         if (SHORT_ZONE_ID_TRIE == null) {
3037             synchronized (TimeZoneFormat.class) {
3038                 if (SHORT_ZONE_ID_TRIE == null) {
3039                     // Build short zone ID trie
3040                     TextTrieMap<String> trie = new TextTrieMap<String>(true);
3041                     Set<String> canonicalIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
3042                     for (String id : canonicalIDs) {
3043                         String shortID = ZoneMeta.getShortID(id);
3044                         if (shortID != null) {
3045                             trie.put(shortID, id);
3046                         }
3047                     }
3048                     // Canonical list does not contain Etc/Unknown
3049                     trie.put(UNKNOWN_SHORT_ZONE_ID, UNKNOWN_ZONE_ID);
3050                     SHORT_ZONE_ID_TRIE = trie;
3051                 }
3052             }
3053         }
3054 
3055         int[] matchLen = new int[] {0};
3056         Iterator<String> itr = SHORT_ZONE_ID_TRIE.get(text, pos.getIndex(), matchLen);
3057         if (itr != null) {
3058             resolvedID = itr.next();
3059             pos.setIndex(pos.getIndex() + matchLen[0]);
3060         } else {
3061             pos.setErrorIndex(pos.getIndex());
3062         }
3063 
3064         return resolvedID;
3065     }
3066 
3067     /**
3068      * Parse an exemplar location string.
3069      * @param text the text contains an exemplar location string at the position.
3070      * @param pos the position.
3071      * @return The zone ID for the parsed exemplar location.
3072      */
3073     private String parseExemplarLocation(String text, ParsePosition pos) {
3074         int startIdx = pos.getIndex();
3075         int parsedPos = -1;
3076         String tzID = null;
3077 
3078         EnumSet<NameType> nameTypes = EnumSet.of(NameType.EXEMPLAR_LOCATION);
3079         Collection<MatchInfo> exemplarMatches = _tznames.find(text, startIdx, nameTypes);
3080         if (exemplarMatches != null) {
3081             MatchInfo exemplarMatch = null;
3082             for (MatchInfo match : exemplarMatches) {
3083                 if (startIdx + match.matchLength() > parsedPos) {
3084                     exemplarMatch = match;
3085                     parsedPos = startIdx + match.matchLength();
3086                 }
3087             }
3088             if (exemplarMatch != null) {
3089                 tzID = getTimeZoneID(exemplarMatch.tzID(), exemplarMatch.mzID());
3090                 pos.setIndex(parsedPos);
3091             }
3092         }
3093         if (tzID == null) {
3094             pos.setErrorIndex(startIdx);
3095         }
3096 
3097         return tzID;
3098     }
3099 
3100     /**
3101      * Implements <code>TimeZoneFormat</code> object cache
3102      */
3103     private static class TimeZoneFormatCache extends SoftCache<ULocale, TimeZoneFormat, ULocale> {
3104 
3105         /* (non-Javadoc)
3106          * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
3107          */
3108         @Override
3109         protected TimeZoneFormat createInstance(ULocale key, ULocale data) {
3110             TimeZoneFormat fmt = new TimeZoneFormat(data);
3111             fmt.freeze();
3112             return fmt;
3113         }
3114     }
3115 
3116     // ----------------------------------
3117     // Serialization stuff
3118     //-----------------------------------
3119 
3120     /**
3121      * @serialField _locale ULocale The locale of this TimeZoneFormat object.
3122      * @serialField _tznames TimeZoneNames The time zone name data.
3123      * @serialField _gmtPattern String The pattern string for localized GMT format.
3124      * @serialField _gmtOffsetPatterns Stirng[] The array of GMT offset patterns used by localized GMT format
3125      *              (positive hour-min, positive hour-min-sec, negative hour-min, negative hour-min-sec).
3126      * @serialField _gmtOffsetDigits String[] The array of decimal digits used by localized GMT format
3127      *              (the size of array is 10).
3128      * @serialField _gmtZeroFormat String The localized GMT string used for GMT(UTC).
3129      * @serialField _parseAllStyles boolean <code>true</code> if this TimeZoneFormat object is configure
3130      *              for parsing all available names.
3131      */
3132     private static final ObjectStreamField[] serialPersistentFields = {
3133         new ObjectStreamField("_locale", ULocale.class),
3134         new ObjectStreamField("_tznames", TimeZoneNames.class),
3135         new ObjectStreamField("_gmtPattern", String.class),
3136         new ObjectStreamField("_gmtOffsetPatterns", String[].class),
3137         new ObjectStreamField("_gmtOffsetDigits", String[].class),
3138         new ObjectStreamField("_gmtZeroFormat", String.class),
3139         new ObjectStreamField("_parseAllStyles", boolean.class),
3140     };
3141 
3142     /**
3143      *
3144      * @param oos the object output stream
3145      * @throws IOException
3146      */
3147     private void writeObject(ObjectOutputStream oos) throws IOException {
3148         ObjectOutputStream.PutField fields = oos.putFields();
3149 
3150         fields.put("_locale", _locale);
3151         fields.put("_tznames", _tznames);
3152         fields.put("_gmtPattern", _gmtPattern);
3153         fields.put("_gmtOffsetPatterns", _gmtOffsetPatterns);
3154         fields.put("_gmtOffsetDigits", _gmtOffsetDigits);
3155         fields.put("_gmtZeroFormat", _gmtZeroFormat);
3156         fields.put("_parseAllStyles", _parseAllStyles);
3157 
3158         oos.writeFields();
3159     }
3160 
3161     /**
3162      *
3163      * @param ois the object input stream
3164      * @throws ClassNotFoundException
3165      * @throws IOException
3166      */
3167     private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
3168         ObjectInputStream.GetField fields = ois.readFields();
3169 
3170         _locale = (ULocale)fields.get("_locale", null);
3171         if (_locale == null) {
3172             throw new InvalidObjectException("Missing field: locale");
3173         }
3174 
3175         _tznames = (TimeZoneNames)fields.get("_tznames", null);
3176         if (_tznames == null) {
3177             throw new InvalidObjectException("Missing field: tznames");
3178         }
3179 
3180         _gmtPattern = (String)fields.get("_gmtPattern", null);
3181         if (_gmtPattern == null) {
3182             throw new InvalidObjectException("Missing field: gmtPattern");
3183         }
3184 
3185         String[] tmpGmtOffsetPatterns = (String[])fields.get("_gmtOffsetPatterns", null);
3186         if (tmpGmtOffsetPatterns == null) {
3187             throw new InvalidObjectException("Missing field: gmtOffsetPatterns");
3188         } else if (tmpGmtOffsetPatterns.length < 4) {
3189             throw new InvalidObjectException("Incompatible field: gmtOffsetPatterns");
3190         }
3191         _gmtOffsetPatterns = new String[6];
3192         if (tmpGmtOffsetPatterns.length == 4) {
3193             for (int i = 0; i < 4; i++) {
3194                 _gmtOffsetPatterns[i] = tmpGmtOffsetPatterns[i];
3195             }
3196             _gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()]);
3197             _gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()]);
3198         } else {
3199             _gmtOffsetPatterns = tmpGmtOffsetPatterns;
3200         }
3201 
3202         _gmtOffsetDigits = (String[])fields.get("_gmtOffsetDigits", null);
3203         if (_gmtOffsetDigits == null) {
3204             throw new InvalidObjectException("Missing field: gmtOffsetDigits");
3205         } else if (_gmtOffsetDigits.length != 10) {
3206             throw new InvalidObjectException("Incompatible field: gmtOffsetDigits");
3207         }
3208 
3209         _gmtZeroFormat = (String)fields.get("_gmtZeroFormat", null);
3210         if (_gmtZeroFormat == null) {
3211             throw new InvalidObjectException("Missing field: gmtZeroFormat");
3212         }
3213 
3214         _parseAllStyles = fields.get("_parseAllStyles", false);
3215         if (fields.defaulted("_parseAllStyles")) {
3216             throw new InvalidObjectException("Missing field: parseAllStyles");
3217         }
3218 
3219         // Optimization for TimeZoneNames
3220         //
3221         // Note:
3222         //
3223         // com.ibm.icu.impl.TimeZoneNamesImpl is a read-only object initialized
3224         // by locale only. But it loads time zone names from resource bundles and
3225         // builds trie for parsing. We want to keep TimeZoneNamesImpl as singleton
3226         // per locale. We cannot do this for custom TimeZoneNames provided by user.
3227         //
3228         // com.ibm.icu.impl.TimeZoneGenericNames is a runtime generated object
3229         // initialized by ULocale and TimeZoneNames. Like TimeZoneNamesImpl, it
3230         // also composes time zone names and trie for parsing. We also want to keep
3231         // TimeZoneGenericNames as siongleton per locale. If TimeZoneNames is
3232         // actually a TimeZoneNamesImpl, we can reuse cached TimeZoneGenericNames
3233         // instance.
3234         if (_tznames instanceof TimeZoneNamesImpl) {
3235             _tznames = TimeZoneNames.getInstance(_locale);
3236             _gnames = null; // will be created by _locale later when necessary
3237         } else {
3238             // Custom TimeZoneNames implementation is used. We need to create
3239             // a new instance of TimeZoneGenericNames here.
3240             _gnames = new TimeZoneGenericNames(_locale, _tznames);
3241         }
3242 
3243         // Transient fields requiring initialization
3244         initGMTPattern(_gmtPattern);
3245         initGMTOffsetPatterns(_gmtOffsetPatterns);
3246 
3247     }
3248 
3249     // ----------------------------------
3250     // Freezable stuff
3251     //-----------------------------------
3252 
3253     /**
3254      * {@inheritDoc}
3255      * @stable ICU 49
3256      */
3257     public boolean isFrozen() {
3258         return _frozen;
3259     }
3260 
3261     /**
3262      * {@inheritDoc}
3263      * @stable ICU 49
3264      */
3265     public TimeZoneFormat freeze() {
3266         _frozen = true;
3267         return this;
3268     }
3269 
3270     /**
3271      * {@inheritDoc}
3272      * @stable ICU 49
3273      */
3274     public TimeZoneFormat cloneAsThawed() {
3275         TimeZoneFormat copy = (TimeZoneFormat)super.clone();
3276         copy._frozen = false;
3277         return copy;
3278     }
3279 }
3280 
3281