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