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