All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.TimeZoneFormat Maven / Gradle / Ivy

The newest version!
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2011-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.text;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Set;

import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SoftCache;
import com.ibm.icu.impl.TZDBTimeZoneNames;
import com.ibm.icu.impl.TextTrieMap;
import com.ibm.icu.impl.TimeZoneGenericNames;
import com.ibm.icu.impl.TimeZoneGenericNames.GenericMatchInfo;
import com.ibm.icu.impl.TimeZoneGenericNames.GenericNameType;
import com.ibm.icu.impl.TimeZoneNamesImpl;
import com.ibm.icu.impl.ZoneMeta;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.TimeZoneNames.MatchInfo;
import com.ibm.icu.text.TimeZoneNames.NameType;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.ULocale;

/**
 * TimeZoneFormat supports time zone display name formatting and parsing.
 * An instance of TimeZoneFormat works as a subformatter of {@link SimpleDateFormat},
 * but you can also directly get a new instance of TimeZoneFormat and
 * formatting/parsing time zone display names.
 * 

* ICU implements the time zone display names defined by UTS#35 * Unicode Locale Data Markup Language (LDML). {@link TimeZoneNames} represents the * time zone display name data model and this class implements the algorithm for actual * formatting and parsing. * * @see SimpleDateFormat * @see TimeZoneNames * @stable ICU 49 */ public class TimeZoneFormat extends UFormat implements Freezable, Serializable { private static final long serialVersionUID = 2281246852693575022L; private static final int ISO_Z_STYLE_FLAG = 0x0080; private static final int ISO_LOCAL_STYLE_FLAG = 0x0100; /** * Time zone display format style enum used by format/parse APIs in TimeZoneFormat. * * @see TimeZoneFormat#format(Style, TimeZone, long) * @see TimeZoneFormat#format(Style, TimeZone, long, Output) * @see TimeZoneFormat#parse(Style, String, ParsePosition, Output) * @stable ICU 49 */ public enum Style { /** * Generic location format, such as "United States Time (New York)" and "Italy Time". * This style is equivalent to the LDML date format pattern "VVVV". * @stable ICU 49 */ GENERIC_LOCATION (0x0001), /** * Generic long non-location format, such as "Eastern Time". * This style is equivalent to the LDML date format pattern "vvvv". * @stable ICU 49 */ GENERIC_LONG (0x0002), /** * Generic short non-location format, such as "ET". * This style is equivalent to the LDML date format pattern "v". * @stable ICU 49 */ GENERIC_SHORT (0x0004), /** * Specific long format, such as "Eastern Standard Time". * This style is equivalent to the LDML date format pattern "zzzz". * @stable ICU 49 */ SPECIFIC_LONG (0x0008), /** * Specific short format, such as "EST", "PDT". * This style is equivalent to the LDML date format pattern "z". * @stable ICU 49 */ SPECIFIC_SHORT (0x0010), /** * Localized GMT offset format, such as "GMT-05:00", "UTC+0100" * This style is equivalent to the LDML date format pattern "OOOO" and "ZZZZ" * @stable ICU 49 */ LOCALIZED_GMT (0x0020), /** * Short localized GMT offset format, such as "GMT-5", "UTC+1:30" * This style is equivalent to the LDML date format pattern "O". * @stable ICU 51 */ LOCALIZED_GMT_SHORT (0x0040), /** * Short ISO 8601 local time difference (basic format) or the UTC indicator. * For example, "-05", "+0530", and "Z"(UTC). * This style is equivalent to the LDML date format pattern "X". * @stable ICU 51 */ ISO_BASIC_SHORT (ISO_Z_STYLE_FLAG), /** * Short ISO 8601 locale time difference (basic format). * For example, "-05" and "+0530". * This style is equivalent to the LDML date format pattern "x". * @stable ICU 51 */ ISO_BASIC_LOCAL_SHORT (ISO_LOCAL_STYLE_FLAG), /** * Fixed width ISO 8601 local time difference (basic format) or the UTC indicator. * For example, "-0500", "+0530", and "Z"(UTC). * This style is equivalent to the LDML date format pattern "XX". * @stable ICU 51 */ ISO_BASIC_FIXED (ISO_Z_STYLE_FLAG), /** * Fixed width ISO 8601 local time difference (basic format). * For example, "-0500" and "+0530". * This style is equivalent to the LDML date format pattern "xx". * @stable ICU 51 */ ISO_BASIC_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG), /** * ISO 8601 local time difference (basic format) with optional seconds field, or the UTC indicator. * For example, "-0500", "+052538", and "Z"(UTC). * This style is equivalent to the LDML date format pattern "XXXX". * @stable ICU 51 */ ISO_BASIC_FULL (ISO_Z_STYLE_FLAG), /** * ISO 8601 local time difference (basic format) with optional seconds field. * For example, "-0500" and "+052538". * This style is equivalent to the LDML date format pattern "xxxx". * @stable ICU 51 */ ISO_BASIC_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG), /** * Fixed width ISO 8601 local time difference (extended format) or the UTC indicator. * For example, "-05:00", "+05:30", and "Z"(UTC). * This style is equivalent to the LDML date format pattern "XXX". * @stable ICU 51 */ ISO_EXTENDED_FIXED (ISO_Z_STYLE_FLAG), /** * Fixed width ISO 8601 local time difference (extended format). * For example, "-05:00" and "+05:30". * This style is equivalent to the LDML date format pattern "xxx" and "ZZZZZ". * @stable ICU 51 */ ISO_EXTENDED_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG), /** * ISO 8601 local time difference (extended format) with optional seconds field, or the UTC indicator. * For example, "-05:00", "+05:25:38", and "Z"(UTC). * This style is equivalent to the LDML date format pattern "XXXXX". * @stable ICU 51 */ ISO_EXTENDED_FULL (ISO_Z_STYLE_FLAG), /** * ISO 8601 local time difference (extended format) with optional seconds field. * For example, "-05:00" and "+05:25:38". * This style is equivalent to the LDML date format pattern "xxxxx". * @stable ICU 51 */ ISO_EXTENDED_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG), /** * Time Zone ID, such as "America/Los_Angeles". * @stable ICU 51 */ ZONE_ID (0x0200), /** * Short Time Zone ID (BCP 47 Unicode location extension, time zone type value), such as "uslax". * @stable ICU 51 */ ZONE_ID_SHORT (0x0400), /** * Exemplar location, such as "Los Angeles" and "Paris". * @stable ICU 51 */ EXEMPLAR_LOCATION (0x0800); final int flag; private Style(int flag) { this.flag = flag; } } /** * Offset pattern type enum. * * @see TimeZoneFormat#getGMTOffsetPattern(GMTOffsetPatternType) * @see TimeZoneFormat#setGMTOffsetPattern(GMTOffsetPatternType, String) * @stable ICU 49 */ public enum GMTOffsetPatternType { /** * Positive offset with hours and minutes fields * @stable ICU 49 */ POSITIVE_HM ("+H:mm", "Hm", true), /** * Positive offset with hours, minutes and seconds fields * @stable ICU 49 */ POSITIVE_HMS ("+H:mm:ss", "Hms", true), /** * Negative offset with hours and minutes fields * @stable ICU 49 */ NEGATIVE_HM ("-H:mm", "Hm", false), /** * Negative offset with hours, minutes and seconds fields * @stable ICU 49 */ NEGATIVE_HMS ("-H:mm:ss", "Hms", false), /** * Positive offset with hours field * @stable ICU 51 */ POSITIVE_H ("+H", "H", true), /** * Negative offset with hours field * @stable ICU 51 */ NEGATIVE_H ("-H", "H", false); private String _defaultPattern; private String _required; private boolean _isPositive; private GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive) { _defaultPattern = defaultPattern; _required = required; _isPositive = isPositive; } private String defaultPattern() { return _defaultPattern; } private String required() { return _required; } private boolean isPositive() { return _isPositive; } } /** * Time type enum used for receiving time type (standard time, daylight time or unknown) * in TimeZoneFormat APIs. * * @stable ICU 49 */ public enum TimeType { /** * Unknown * @stable ICU 49 */ UNKNOWN, /** * Standard time * @stable ICU 49 */ STANDARD, /** * Daylight saving time * @stable ICU 49 */ DAYLIGHT; } /** * Parse option enum, used for specifying optional parse behavior. * @stable ICU 49 */ public enum ParseOption { /** * When a time zone display name is not found within a set of display names * used for the specified style, look for the name from display names used * by other styles. * @stable ICU 49 */ ALL_STYLES, /** * When parsing a time zone display name in {@link Style#SPECIFIC_SHORT}, * look for the IANA tz database compatible zone abbreviations in addition * to the localized names coming from the {@link TimeZoneNames} currently * used by the {@link TimeZoneFormat}. * @stable ICU 54 */ TZ_DATABASE_ABBREVIATIONS; } /* * fields to be serialized */ private ULocale _locale; private TimeZoneNames _tznames; private String _gmtPattern; private String[] _gmtOffsetPatterns; private String[] _gmtOffsetDigits; private String _gmtZeroFormat; private boolean _parseAllStyles; private boolean _parseTZDBNames; /* * Transient fields */ private transient volatile TimeZoneGenericNames _gnames; private transient String _gmtPatternPrefix; private transient String _gmtPatternSuffix; private transient Object[][] _gmtOffsetPatternItems; // cache if offset hours and minutes are abutting private transient boolean _abuttingOffsetHoursAndMinutes; private transient String _region; private volatile transient boolean _frozen; private transient volatile TimeZoneNames _tzdbNames; /* * Static final fields */ private static final String TZID_GMT = "Etc/GMT"; // canonical tzid for GMT private static final String[] ALT_GMT_STRINGS = {"GMT", "UTC", "UT"}; private static final String DEFAULT_GMT_PATTERN = "GMT{0}"; private static final String DEFAULT_GMT_ZERO = "GMT"; private static final String[] DEFAULT_GMT_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; private static final char DEFAULT_GMT_OFFSET_SEP = ':'; private static final String ASCII_DIGITS = "0123456789"; private static final String ISO8601_UTC = "Z"; private static final String UNKNOWN_ZONE_ID = "Etc/Unknown"; private static final String UNKNOWN_SHORT_ZONE_ID = "unk"; private static final String UNKNOWN_LOCATION = "Unknown"; // Order of GMT offset pattern parsing, *_HMS must be evaluated first // because *_HM is most likely a substring of *_HMS private static final GMTOffsetPatternType[] PARSE_GMT_OFFSET_TYPES = { GMTOffsetPatternType.POSITIVE_HMS, GMTOffsetPatternType.NEGATIVE_HMS, GMTOffsetPatternType.POSITIVE_HM, GMTOffsetPatternType.NEGATIVE_HM, GMTOffsetPatternType.POSITIVE_H, GMTOffsetPatternType.NEGATIVE_H, }; private static final int MILLIS_PER_HOUR = 60 * 60 * 1000; private static final int MILLIS_PER_MINUTE = 60 * 1000; private static final int MILLIS_PER_SECOND = 1000; // Maximum offset (exclusive) in millisecond supported by offset formats private static final int MAX_OFFSET = 24 * MILLIS_PER_HOUR; // Maximum values for GMT offset fields private static final int MAX_OFFSET_HOUR = 23; private static final int MAX_OFFSET_MINUTE = 59; private static final int MAX_OFFSET_SECOND = 59; private static final int UNKNOWN_OFFSET = Integer.MAX_VALUE; private static TimeZoneFormatCache _tzfCache = new TimeZoneFormatCache(); // The filter used for searching all specific names and exemplar location names private static final EnumSet ALL_SIMPLE_NAME_TYPES = EnumSet.of( NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, NameType.EXEMPLAR_LOCATION ); // The filter used for searching all generic names private static final EnumSet ALL_GENERIC_NAME_TYPES = EnumSet.of( GenericNameType.LOCATION, GenericNameType.LONG, GenericNameType.SHORT ); private static volatile TextTrieMap ZONE_ID_TRIE; private static volatile TextTrieMap SHORT_ZONE_ID_TRIE; /** * The protected constructor for subclassing. * @param locale the locale * @stable ICU 49 */ protected TimeZoneFormat(ULocale locale) { _locale = locale; _tznames = TimeZoneNames.getInstance(locale); // TimeZoneGenericNames _gnames will be instantiated lazily String gmtPattern = null; String hourFormats = null; _gmtZeroFormat = DEFAULT_GMT_ZERO; try { ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance( ICUData.ICU_ZONE_BASE_NAME, locale); try { gmtPattern = bundle.getStringWithFallback("zoneStrings/gmtFormat"); } catch (MissingResourceException e) { // fall through } try { hourFormats = bundle.getStringWithFallback("zoneStrings/hourFormat"); } catch (MissingResourceException e) { // fall through } try { _gmtZeroFormat = bundle.getStringWithFallback("zoneStrings/gmtZeroFormat"); } catch (MissingResourceException e) { // fall through } } catch (MissingResourceException e) { // fall through } if (gmtPattern == null) { gmtPattern = DEFAULT_GMT_PATTERN; } initGMTPattern(gmtPattern); String[] gmtOffsetPatterns = new String[GMTOffsetPatternType.values().length]; if (hourFormats != null) { String[] hourPatterns = hourFormats.split(";", 2); gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[0]); gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()] = hourPatterns[0]; gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[0]); gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[1]); gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()] = hourPatterns[1]; gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[1]); } else { for (GMTOffsetPatternType patType : GMTOffsetPatternType.values()) { gmtOffsetPatterns[patType.ordinal()] = patType.defaultPattern(); } } initGMTOffsetPatterns(gmtOffsetPatterns); _gmtOffsetDigits = DEFAULT_GMT_DIGITS; NumberingSystem ns = NumberingSystem.getInstance(locale); if (!ns.isAlgorithmic()) { // we do not support algorithmic numbering system for GMT offset for now _gmtOffsetDigits = toCodePoints(ns.getDescription()); } } /** * Returns a frozen instance of TimeZoneFormat for the given locale. *

Note: The instance returned by this method is frozen. If you want to * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a * thawed copy first. * * @param locale the locale. * @return a frozen instance of TimeZoneFormat for the given locale. * @stable ICU 49 */ public static TimeZoneFormat getInstance(ULocale locale) { if (locale == null) { throw new NullPointerException("locale is null"); } return _tzfCache.getInstance(locale, locale); } /** * Returns a frozen instance of TimeZoneFormat for the given * {@link java.util.Locale}. *

Note: The instance returned by this method is frozen. If you want to * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a * thawed copy first. * * @param locale the {@link Locale}. * @return a frozen instance of TimeZoneFormat for the given locale. * @stable ICU 54 */ public static TimeZoneFormat getInstance(Locale locale) { return getInstance(ULocale.forLocale(locale)); } /** * Returns the time zone display name data used by this instance. * * @return the time zone display name data. * @see #setTimeZoneNames(TimeZoneNames) * @stable ICU 49 */ public TimeZoneNames getTimeZoneNames() { return _tznames; } /** * Private method returning the instance of TimeZoneGenericNames * used by this object. The instance of TimeZoneGenericNames might * not be available until the first use (lazy instantiation) because * it is only required for handling generic names (that are not used * by DateFormat's default patterns) and it requires relatively heavy * one time initialization. * @return the instance of TimeZoneGenericNames used by this object. */ private TimeZoneGenericNames getTimeZoneGenericNames() { if (_gnames == null) { // _gnames is volatile synchronized(this) { if (_gnames == null) { _gnames = TimeZoneGenericNames.getInstance(_locale); } } } return _gnames; } /** * Private method returning the instance of TZDBTimeZoneNames. * The instance if used only for parsing when {@link ParseOption#TZ_DATABASE_ABBREVIATIONS} * is enabled. * @return an instance of TZDBTimeZoneNames. */ private TimeZoneNames getTZDBTimeZoneNames() { if (_tzdbNames == null) { synchronized(this) { if (_tzdbNames == null) { _tzdbNames = new TZDBTimeZoneNames(_locale); } } } return _tzdbNames; } /** * Sets the time zone display name data to this instance. * * @param tznames the time zone display name data. * @return this object. * @throws UnsupportedOperationException when this object is frozen. * @see #getTimeZoneNames() * @stable ICU 49 */ public TimeZoneFormat setTimeZoneNames(TimeZoneNames tznames) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); } _tznames = tznames; // TimeZoneGenericNames must be changed to utilize the new TimeZoneNames instance. _gnames = new TimeZoneGenericNames(_locale, _tznames); return this; } /** * Returns the localized GMT format pattern. * * @return the localized GMT format pattern. * @see #setGMTPattern(String) * @stable ICU 49 */ public String getGMTPattern() { return _gmtPattern; } /** * Sets the localized GMT format pattern. The pattern must contain * a single argument {0}, for example "GMT {0}". * * @param pattern the localized GMT format pattern string * @return this object. * @throws IllegalArgumentException when the pattern string does not contain "{0}" * @throws UnsupportedOperationException when this object is frozen. * @see #getGMTPattern() * @stable ICU 49 */ public TimeZoneFormat setGMTPattern(String pattern) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); } initGMTPattern(pattern); return this; } /** * Returns the offset pattern used for localized GMT format. * * @param type the offset pattern enum * @see #setGMTOffsetPattern(GMTOffsetPatternType, String) * @stable ICU 49 */ public String getGMTOffsetPattern(GMTOffsetPatternType type) { return _gmtOffsetPatterns[type.ordinal()]; } /** * Sets the offset pattern for the given offset type. * * @param type the offset pattern. * @param pattern the pattern string. * @return this object. * @throws IllegalArgumentException when the pattern string does not have required time field letters. * @throws UnsupportedOperationException when this object is frozen. * @see #getGMTOffsetPattern(GMTOffsetPatternType) * @stable ICU 49 */ public TimeZoneFormat setGMTOffsetPattern(GMTOffsetPatternType type, String pattern) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); } if (pattern == null) { throw new NullPointerException("Null GMT offset pattern"); } Object[] parsedItems = parseOffsetPattern(pattern, type.required()); _gmtOffsetPatterns[type.ordinal()] = pattern; _gmtOffsetPatternItems[type.ordinal()] = parsedItems; checkAbuttingHoursAndMinutes(); return this; } /** * Returns the decimal digit characters used for localized GMT format in a single string * containing from 0 to 9 in the ascending order. * * @return the decimal digits for localized GMT format. * @see #setGMTOffsetDigits(String) * @stable ICU 49 */ public String getGMTOffsetDigits() { StringBuilder buf = new StringBuilder(_gmtOffsetDigits.length); for (String digit : _gmtOffsetDigits) { buf.append(digit); } return buf.toString(); } /** * Sets the decimal digit characters used for localized GMT format. * * @param digits a string contains the decimal digit characters from 0 to 9 n the ascending order. * @return this object. * @throws IllegalArgumentException when the string did not contain ten characters. * @throws UnsupportedOperationException when this object is frozen. * @see #getGMTOffsetDigits() * @stable ICU 49 */ public TimeZoneFormat setGMTOffsetDigits(String digits) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); } if (digits == null) { throw new NullPointerException("Null GMT offset digits"); } String[] digitArray = toCodePoints(digits); if (digitArray.length != 10) { throw new IllegalArgumentException("Length of digits must be 10"); } _gmtOffsetDigits = digitArray; return this; } /** * Returns the localized GMT format string for GMT(UTC) itself (GMT offset is 0). * * @return the localized GMT string string for GMT(UTC) itself. * @see #setGMTZeroFormat(String) * @stable ICU 49 */ public String getGMTZeroFormat() { return _gmtZeroFormat; } /** * Sets the localized GMT format string for GMT(UTC) itself (GMT offset is 0). * * @param gmtZeroFormat the localized GMT format string for GMT(UTC). * @return this object. * @throws UnsupportedOperationException when this object is frozen. * @see #getGMTZeroFormat() * @stable ICU 49 */ public TimeZoneFormat setGMTZeroFormat(String gmtZeroFormat) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); } if (gmtZeroFormat == null) { throw new NullPointerException("Null GMT zero format"); } if (gmtZeroFormat.length() == 0) { throw new IllegalArgumentException("Empty GMT zero format"); } _gmtZeroFormat = gmtZeroFormat; return this; } /** * Sets the default parse options. *

* Note: By default, an instance of TimeZoneFormat * created by {@link #getInstance(ULocale)} has no parse options set. * * @param options the default parse options. * @return this object. * @see ParseOption * @stable ICU 49 */ public TimeZoneFormat setDefaultParseOptions(EnumSet options) { _parseAllStyles = options.contains(ParseOption.ALL_STYLES); _parseTZDBNames = options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS); return this; } /** * Returns the default parse options used by this TimeZoneFormat instance. * @return the default parse options. * @see ParseOption * @stable ICU 49 */ public EnumSet getDefaultParseOptions() { if (_parseAllStyles && _parseTZDBNames) { return EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS); } else if (_parseAllStyles) { return EnumSet.of(ParseOption.ALL_STYLES); } else if (_parseTZDBNames) { return EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS); } return EnumSet.noneOf(ParseOption.class); } /** * Returns the ISO 8601 basic time zone string for the given offset. * For example, "-08", "-0830" and "Z" * * @param offset the offset from GMT(UTC) in milliseconds. * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0. * @param isShort true if shortest form is used. * @param ignoreSeconds true if non-zero offset seconds is appended. * @return the ISO 8601 basic format. * @throws IllegalArgumentException if the specified offset is out of supported range * (-24 hours < offset < +24 hours). * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean) * @see #parseOffsetISO8601(String, ParsePosition) * @stable ICU 51 */ public final String formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { return formatOffsetISO8601(offset, true, useUtcIndicator, isShort, ignoreSeconds); } /** * Returns the ISO 8601 extended time zone string for the given offset. * For example, "-08:00", "-08:30" and "Z" * * @param offset the offset from GMT(UTC) in milliseconds. * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0. * @param isShort true if shortest form is used. * @param ignoreSeconds true if non-zero offset seconds is appended. * @return the ISO 8601 extended format. * @throws IllegalArgumentException if the specified offset is out of supported range * (-24 hours < offset < +24 hours). * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean) * @see #parseOffsetISO8601(String, ParsePosition) * @stable ICU 51 */ public final String formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { return formatOffsetISO8601(offset, false, useUtcIndicator, isShort, ignoreSeconds); } /** * Returns the localized GMT(UTC) offset format for the given offset. * The localized GMT offset is defined by; *

    *
  • GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) *
  • Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) *
  • Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) *
  • GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) *
* This format always uses 2 digit hours and minutes. When the given offset has non-zero * seconds, 2 digit seconds field will be appended. For example, * GMT+05:00 and GMT+05:28:06. * @param offset the offset from GMT(UTC) in milliseconds. * @return the localized GMT format string * @see #parseOffsetLocalizedGMT(String, ParsePosition) * @throws IllegalArgumentException if the specified offset is out of supported range * (-24 hours < offset < +24 hours). * @stable ICU 49 */ public String formatOffsetLocalizedGMT(int offset) { return formatOffsetLocalizedGMT(offset, false); } /** * Returns the short localized GMT(UTC) offset format for the given offset. * The short localized GMT offset is defined by; *
    *
  • GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) *
  • Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) *
  • Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) *
  • GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) *
* This format uses the shortest representation of offset. The hours field does not * have leading zero and lower fields with zero will be truncated. For example, * GMT+5 and GMT+530. * @param offset the offset from GMT(UTC) in milliseconds. * @return the short localized GMT format string * @see #parseOffsetLocalizedGMT(String, ParsePosition) * @throws IllegalArgumentException if the specified offset is out of supported range * (-24 hours < offset < +24 hours). * @stable ICU 51 */ public String formatOffsetShortLocalizedGMT(int offset) { return formatOffsetLocalizedGMT(offset, true); } /** * Returns the display name of the time zone at the given date for * the style. * *

Note: A style may have fallback styles defined. For example, * when GENERIC_LONG is requested, but there is no display name * data available for GENERIC_LONG style, the implementation * may use GENERIC_LOCATION or LOCALIZED_GMT. * See UTS#35 UNICODE LOCALE DATA MARKUP LANGUAGE (LDML) * Appendix J: Time Zone Display Name * for the details. * * @param style the style enum (e.g. GENERIC_LONG, LOCALIZED_GMT...) * @param tz the time zone. * @param date the date. * @return the display name of the time zone. * @see Style * @see #format(Style, TimeZone, long, Output) * @stable ICU 49 */ public final String format(Style style, TimeZone tz, long date) { return format(style, tz, date, null); } /** * Returns the display name of the time zone at the given date for * the style. This method takes an extra argument Output<TimeType> timeType * in addition to the argument list of {@link #format(Style, TimeZone, long)}. * The argument is used for receiving the time type (standard time * or daylight saving time, or unknown) actually used for the display name. * * @param style the style enum (e.g. GENERIC_LONG, LOCALIZED_GMT...) * @param tz the time zone. * @param date the date. * @param timeType the output argument for receiving the time type (standard/daylight/unknown) * used for the display name, or specify null if the information is not necessary. * @return the display name of the time zone. * @see Style * @see #format(Style, TimeZone, long) * @stable ICU 49 */ public String format(Style style, TimeZone tz, long date, Output timeType) { String result = null; if (timeType != null) { timeType.value = TimeType.UNKNOWN; } boolean noOffsetFormatFallback = false; switch (style) { case GENERIC_LOCATION: result = getTimeZoneGenericNames().getGenericLocationName(ZoneMeta.getCanonicalCLDRID(tz)); break; case GENERIC_LONG: result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.LONG, date); break; case GENERIC_SHORT: result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.SHORT, date); break; case SPECIFIC_LONG: result = formatSpecific(tz, NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, date, timeType); break; case SPECIFIC_SHORT: result = formatSpecific(tz, NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, date, timeType); break; case ZONE_ID: result = tz.getID(); noOffsetFormatFallback = true; break; case ZONE_ID_SHORT: result = ZoneMeta.getShortID(tz); if (result == null) { result = UNKNOWN_SHORT_ZONE_ID; } noOffsetFormatFallback = true; break; case EXEMPLAR_LOCATION: result = formatExemplarLocation(tz); noOffsetFormatFallback = true; break; default: // will be handled below break; } if (result == null && !noOffsetFormatFallback) { int[] offsets = {0, 0}; tz.getOffset(date, false, offsets); int offset = offsets[0] + offsets[1]; switch (style) { case GENERIC_LOCATION: case GENERIC_LONG: case SPECIFIC_LONG: case LOCALIZED_GMT: result = formatOffsetLocalizedGMT(offset); break; case GENERIC_SHORT: case SPECIFIC_SHORT: case LOCALIZED_GMT_SHORT: result = formatOffsetShortLocalizedGMT(offset); break; case ISO_BASIC_SHORT: result = formatOffsetISO8601Basic(offset, true, true, true); break; case ISO_BASIC_LOCAL_SHORT: result = formatOffsetISO8601Basic(offset, false, true, true); break; case ISO_BASIC_FIXED: result = formatOffsetISO8601Basic(offset, true, false, true); break; case ISO_BASIC_LOCAL_FIXED: result = formatOffsetISO8601Basic(offset, false, false, true); break; case ISO_BASIC_FULL: result = formatOffsetISO8601Basic(offset, true, false, false); break; case ISO_BASIC_LOCAL_FULL: result = formatOffsetISO8601Basic(offset, false, false, false); break; case ISO_EXTENDED_FIXED: result = formatOffsetISO8601Extended(offset, true, false, true); break; case ISO_EXTENDED_LOCAL_FIXED: result = formatOffsetISO8601Extended(offset, false, false, true); break; case ISO_EXTENDED_FULL: result = formatOffsetISO8601Extended(offset, true, false, false); break; case ISO_EXTENDED_LOCAL_FULL: result = formatOffsetISO8601Extended(offset, false, false, false); break; default: // Other cases are handled earlier and never comes into this // switch statement. assert false; break; } // time type if (timeType != null) { timeType.value = (offsets[1] != 0) ? TimeType.DAYLIGHT : TimeType.STANDARD; } } assert(result != null); return result; } /** * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601 * basic or extended time zone string. When the given string is not an ISO 8601 time * zone string, this method sets the current position as the error index * to ParsePosition pos and returns 0. * * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-0800", "-08:00", and "Z") * at the position. * @param pos the position. * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style * time zone string. * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean) * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean) * @stable ICU 49 */ public final int parseOffsetISO8601(String text, ParsePosition pos) { return parseOffsetISO8601(text, pos, false, null); } /** * Returns offset from GMT(UTC) in milliseconds for the given localized GMT * offset format string. When the given string cannot be parsed, this method * sets the current position as the error index to ParsePosition pos * and returns 0. * * @param text the text contains a localized GMT offset string at the position. * @param pos the position. * @return the offset from GMT(UTC) in milliseconds for the given localized GMT * offset format string. * @see #formatOffsetLocalizedGMT(int) * @stable ICU 49 */ public int parseOffsetLocalizedGMT(String text, ParsePosition pos) { return parseOffsetLocalizedGMT(text, pos, false, null); } /** * Returns offset from GMT(UTC) in milliseconds for the given short localized GMT * offset format string. When the given string cannot be parsed, this method * sets the current position as the error index to ParsePosition pos * and returns 0. * * @param text the text contains a short localized GMT offset string at the position. * @param pos the position. * @return the offset from GMT(UTC) in milliseconds for the given short localized GMT * offset format string. * @see #formatOffsetShortLocalizedGMT(int) * @stable ICU 51 */ public int parseOffsetShortLocalizedGMT(String text, ParsePosition pos) { return parseOffsetLocalizedGMT(text, pos, true, null); } /** * Returns a TimeZone by parsing the time zone string according to * the parse position, the style and the parse options. * * @param text the text contains a time zone string at the position. * @param style the format style. * @param pos the position. * @param options the parse options. * @param timeType The output argument for receiving the time type (standard/daylight/unknown), * or specify null if the information is not necessary. * @return A TimeZone, or null if the input could not be parsed. * @see Style * @see #format(Style, TimeZone, long, Output) * @stable ICU 49 */ public TimeZone parse(Style style, String text, ParsePosition pos, EnumSet options, Output timeType) { if (timeType == null) { timeType = new Output<>(TimeType.UNKNOWN); } else { timeType.value = TimeType.UNKNOWN; } int startIdx = pos.getIndex(); int maxPos = text.length(); int offset; // Styles using localized GMT format as fallback boolean fallbackLocalizedGMT = (style == Style.SPECIFIC_LONG || style == Style.GENERIC_LONG || style == Style.GENERIC_LOCATION); boolean fallbackShortLocalizedGMT = (style == Style.SPECIFIC_SHORT || style == Style.GENERIC_SHORT); int evaluated = 0; // bit flags representing already evaluated styles ParsePosition tmpPos = new ParsePosition(startIdx); int parsedOffset = UNKNOWN_OFFSET; // stores successfully parsed offset for later use int parsedPos = -1; // stores successfully parsed offset position for later use // Try localized GMT format first if necessary if (fallbackLocalizedGMT || fallbackShortLocalizedGMT) { Output hasDigitOffset = new Output<>(false); offset = parseOffsetLocalizedGMT(text, tmpPos, fallbackShortLocalizedGMT, hasDigitOffset); if (tmpPos.getErrorIndex() == -1) { // Even when the input text was successfully parsed as a localized GMT format text, // we may still need to evaluate the specified style if - // 1) GMT zero format was used, and // 2) The input text was not completely processed if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } parsedOffset = offset; parsedPos = tmpPos.getIndex(); } // Note: For now, no distinction between long/short localized GMT format in the parser. // This might be changed in future. // evaluated |= (fallbackLocalizedGMT ? Style.LOCALIZED_GMT.flag : Style.LOCALIZED_GMT_SHORT.flag); evaluated |= (Style.LOCALIZED_GMT.flag | Style.LOCALIZED_GMT_SHORT.flag); } boolean parseTZDBAbbrev = (options == null) ? getDefaultParseOptions().contains(ParseOption.TZ_DATABASE_ABBREVIATIONS) : options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS); // Try the specified style switch (style) { case LOCALIZED_GMT: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); offset = parseOffsetLocalizedGMT(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } // Note: For now, no distinction between long/short localized GMT format in the parser. // This might be changed in future. evaluated |= Style.LOCALIZED_GMT_SHORT.flag; break; } case LOCALIZED_GMT_SHORT: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); offset = parseOffsetShortLocalizedGMT(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } // Note: For now, no distinction between long/short localized GMT format in the parser. // This might be changed in future. evaluated |= Style.LOCALIZED_GMT.flag; break; } case ISO_BASIC_SHORT: case ISO_BASIC_FIXED: case ISO_BASIC_FULL: case ISO_EXTENDED_FIXED: case ISO_EXTENDED_FULL: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); offset = parseOffsetISO8601(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } break; } case ISO_BASIC_LOCAL_SHORT: case ISO_BASIC_LOCAL_FIXED: case ISO_BASIC_LOCAL_FULL: case ISO_EXTENDED_LOCAL_FIXED: case ISO_EXTENDED_LOCAL_FULL: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); // Exclude the case of UTC Indicator "Z" here Output hasDigitOffset = new Output<>(false); offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset); if (tmpPos.getErrorIndex() == -1 && hasDigitOffset.value) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } break; } case SPECIFIC_LONG: case SPECIFIC_SHORT: { // Specific styles EnumSet nameTypes = null; if (style == Style.SPECIFIC_LONG) { nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT); } else { assert style == Style.SPECIFIC_SHORT; nameTypes = EnumSet.of(NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT); } Collection specificMatches = _tznames.find(text, startIdx, nameTypes); if (specificMatches != null) { MatchInfo specificMatch = null; for (MatchInfo match : specificMatches) { if (startIdx + match.matchLength() > parsedPos) { specificMatch = match; parsedPos = startIdx + match.matchLength(); } } if (specificMatch != null) { timeType.value = getTimeType(specificMatch.nameType()); pos.setIndex(parsedPos); return TimeZone.getTimeZone(getTimeZoneID(specificMatch.tzID(), specificMatch.mzID())); } } if (parseTZDBAbbrev && style == Style.SPECIFIC_SHORT) { assert nameTypes.contains(NameType.SHORT_STANDARD); assert nameTypes.contains(NameType.SHORT_DAYLIGHT); Collection tzdbNameMatches = getTZDBTimeZoneNames().find(text, startIdx, nameTypes); if (tzdbNameMatches != null) { MatchInfo tzdbNameMatch = null; for (MatchInfo match : tzdbNameMatches) { if (startIdx + match.matchLength() > parsedPos) { tzdbNameMatch = match; parsedPos = startIdx + match.matchLength(); } } if (tzdbNameMatch != null) { timeType.value = getTimeType(tzdbNameMatch.nameType()); pos.setIndex(parsedPos); return TimeZone.getTimeZone(getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID())); } } } break; } case GENERIC_LONG: case GENERIC_SHORT: case GENERIC_LOCATION: { EnumSet genericNameTypes = null; switch (style) { case GENERIC_LOCATION: genericNameTypes = EnumSet.of(GenericNameType.LOCATION); break; case GENERIC_LONG: genericNameTypes = EnumSet.of(GenericNameType.LONG, GenericNameType.LOCATION); break; case GENERIC_SHORT: genericNameTypes = EnumSet.of(GenericNameType.SHORT, GenericNameType.LOCATION); break; default: // style cannot be other than above cases assert false; break; } GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, genericNameTypes); if (bestGeneric != null && (startIdx + bestGeneric.matchLength() > parsedPos)) { timeType.value = bestGeneric.timeType(); pos.setIndex(startIdx + bestGeneric.matchLength()); return TimeZone.getTimeZone(bestGeneric.tzID()); } break; } case ZONE_ID: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); String id = parseZoneID(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return TimeZone.getTimeZone(id); } break; } case ZONE_ID_SHORT: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); String id = parseShortZoneID(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return TimeZone.getTimeZone(id); } break; } case EXEMPLAR_LOCATION: { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); String id = parseExemplarLocation(text, tmpPos); if (tmpPos.getErrorIndex() == -1) { pos.setIndex(tmpPos.getIndex()); return TimeZone.getTimeZone(id); } break; } } evaluated |= style.flag; if (parsedPos > startIdx) { // When the specified style is one of SPECIFIC_XXX or GENERIC_XXX, we tried to parse the input // as localized GMT format earlier. If parsedOffset is positive, it means it was successfully // parsed as localized GMT format, but offset digits were not detected (more specifically, GMT // zero format). Then, it tried to find a match within the set of display names, but could not // find a match. At this point, we can safely assume the input text contains the localized // GMT format. assert parsedOffset != UNKNOWN_OFFSET; pos.setIndex(parsedPos); return getTimeZoneForOffset(parsedOffset); } // Failed to parse the input text as the time zone format in the specified style. // Check the longest match among other styles below. String parsedID = null; // stores successfully parsed zone ID for later use TimeType parsedTimeType = TimeType.UNKNOWN; // stores successfully parsed time type for later use assert parsedPos < 0; assert parsedOffset == UNKNOWN_OFFSET; // ISO 8601 if (parsedPos < maxPos && ((evaluated & ISO_Z_STYLE_FLAG) == 0 || (evaluated & ISO_LOCAL_STYLE_FLAG) == 0)) { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); Output hasDigitOffset = new Output<>(false); offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset); if (tmpPos.getErrorIndex() == -1) { if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } // Note: When ISO 8601 format contains offset digits, it should not // collide with other formats. However, ISO 8601 UTC format "Z" (single letter) // may collide with other names. In this case, we need to evaluate other names. if (parsedPos < tmpPos.getIndex()) { parsedOffset = offset; parsedID = null; parsedTimeType = TimeType.UNKNOWN; parsedPos = tmpPos.getIndex(); assert parsedPos == startIdx + 1; // only when "Z" is used } } } // Localized GMT format if (parsedPos < maxPos && (evaluated & Style.LOCALIZED_GMT.flag) == 0) { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); Output hasDigitOffset = new Output<>(false); offset = parseOffsetLocalizedGMT(text, tmpPos, false, hasDigitOffset); if (tmpPos.getErrorIndex() == -1) { if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } // Evaluate other names - see the comment earlier in this method. if (parsedPos < tmpPos.getIndex()) { parsedOffset = offset; parsedID = null; parsedTimeType = TimeType.UNKNOWN; parsedPos = tmpPos.getIndex(); } } } if (parsedPos < maxPos && (evaluated & Style.LOCALIZED_GMT_SHORT.flag) == 0) { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); Output hasDigitOffset = new Output<>(false); offset = parseOffsetLocalizedGMT(text, tmpPos, true, hasDigitOffset); if (tmpPos.getErrorIndex() == -1) { if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { pos.setIndex(tmpPos.getIndex()); return getTimeZoneForOffset(offset); } // Evaluate other names - see the comment earlier in this method. if (parsedPos < tmpPos.getIndex()) { parsedOffset = offset; parsedID = null; parsedTimeType = TimeType.UNKNOWN; parsedPos = tmpPos.getIndex(); } } } // When ParseOption.ALL_STYLES is available, we also try to look all possible display names and IDs. // For example, when style is GENERIC_LONG, "EST" (SPECIFIC_SHORT) is never // used for America/New_York. With parseAllStyles true, this code parses "EST" // as America/New_York. // Note: Adding all possible names into the trie used by the implementation is quite heavy operation, // which we want to avoid normally (note that we cache the trie, so this is applicable to the // first time only as long as the cache does not expire). boolean parseAllStyles = (options == null) ? getDefaultParseOptions().contains(ParseOption.ALL_STYLES) : options.contains(ParseOption.ALL_STYLES); if (parseAllStyles) { // Try all specific names and exemplar location names if (parsedPos < maxPos) { Collection specificMatches = _tznames.find(text, startIdx, ALL_SIMPLE_NAME_TYPES); MatchInfo specificMatch = null; int matchPos = -1; if (specificMatches != null) { for (MatchInfo match : specificMatches) { if (startIdx + match.matchLength() > matchPos) { specificMatch = match; matchPos = startIdx + match.matchLength(); } } } if (parsedPos < matchPos) { parsedPos = matchPos; parsedID = getTimeZoneID(specificMatch.tzID(), specificMatch.mzID()); parsedTimeType = getTimeType(specificMatch.nameType()); parsedOffset = UNKNOWN_OFFSET; } } if (parseTZDBAbbrev && parsedPos < maxPos && (evaluated & Style.SPECIFIC_SHORT.flag) == 0) { Collection tzdbNameMatches = getTZDBTimeZoneNames().find(text, startIdx, ALL_SIMPLE_NAME_TYPES); MatchInfo tzdbNameMatch = null; int matchPos = -1; if (tzdbNameMatches != null) { for (MatchInfo match : tzdbNameMatches) { if (startIdx + match.matchLength() > matchPos) { tzdbNameMatch = match; matchPos = startIdx + match.matchLength(); } } if (parsedPos < matchPos) { parsedPos = matchPos; parsedID = getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID()); parsedTimeType = getTimeType(tzdbNameMatch.nameType()); parsedOffset = UNKNOWN_OFFSET; } } } // Try generic names if (parsedPos < maxPos) { GenericMatchInfo genericMatch = getTimeZoneGenericNames().findBestMatch(text, startIdx, ALL_GENERIC_NAME_TYPES); if (genericMatch != null && parsedPos < startIdx + genericMatch.matchLength()) { parsedPos = startIdx + genericMatch.matchLength(); parsedID = genericMatch.tzID(); parsedTimeType = genericMatch.timeType(); parsedOffset = UNKNOWN_OFFSET; } } // Try time zone ID if (parsedPos < maxPos && (evaluated & Style.ZONE_ID.flag) == 0) { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); String id = parseZoneID(text, tmpPos); if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) { parsedPos = tmpPos.getIndex(); parsedID = id; parsedTimeType = TimeType.UNKNOWN; parsedOffset = UNKNOWN_OFFSET; } } // Try short time zone ID if (parsedPos < maxPos && (evaluated & Style.ZONE_ID_SHORT.flag) == 0) { tmpPos.setIndex(startIdx); tmpPos.setErrorIndex(-1); String id = parseShortZoneID(text, tmpPos); if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) { parsedPos = tmpPos.getIndex(); parsedID = id; parsedTimeType = TimeType.UNKNOWN; parsedOffset = UNKNOWN_OFFSET; } } } if (parsedPos > startIdx) { // Parsed successfully TimeZone parsedTZ = null; if (parsedID != null) { parsedTZ = TimeZone.getTimeZone(parsedID); } else { assert parsedOffset != UNKNOWN_OFFSET; parsedTZ = getTimeZoneForOffset(parsedOffset); } timeType.value = parsedTimeType; pos.setIndex(parsedPos); return parsedTZ; } pos.setErrorIndex(startIdx); return null; } /** * Returns a TimeZone by parsing the time zone string according to * the parse position, the style and the default parse options. *

* Note: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output) * parse(style, text, pos, null, timeType)}. * * @param text the text contains a time zone string at the position. * @param style the format style * @param pos the position. * @param timeType The output argument for receiving the time type (standard/daylight/unknown), * or specify null if the information is not necessary. * @return A TimeZone, or null if the input could not be parsed. * @see Style * @see #parse(Style, String, ParsePosition, EnumSet, Output) * @see #format(Style, TimeZone, long, Output) * @see #setDefaultParseOptions(EnumSet) * @stable ICU 49 */ public TimeZone parse(Style style, String text, ParsePosition pos, Output timeType) { return parse(style, text, pos, null, timeType); } /** * Returns a TimeZone by parsing the time zone string according to * the given parse position. *

* Note: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output) * parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), timeType)}. * * @param text the text contains a time zone string at the position. * @param pos the position. * @return A TimeZone, or null if the input could not be parsed. * @see #parse(Style, String, ParsePosition, EnumSet, Output) * @stable ICU 49 */ public final TimeZone parse(String text, ParsePosition pos) { return parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), null); } /** * Returns a TimeZone for the given text. *

* Note: The behavior of this method is equivalent to {@link #parse(String, ParsePosition)}. * @param text the time zone string * @return A TimeZone. * @throws ParseException when the input could not be parsed as a time zone string. * @see #parse(String, ParsePosition) * @stable ICU 49 */ public final TimeZone parse(String text) throws ParseException { ParsePosition pos = new ParsePosition(0); TimeZone tz = parse(text, pos); if (pos.getErrorIndex() >= 0) { throw new ParseException("Unparseable time zone: \"" + text + "\"" , 0); } assert(tz != null); return tz; } /** * {@inheritDoc} * * @stable ICU 49 */ @Override public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { TimeZone tz = null; long date = System.currentTimeMillis(); if (obj instanceof TimeZone) { tz = (TimeZone)obj; } else if (obj instanceof Calendar) { tz = ((Calendar)obj).getTimeZone(); date = ((Calendar)obj).getTimeInMillis(); } else { throw new IllegalArgumentException("Cannot format given Object (" + obj.getClass().getName() + ") as a time zone"); } assert(tz != null); String result = formatOffsetLocalizedGMT(tz.getOffset(date)); toAppendTo.append(result); if (pos.getFieldAttribute() == DateFormat.Field.TIME_ZONE || pos.getField() == DateFormat.TIMEZONE_FIELD) { pos.setBeginIndex(0); pos.setEndIndex(result.length()); } return toAppendTo; } /** * {@inheritDoc} * * @stable ICU 49 */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object obj) { StringBuffer toAppendTo = new StringBuffer(); FieldPosition pos = new FieldPosition(0); toAppendTo = format(obj, toAppendTo, pos); // supporting only DateFormat.Field.TIME_ZONE AttributedString as = new AttributedString(toAppendTo.toString()); as.addAttribute(DateFormat.Field.TIME_ZONE, DateFormat.Field.TIME_ZONE); return as.getIterator(); } /** * {@inheritDoc} * * @stable ICU 49 */ @Override public Object parseObject(String source, ParsePosition pos) { return parse(source, pos); } /** * Private method used for localized GMT formatting. * @param offset the zone's UTC offset * @param isShort true if the short localized GMT format is desired * @return the localized GMT string */ private String formatOffsetLocalizedGMT(int offset, boolean isShort) { if (offset == 0) { return _gmtZeroFormat; } StringBuilder buf = new StringBuilder(); boolean positive = true; if (offset < 0) { offset = -offset; positive = false; } int offsetH = offset / MILLIS_PER_HOUR; offset = offset % MILLIS_PER_HOUR; int offsetM = offset / MILLIS_PER_MINUTE; offset = offset % MILLIS_PER_MINUTE; int offsetS = offset / MILLIS_PER_SECOND; if (offsetH > MAX_OFFSET_HOUR || offsetM > MAX_OFFSET_MINUTE || offsetS > MAX_OFFSET_SECOND) { throw new IllegalArgumentException("Offset out of range :" + offset); } Object[] offsetPatternItems; if (positive) { if (offsetS != 0) { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HMS.ordinal()]; } else if (offsetM != 0 || !isShort) { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HM.ordinal()]; } else { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_H.ordinal()]; } } else { if (offsetS != 0) { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()]; } else if (offsetM != 0 || !isShort) { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HM.ordinal()]; } else { offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_H.ordinal()]; } } // Building the GMT format string buf.append(_gmtPatternPrefix); for (Object item : offsetPatternItems) { if (item instanceof String) { // pattern literal buf.append((String)item); } else if (item instanceof GMTOffsetField) { // Hour/minute/second field GMTOffsetField field = (GMTOffsetField)item; switch (field.getType()) { case 'H': appendOffsetDigits(buf, offsetH, (isShort ? 1 : 2)); break; case 'm': appendOffsetDigits(buf, offsetM, 2); break; case 's': appendOffsetDigits(buf, offsetS, 2); break; } } } buf.append(_gmtPatternSuffix); return buf.toString(); } /** * Numeric offset field combinations */ private enum OffsetFields { H, HM, HMS } private String formatOffsetISO8601(int offset, boolean isBasic, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { int absOffset = offset < 0 ? -offset : offset; if (useUtcIndicator && (absOffset < MILLIS_PER_SECOND || (ignoreSeconds && absOffset < MILLIS_PER_MINUTE))) { return ISO8601_UTC; } OffsetFields minFields = isShort ? OffsetFields.H : OffsetFields.HM; OffsetFields maxFields = ignoreSeconds ? OffsetFields.HM : OffsetFields.HMS; Character sep = isBasic ? null : ':'; // Note: OffsetFields.HMS as maxFields is an ICU extension. ISO 8601 specification does // not support seconds field. if (absOffset >= MAX_OFFSET) { throw new IllegalArgumentException("Offset out of range :" + offset); } int[] fields = new int[3]; fields[0] = absOffset / MILLIS_PER_HOUR; absOffset = absOffset % MILLIS_PER_HOUR; fields[1] = absOffset / MILLIS_PER_MINUTE; absOffset = absOffset % MILLIS_PER_MINUTE; fields[2] = absOffset / MILLIS_PER_SECOND; assert(fields[0] >= 0 && fields[0] <= MAX_OFFSET_HOUR); assert(fields[1] >= 0 && fields[1] <= MAX_OFFSET_MINUTE); assert(fields[2] >= 0 && fields[2] <= MAX_OFFSET_SECOND); int lastIdx = maxFields.ordinal(); while (lastIdx > minFields.ordinal()) { if (fields[lastIdx] != 0) { break; } lastIdx--; } StringBuilder buf = new StringBuilder(); char sign = '+'; if (offset < 0) { // if all output fields are 0s, do not use negative sign for (int idx = 0; idx <= lastIdx; idx++) { if (fields[idx] != 0) { sign = '-'; break; } } } buf.append(sign); for (int idx = 0; idx <= lastIdx; idx++) { if (sep != null && idx != 0) { buf.append(sep); } if (fields[idx] < 10) { buf.append('0'); } buf.append(fields[idx]); } return buf.toString(); } /** * Private method returning the time zone's specific format string. * * @param tz the time zone * @param stdType the name type used for standard time * @param dstType the name type used for daylight time * @param date the date * @param timeType when null, actual time type is set * @return the time zone's specific format name string */ private String formatSpecific(TimeZone tz, NameType stdType, NameType dstType, long date, Output timeType) { assert(stdType == NameType.LONG_STANDARD || stdType == NameType.SHORT_STANDARD); assert(dstType == NameType.LONG_DAYLIGHT || dstType == NameType.SHORT_DAYLIGHT); boolean isDaylight = tz.inDaylightTime(new Date(date)); String name = isDaylight? getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), dstType, date) : getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), stdType, date); if (name != null && timeType != null) { timeType.value = isDaylight ? TimeType.DAYLIGHT : TimeType.STANDARD; } return name; } /** * Private method returning the time zone's exemplar location string. * This method will never return null. * * @param tz the time zone * @return the time zone's exemplar location name. */ private String formatExemplarLocation(TimeZone tz) { String location = getTimeZoneNames().getExemplarLocationName(ZoneMeta.getCanonicalCLDRID(tz)); if (location == null) { // Use "unknown" location location = getTimeZoneNames().getExemplarLocationName(UNKNOWN_ZONE_ID); if (location == null) { // last resort location = UNKNOWN_LOCATION; } } return location; } /** * Private method returns a time zone ID. If tzID is not null, the value of tzID is returned. * If tzID is null, then this method look up a time zone ID for the current region. This is a * small helper method used by the parse implementation method * * @param tzID * the time zone ID or null * @param mzID * the meta zone ID or null * @return A time zone ID * @throws IllegalArgumentException * when both tzID and mzID are null */ private String getTimeZoneID(String tzID, String mzID) { String id = tzID; if (id == null) { assert (mzID != null); id = _tznames.getReferenceZoneID(mzID, getTargetRegion()); if (id == null) { throw new IllegalArgumentException("Invalid mzID: " + mzID); } } return id; } /** * Private method returning the target region. The target regions is determined by * the locale of this instance. When a generic name is coming from * a meta zone, this region is used for checking if the time zone * is a reference zone of the meta zone. * * @return the target region */ private synchronized String getTargetRegion() { if (_region == null) { _region = _locale.getCountry(); if (_region.length() == 0) { ULocale tmp = ULocale.addLikelySubtags(_locale); _region = tmp.getCountry(); if (_region.length() == 0) { _region = "001"; } } } return _region; } /** * Returns the time type for the given name type * @param nameType the name type * @return the time type (unknown/standard/daylight) */ private TimeType getTimeType(NameType nameType) { switch (nameType) { case LONG_STANDARD: case SHORT_STANDARD: return TimeType.STANDARD; case LONG_DAYLIGHT: case SHORT_DAYLIGHT: return TimeType.DAYLIGHT; default: return TimeType.UNKNOWN; } } /** * Parses the localized GMT pattern string and initialize * localized gmt pattern fields including {@link #_gmtPatternTokens}. * This method must be also called at deserialization time. * * @param gmtPattern the localized GMT pattern string such as "GMT {0}" * @throws IllegalArgumentException when the pattern string does not contain "{0}" */ private void initGMTPattern(String gmtPattern) { // This implementation not perfect, but sufficient practically. int idx = gmtPattern.indexOf("{0}"); if (idx < 0) { throw new IllegalArgumentException("Bad localized GMT pattern: " + gmtPattern); } _gmtPattern = gmtPattern; _gmtPatternPrefix = unquote(gmtPattern.substring(0, idx)); _gmtPatternSuffix = unquote(gmtPattern.substring(idx + 3)); } /** * Unquotes the message format style pattern. * * @param s the pattern * @return the unquoted pattern string */ private static String unquote(String s) { if (s.indexOf('\'') < 0) { return s; } boolean isPrevQuote = false; boolean inQuote = false; StringBuilder buf = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '\'') { if (isPrevQuote) { buf.append(c); isPrevQuote = false; } else { isPrevQuote = true; } inQuote = !inQuote; } else { isPrevQuote = false; buf.append(c); } } return buf.toString(); } /** * Initialize localized GMT format offset hour/min/sec patterns. * This method parses patterns into optimized run-time format. * This method must be called at deserialization time. * * @param gmtOffsetPatterns patterns, String[4] * @throws IllegalArgumentException when patterns are not valid */ private void initGMTOffsetPatterns(String[] gmtOffsetPatterns) { int size = GMTOffsetPatternType.values().length; if (gmtOffsetPatterns.length < size) { throw new IllegalArgumentException("Insufficient number of elements in gmtOffsetPatterns"); } Object[][] gmtOffsetPatternItems = new Object[size][]; for (GMTOffsetPatternType t : GMTOffsetPatternType.values()) { int idx = t.ordinal(); // Note: parseOffsetPattern will validate the given pattern and throws // IllegalArgumentException when pattern is not valid Object[] parsedItems = parseOffsetPattern(gmtOffsetPatterns[idx], t.required()); gmtOffsetPatternItems[idx] = parsedItems; } _gmtOffsetPatterns = new String[size]; System.arraycopy(gmtOffsetPatterns, 0, _gmtOffsetPatterns, 0, size); _gmtOffsetPatternItems = gmtOffsetPatternItems; checkAbuttingHoursAndMinutes(); } private void checkAbuttingHoursAndMinutes() { _abuttingOffsetHoursAndMinutes = false; for (Object[] items : _gmtOffsetPatternItems) { boolean afterH = false; for (Object item : items) { if (item instanceof GMTOffsetField) { GMTOffsetField fld = (GMTOffsetField)item; if (afterH) { _abuttingOffsetHoursAndMinutes = true; } else if (fld.getType() == 'H') { afterH = true; } } else if (afterH) { break; } } } } /** * Used for representing localized GMT time fields in the parsed pattern object. * @see TimeZoneFormat#parseOffsetPattern(String, String) */ private static class GMTOffsetField { final char _type; final int _width; GMTOffsetField(char type, int width) { _type = type; _width = width; } char getType() { return _type; } @SuppressWarnings("unused") int getWidth() { return _width; } static boolean isValid(char type, int width) { return (width == 1 || width == 2); } } /** * Parse the GMT offset pattern into runtime optimized format * * @param pattern the offset pattern string * @param letters the required pattern letters such as "Hm" * @return An array of Object. Each array entry is either String (representing * pattern literal) or GMTOffsetField (hour/min/sec field) */ private static Object[] parseOffsetPattern(String pattern, String letters) { boolean isPrevQuote = false; boolean inQuote = false; StringBuilder text = new StringBuilder(); char itemType = 0; // 0 for string literal, otherwise time pattern character int itemLength = 1; boolean invalidPattern = false; List items = new ArrayList<>(); BitSet checkBits = new BitSet(letters.length()); for (int i = 0; i < pattern.length(); i++) { char ch = pattern.charAt(i); if (ch == '\'') { if (isPrevQuote) { text.append('\''); isPrevQuote = false; } else { isPrevQuote = true; if (itemType != 0) { if (GMTOffsetField.isValid(itemType, itemLength)) { items.add(new GMTOffsetField(itemType, itemLength)); } else { invalidPattern = true; break; } itemType = 0; } } inQuote = !inQuote; } else { isPrevQuote = false; if (inQuote) { text.append(ch); } else { int patFieldIdx = letters.indexOf(ch); if (patFieldIdx >= 0) { // an offset time pattern character if (ch == itemType) { itemLength++; } else { if (itemType == 0) { if (text.length() > 0) { items.add(text.toString()); text.setLength(0); } } else { if (GMTOffsetField.isValid(itemType, itemLength)) { items.add(new GMTOffsetField(itemType, itemLength)); } else { invalidPattern = true; break; } } itemType = ch; itemLength = 1; checkBits.set(patFieldIdx); } } else { // a string literal if (itemType != 0) { if (GMTOffsetField.isValid(itemType, itemLength)) { items.add(new GMTOffsetField(itemType, itemLength)); } else { invalidPattern = true; break; } itemType = 0; } text.append(ch); } } } } // handle last item if (!invalidPattern) { if (itemType == 0) { if (text.length() > 0) { items.add(text.toString()); text.setLength(0); } } else { if (GMTOffsetField.isValid(itemType, itemLength)) { items.add(new GMTOffsetField(itemType, itemLength)); } else { invalidPattern = true; } } } if (invalidPattern || checkBits.cardinality() != letters.length()) { throw new IllegalStateException("Bad localized GMT offset pattern: " + pattern); } return items.toArray(new Object[items.size()]); } /** * Appends seconds field to the offset pattern with hour/minute * * @param offsetHM the offset pattern including hours and minutes fields * @return the offset pattern including hours, minutes and seconds fields */ //TODO This code will be obsoleted once we add hour-minute-second pattern data in CLDR private static String expandOffsetPattern(String offsetHM) { int idx_mm = offsetHM.indexOf("mm"); if (idx_mm < 0) { throw new RuntimeException("Bad time zone hour pattern data"); } String sep = ":"; int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H"); if (idx_H >= 0) { sep = offsetHM.substring(idx_H + 1, idx_mm); } return offsetHM.substring(0, idx_mm + 2) + sep + "ss" + offsetHM.substring(idx_mm + 2); } /** * Truncates minutes field from the offset pattern with hour/minute * * @param offsetHM the offset pattern including hours and minutes fields * @return the offset pattern including only hours field */ //TODO This code will be obsoleted once we add hour pattern data in CLDR private static String truncateOffsetPattern(String offsetHM) { int idx_mm = offsetHM.indexOf("mm"); if (idx_mm < 0) { throw new RuntimeException("Bad time zone hour pattern data"); } int idx_HH = offsetHM.substring(0, idx_mm).lastIndexOf("HH"); if (idx_HH >= 0) { return offsetHM.substring(0, idx_HH + 2); } int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H"); if (idx_H >= 0) { return offsetHM.substring(0, idx_H + 1); } throw new RuntimeException("Bad time zone hour pattern data"); } /** * Appends localized digits to the buffer. *

* Note: This code assumes that the input number is 0 - 59 * * @param buf the target buffer * @param n the integer number * @param minDigits the minimum digits width */ private void appendOffsetDigits(StringBuilder buf, int n, int minDigits) { assert(n >= 0 && n < 60); int numDigits = n >= 10 ? 2 : 1; for (int i = 0; i < minDigits - numDigits; i++) { buf.append(_gmtOffsetDigits[0]); } if (numDigits == 2) { buf.append(_gmtOffsetDigits[n / 10]); } buf.append(_gmtOffsetDigits[n % 10]); } /** * Creates an instance of TimeZone for the given offset * @param offset the offset * @return A TimeZone with the given offset */ private TimeZone getTimeZoneForOffset(int offset) { if (offset == 0) { // when offset is 0, we should use "Etc/GMT" return TimeZone.getTimeZone(TZID_GMT); } return ZoneMeta.getCustomTimeZone(offset); } /** * Returns offset from GMT(UTC) in milliseconds for the given localized GMT * offset format string. When the given string cannot be parsed, this method * sets the current position as the error index to ParsePosition pos * and returns 0. * * @param text the text contains a localized GMT offset string at the position. * @param pos the position. * @param isShort true if this parser to try the short format first * @param hasDigitOffset receiving if the parsed zone string contains offset digits. * @return the offset from GMT(UTC) in milliseconds for the given localized GMT * offset format string. */ private int parseOffsetLocalizedGMT(String text, ParsePosition pos, boolean isShort, Output hasDigitOffset) { int start = pos.getIndex(); int offset = 0; int[] parsedLength = {0}; if (hasDigitOffset != null) { hasDigitOffset.value = false; } offset = parseOffsetLocalizedGMTPattern(text, start, isShort, parsedLength); // For now, parseOffsetLocalizedGMTPattern handles both long and short // formats, no matter isShort is true or false. This might be changed in future // when strict parsing is necessary, or different set of patterns are used for // short/long formats. // if (parsedLength[0] == 0) { // offset = parseOffsetLocalizedGMTPattern(text, start, !isShort, parsedLength); // } if (parsedLength[0] > 0) { if (hasDigitOffset != null) { hasDigitOffset.value = true; } pos.setIndex(start + parsedLength[0]); return offset; } // Try the default patterns offset = parseOffsetDefaultLocalizedGMT(text, start, parsedLength); if (parsedLength[0] > 0) { if (hasDigitOffset != null) { hasDigitOffset.value = true; } pos.setIndex(start + parsedLength[0]); return offset; } // Check if this is a GMT zero format if (text.regionMatches(true, start, _gmtZeroFormat, 0, _gmtZeroFormat.length())) { pos.setIndex(start + _gmtZeroFormat.length()); return 0; } // Check if this is a default GMT zero format for (String defGMTZero : ALT_GMT_STRINGS) { if (text.regionMatches(true, start, defGMTZero, 0, defGMTZero.length())) { pos.setIndex(start + defGMTZero.length()); return 0; } } // Nothing matched pos.setErrorIndex(start); return 0; } /** * Parse localized GMT format generated by the pattern used by this formatter, except * GMT Zero format. * @param text the input text * @param start the start index * @param isShort true if the short localized GMT format is parsed. * @param parsedLen the parsed length, or 0 on failure. * @return the parsed offset in milliseconds. */ private int parseOffsetLocalizedGMTPattern(String text, int start, boolean isShort, int[] parsedLen) { int idx = start; int offset = 0; boolean parsed = false; do { // Prefix part int len = _gmtPatternPrefix.length(); if (len > 0 && !text.regionMatches(true, idx, _gmtPatternPrefix, 0, len)) { // prefix match failed break; } idx += len; // Offset part int[] offsetLen = new int[1]; offset = parseOffsetFields(text, idx, false, offsetLen); if (offsetLen[0] == 0) { // offset field match failed break; } idx += offsetLen[0]; // Suffix part len = _gmtPatternSuffix.length(); if (len > 0 && !text.regionMatches(true, idx, _gmtPatternSuffix, 0, len)) { // no suffix match break; } idx += len; parsed = true; } while (false); parsedLen[0] = parsed ? idx - start : 0; return offset; } /** * Parses localized GMT offset fields into offset. * * @param text the input text * @param start the start index * @param isShort true if this is a short format - currently not used * @param parsedLen the parsed length, or 0 on failure. * @return the parsed offset in milliseconds. */ private int parseOffsetFields(String text, int start, boolean isShort, int[] parsedLen) { int outLen = 0; int offset = 0; int sign = 1; if (parsedLen != null && parsedLen.length >= 1) { parsedLen[0] = 0; } int offsetH, offsetM, offsetS; offsetH = offsetM = offsetS = 0; int[] fields = {0, 0, 0}; for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) { Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()]; assert items != null; outLen = parseOffsetFieldsWithPattern(text, start, items, false, fields); if (outLen > 0) { sign = gmtPatType.isPositive() ? 1 : -1; offsetH = fields[0]; offsetM = fields[1]; offsetS = fields[2]; break; } } if (outLen > 0 && _abuttingOffsetHoursAndMinutes) { // When hours field is abutting minutes field, // the parse result above may not be appropriate. // For example, "01020" is parsed as 01:02 above, // but it should be parsed as 00:10:20. int tmpLen = 0; int tmpSign = 1; for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) { Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()]; assert items != null; // forcing parse to use single hour digit tmpLen = parseOffsetFieldsWithPattern(text, start, items, true, fields); if (tmpLen > 0) { tmpSign = gmtPatType.isPositive() ? 1 : -1; break; } } if (tmpLen > outLen) { // Better parse result with single hour digit outLen = tmpLen; sign = tmpSign; offsetH = fields[0]; offsetM = fields[1]; offsetS = fields[2]; } } if (parsedLen != null && parsedLen.length >= 1) { parsedLen[0] = outLen; } if (outLen > 0) { offset = ((((offsetH * 60) + offsetM) * 60) + offsetS) * 1000 * sign; } return offset; } /** * Parses localized GMT offset fields with the given pattern * * @param text the input text * @param start the start index * @param patternItems the pattern (already itemized) * @param forceSingleHourDigit true if hours field is parsed as a single digit * @param fields receives the parsed hours/minutes/seconds * @return parsed length */ private int parseOffsetFieldsWithPattern(String text, int start, Object[] patternItems, boolean forceSingleHourDigit, int fields[]) { assert (fields != null && fields.length >= 3); fields[0] = fields[1] = fields[2] = 0; boolean failed = false; int offsetH, offsetM, offsetS; offsetH = offsetM = offsetS = 0; int idx = start; int[] tmpParsedLen = {0}; for (int i = 0; i < patternItems.length; i++) { if (patternItems[i] instanceof String) { String patStr = (String)patternItems[i]; int len = patStr.length(); int patIdx = 0; if (i == 0) { // When TimeZoneFormat parse() is called from SimpleDateFormat, // leading space characters might be truncated. If the first pattern text // starts with such character (e.g. Bidi control), then we need to // skip the leading space characters. if (idx < text.length() && !PatternProps.isWhiteSpace(text.codePointAt(idx))) { while (len > 0) { int cp = patStr.codePointAt(patIdx); if (PatternProps.isWhiteSpace(cp)) { int cpLen = Character.charCount(cp); len -= cpLen; patIdx += cpLen; } else { break; } } } } if (!text.regionMatches(true, idx, patStr, patIdx, len)) { failed = true; break; } idx += len; } else { assert(patternItems[i] instanceof GMTOffsetField); GMTOffsetField field = (GMTOffsetField)patternItems[i]; char fieldType = field.getType(); if (fieldType == 'H') { int maxDigits = forceSingleHourDigit ? 1 : 2; offsetH = parseOffsetFieldWithLocalizedDigits(text, idx, 1, maxDigits, 0, MAX_OFFSET_HOUR, tmpParsedLen); } else if (fieldType == 'm') { offsetM = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_MINUTE, tmpParsedLen); } else if (fieldType == 's') { offsetS = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_SECOND, tmpParsedLen); } if (tmpParsedLen[0] == 0) { failed = true; break; } idx += tmpParsedLen[0]; } } if (failed) { return 0; } fields[0] = offsetH; fields[1] = offsetM; fields[2] = offsetS; return idx - start; } /** * Parses the input text using the default format patterns (e.g. "UTC{0}"). * @param text the input text * @param start the start index * @param parsedLen the parsed length, or 0 on failure * @return the parsed offset in milliseconds. */ private int parseOffsetDefaultLocalizedGMT(String text, int start, int[] parsedLen) { int idx = start; int offset = 0; int parsed = 0; do { // check global default GMT alternatives int gmtLen = 0; for (String gmt : ALT_GMT_STRINGS) { int len = gmt.length(); if (text.regionMatches(true, idx, gmt, 0, len)) { gmtLen = len; break; } } if (gmtLen == 0) { break; } idx += gmtLen; // offset needs a sign char and a digit at minimum if (idx + 1 >= text.length()) { break; } // parse sign int sign = 1; char c = text.charAt(idx); if (c == '+') { sign = 1; } else if (c == '-') { sign = -1; } else { break; } idx++; // offset part // try the default pattern with the separator first int[] lenWithSep = {0}; int offsetWithSep = parseDefaultOffsetFields(text, idx, DEFAULT_GMT_OFFSET_SEP, lenWithSep); if (lenWithSep[0] == text.length() - idx) { // maximum match offset = offsetWithSep * sign; idx += lenWithSep[0]; } else { // try abutting field pattern int[] lenAbut = {0}; int offsetAbut = parseAbuttingOffsetFields(text, idx, lenAbut); if (lenWithSep[0] > lenAbut[0]) { offset = offsetWithSep * sign; idx += lenWithSep[0]; } else { offset = offsetAbut * sign; idx += lenAbut[0]; } } parsed = idx - start; } while (false); parsedLen[0] = parsed; return offset; } /** * Parses the input GMT offset fields with the default offset pattern. * @param text the input text * @param start the start index * @param separator the separator character, e.g. ':' * @param parsedLen the parsed length, or 0 on failure. * @return the parsed offset in milliseconds. */ private int parseDefaultOffsetFields(String text, int start, char separator, int[] parsedLen) { int max = text.length(); int idx = start; int[] len = {0}; int hour = 0, min = 0, sec = 0; do { hour = parseOffsetFieldWithLocalizedDigits(text, idx, 1, 2, 0, MAX_OFFSET_HOUR, len); if (len[0] == 0) { break; } idx += len[0]; if (idx + 1 < max && text.charAt(idx) == separator) { min = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_MINUTE, len); if (len[0] == 0) { break; } idx += (1 + len[0]); if (idx + 1 < max && text.charAt(idx) == separator) { sec = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_SECOND, len); if (len[0] == 0) { break; } idx += (1 + len[0]); } } } while (false); if (idx == start) { parsedLen[0] = 0; return 0; } parsedLen[0] = idx - start; return hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND; } /** * Parses abutting localized GMT offset fields (such as 0800) into offset. * @param text the input text * @param start the start index * @param parsedLen the parsed length, or 0 on failure * @return the parsed offset in milliseconds. */ private int parseAbuttingOffsetFields(String text, int start, int[] parsedLen) { final int MAXDIGITS = 6; int[] digits = new int[MAXDIGITS]; int[] parsed = new int[MAXDIGITS]; // accumulative offsets // Parse digits into int[] int idx = start; int[] len = {0}; int numDigits = 0; for (int i = 0; i < MAXDIGITS; i++) { digits[i] = parseSingleLocalizedDigit(text, idx, len); if (digits[i] < 0) { break; } idx += len[0]; parsed[i] = idx - start; numDigits++; } if (numDigits == 0) { parsedLen[0] = 0; return 0; } int offset = 0; while (numDigits > 0) { int hour = 0; int min = 0; int sec = 0; assert(numDigits > 0 && numDigits <= 6); switch (numDigits) { case 1: // H hour = digits[0]; break; case 2: // HH hour = digits[0] * 10 + digits[1]; break; case 3: // Hmm hour = digits[0]; min = digits[1] * 10 + digits[2]; break; case 4: // HHmm hour = digits[0] * 10 + digits[1]; min = digits[2] * 10 + digits[3]; break; case 5: // Hmmss hour = digits[0]; min = digits[1] * 10 + digits[2]; sec = digits[3] * 10 + digits[4]; break; case 6: // HHmmss hour = digits[0] * 10 + digits[1]; min = digits[2] * 10 + digits[3]; sec = digits[4] * 10 + digits[5]; break; } if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) { // found a valid combination offset = hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND; parsedLen[0] = parsed[numDigits - 1]; break; } numDigits--; } return offset; } /** * Reads an offset field value. This method will stop parsing when * 1) number of digits reaches maxDigits * 2) just before already parsed number exceeds maxVal * * @param text the text * @param start the start offset * @param minDigits the minimum number of required digits * @param maxDigits the maximum number of digits * @param minVal the minimum value * @param maxVal the maximum value * @param parsedLen the actual parsed length is set to parsedLen[0], must not be null. * @return the integer value parsed */ private int parseOffsetFieldWithLocalizedDigits(String text, int start, int minDigits, int maxDigits, int minVal, int maxVal, int[] parsedLen) { parsedLen[0] = 0; int decVal = 0; int numDigits = 0; int idx = start; int[] digitLen = {0}; while (idx < text.length() && numDigits < maxDigits) { int digit = parseSingleLocalizedDigit(text, idx, digitLen); if (digit < 0) { break; } int tmpVal = decVal * 10 + digit; if (tmpVal > maxVal) { break; } decVal = tmpVal; numDigits++; idx += digitLen[0]; } // Note: maxVal is checked in the while loop if (numDigits < minDigits || decVal < minVal) { decVal = -1; numDigits = 0; } else { parsedLen[0] = idx - start; } return decVal; } /** * Reads a single decimal digit, either localized digits used by this object * or any Unicode numeric character. * @param text the text * @param start the start index * @param len the actual length read from the text * the start index is not a decimal number. * @return the integer value of the parsed digit, or -1 on failure. */ private int parseSingleLocalizedDigit(String text, int start, int[] len) { int digit = -1; len[0] = 0; if (start < text.length()) { int cp = Character.codePointAt(text, start); // First, try digits configured for this instance for (int i = 0; i < _gmtOffsetDigits.length; i++) { if (cp == _gmtOffsetDigits[i].codePointAt(0)) { digit = i; break; } } // If failed, check if this is a Unicode digit if (digit < 0) { digit = UCharacter.digit(cp); } if (digit >= 0) { len[0] = Character.charCount(cp); } } return digit; } /** * Break input String into String[]. Each array element represents * a code point. This method is used for parsing localized digit * characters and support characters in Unicode supplemental planes. * * @param str the string * @return the array of code points in String[] */ private static String[] toCodePoints(String str) { int len = str.codePointCount(0, str.length()); String[] codePoints = new String[len]; for (int i = 0, offset = 0; i < len; i++) { int code = str.codePointAt(offset); int codeLen = Character.charCount(code); codePoints[i] = str.substring(offset, offset + codeLen); offset += codeLen; } return codePoints; } /** * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601 time zone string * (basic format, extended format, or UTC indicator). When the given string is not an ISO 8601 time * zone string, this method sets the current position as the error index * to ParsePosition pos and returns 0. * * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-08:00", "Z") * at the position. * @param pos the position. * @param extendedOnly true if parsing the text as ISO 8601 extended offset format (e.g. "-08:00"), * or false to evaluate the text as basic format. * @param hasDigitOffset receiving if the parsed zone string contains offset digits. * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style * time zone string. */ private static int parseOffsetISO8601(String text, ParsePosition pos, boolean extendedOnly, Output hasDigitOffset) { if (hasDigitOffset != null) { hasDigitOffset.value = false; } int start = pos.getIndex(); if (start >= text.length()) { pos.setErrorIndex(start); return 0; } char firstChar = text.charAt(start); if (Character.toUpperCase(firstChar) == ISO8601_UTC.charAt(0)) { // "Z" - indicates UTC pos.setIndex(start + 1); return 0; } int sign; if (firstChar == '+') { sign = 1; } else if (firstChar == '-') { sign = -1; } else { // Not an ISO 8601 offset string pos.setErrorIndex(start); return 0; } ParsePosition posOffset = new ParsePosition(start + 1); int offset = parseAsciiOffsetFields(text, posOffset, ':', OffsetFields.H, OffsetFields.HMS); if (posOffset.getErrorIndex() == -1 && !extendedOnly && (posOffset.getIndex() - start <= 3)) { // If the text is successfully parsed as extended format with the options above, it can be also parsed // as basic format. For example, "0230" can be parsed as offset 2:00 (only first digits are valid for // extended format), but it can be parsed as offset 2:30 with basic format. We use longer result. ParsePosition posBasic = new ParsePosition(start + 1); int tmpOffset = parseAbuttingAsciiOffsetFields(text, posBasic, OffsetFields.H, OffsetFields.HMS, false); if (posBasic.getErrorIndex() == -1 && posBasic.getIndex() > posOffset.getIndex()) { offset = tmpOffset; posOffset.setIndex(posBasic.getIndex()); } } if (posOffset.getErrorIndex() != -1) { pos.setErrorIndex(start); return 0; } pos.setIndex(posOffset.getIndex()); if (hasDigitOffset != null) { hasDigitOffset.value = true; } return sign * offset; } /** * Parses offset represented by contiguous ASCII digits *

* Note: This method expects the input position is already at the start of * ASCII digits and does not parse sign (+/-). * * @param text The text contains a sequence of ASCII digits * @param pos The parse position * @param minFields The minimum Fields to be parsed * @param maxFields The maximum Fields to be parsed * @param fixedHourWidth true if hours field must be width of 2 * @return Parsed offset, 0 or positive number. */ private static int parseAbuttingAsciiOffsetFields(String text, ParsePosition pos, OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWidth) { int start = pos.getIndex(); int minDigits = 2 * (minFields.ordinal() + 1) - (fixedHourWidth ? 0 : 1); int maxDigits = 2 * (maxFields.ordinal() + 1); int[] digits = new int[maxDigits]; int numDigits = 0; int idx = start; while (numDigits < digits.length && idx < text.length()) { int digit = ASCII_DIGITS.indexOf(text.charAt(idx)); if (digit < 0) { break; } digits[numDigits] = digit; numDigits++; idx++; } if (fixedHourWidth && ((numDigits & 1) != 0)) { // Fixed digits, so the number of digits must be even number. Truncating. numDigits--; } if (numDigits < minDigits) { pos.setErrorIndex(start); return 0; } int hour = 0, min = 0, sec = 0; boolean bParsed = false; while (numDigits >= minDigits) { switch (numDigits) { case 1: //H hour = digits[0]; break; case 2: //HH hour = digits[0] * 10 + digits[1]; break; case 3: //Hmm hour = digits[0]; min = digits[1] * 10 + digits[2]; break; case 4: //HHmm hour = digits[0] * 10 + digits[1]; min = digits[2] * 10 + digits[3]; break; case 5: //Hmmss hour = digits[0]; min = digits[1] * 10 + digits[2]; sec = digits[3] * 10 + digits[4]; break; case 6: //HHmmss hour = digits[0] * 10 + digits[1]; min = digits[2] * 10 + digits[3]; sec = digits[4] * 10 + digits[5]; break; } if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) { // Successfully parsed bParsed = true; break; } // Truncating numDigits -= (fixedHourWidth ? 2 : 1); hour = min = sec = 0; } if (!bParsed) { pos.setErrorIndex(start); return 0; } pos.setIndex(start + numDigits); return ((((hour * 60) + min) * 60) + sec) * 1000; } /** * Parses offset represented by ASCII digits and separators. *

* Note: This method expects the input position is already at the start of * ASCII digits and does not parse sign (+/-). * * @param text The text * @param pos The parse position * @param sep The separator character * @param minFields The minimum Fields to be parsed * @param maxFields The maximum Fields to be parsed * @return Parsed offset, 0 or positive number. */ private static int parseAsciiOffsetFields(String text, ParsePosition pos, char sep, OffsetFields minFields, OffsetFields maxFields) { int start = pos.getIndex(); int[] fieldVal = {0, 0, 0}; int[] fieldLen = {0, -1, -1}; for (int idx = start, fieldIdx = 0; idx < text.length() && fieldIdx <= maxFields.ordinal(); idx++) { char c = text.charAt(idx); if (c == sep) { if (fieldIdx == 0) { if (fieldLen[0] == 0) { // no hours field break; } // 1 digit hour, move to next field fieldIdx++; } else { if (fieldLen[fieldIdx] != -1) { // premature minutes or seconds field break; } fieldLen[fieldIdx] = 0; } continue; } else if (fieldLen[fieldIdx] == -1) { // no separator after 2 digit field break; } int digit = ASCII_DIGITS.indexOf(c); if (digit < 0) { // not a digit break; } fieldVal[fieldIdx] = fieldVal[fieldIdx] * 10 + digit; fieldLen[fieldIdx]++; if (fieldLen[fieldIdx] >= 2) { // parsed 2 digits, move to next field fieldIdx++; } } int offset = 0; int parsedLen = 0; OffsetFields parsedFields = null; do { // hour if (fieldLen[0] == 0) { break; } if (fieldVal[0] > MAX_OFFSET_HOUR) { offset = (fieldVal[0] / 10) * MILLIS_PER_HOUR; parsedFields = OffsetFields.H; parsedLen = 1; break; } offset = fieldVal[0] * MILLIS_PER_HOUR; parsedLen = fieldLen[0]; parsedFields = OffsetFields.H; // minute if (fieldLen[1] != 2 || fieldVal[1] > MAX_OFFSET_MINUTE) { break; } offset += fieldVal[1] * MILLIS_PER_MINUTE; parsedLen += (1 + fieldLen[1]); parsedFields = OffsetFields.HM; // second if (fieldLen[2] != 2 || fieldVal[2] > MAX_OFFSET_SECOND) { break; } offset += fieldVal[2] * MILLIS_PER_SECOND; parsedLen += (1 + fieldLen[2]); parsedFields = OffsetFields.HMS; } while (false); if (parsedFields == null || parsedFields.ordinal() < minFields.ordinal()) { pos.setErrorIndex(start); return 0; } pos.setIndex(start + parsedLen); return offset; } /** * Parse a zone ID. * @param text the text contains a time zone ID string at the position. * @param pos the position. * @return The zone ID parsed. */ private static String parseZoneID(String text, ParsePosition pos) { String resolvedID = null; if (ZONE_ID_TRIE == null) { synchronized (TimeZoneFormat.class) { if (ZONE_ID_TRIE == null) { // Build zone ID trie TextTrieMap trie = new TextTrieMap<>(true); String[] ids = TimeZone.getAvailableIDs(); for (String id : ids) { trie.put(id, id); } ZONE_ID_TRIE = trie; } } } TextTrieMap.Output trieOutput = new TextTrieMap.Output(); Iterator itr = ZONE_ID_TRIE.get(text, pos.getIndex(), trieOutput); if (itr != null) { resolvedID = itr.next(); pos.setIndex(pos.getIndex() + trieOutput.matchLength); } else { // TODO // We many need to handle rule based custom zone ID (See ZoneMeta.parseCustomID), // such as GM+05:00. However, the public parse method in this class also calls // parseOffsetLocalizedGMT and custom zone IDs are likely supported by the parser, // so we might not need to handle them here. pos.setErrorIndex(pos.getIndex()); } return resolvedID; } /** * Parse a short zone ID. * @param text the text contains a time zone ID string at the position. * @param pos the position. * @return The zone ID for the parsed short zone ID. */ private static String parseShortZoneID(String text, ParsePosition pos) { String resolvedID = null; if (SHORT_ZONE_ID_TRIE == null) { synchronized (TimeZoneFormat.class) { if (SHORT_ZONE_ID_TRIE == null) { // Build short zone ID trie TextTrieMap trie = new TextTrieMap<>(true); Set canonicalIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); for (String id : canonicalIDs) { String shortID = ZoneMeta.getShortID(id); if (shortID != null) { trie.put(shortID, id); } } // Canonical list does not contain Etc/Unknown trie.put(UNKNOWN_SHORT_ZONE_ID, UNKNOWN_ZONE_ID); SHORT_ZONE_ID_TRIE = trie; } } } TextTrieMap.Output trieOutput = new TextTrieMap.Output(); Iterator itr = SHORT_ZONE_ID_TRIE.get(text, pos.getIndex(), trieOutput); if (itr != null) { resolvedID = itr.next(); pos.setIndex(pos.getIndex() + trieOutput.matchLength); } else { pos.setErrorIndex(pos.getIndex()); } return resolvedID; } /** * Parse an exemplar location string. * @param text the text contains an exemplar location string at the position. * @param pos the position. * @return The zone ID for the parsed exemplar location. */ private String parseExemplarLocation(String text, ParsePosition pos) { int startIdx = pos.getIndex(); int parsedPos = -1; String tzID = null; EnumSet nameTypes = EnumSet.of(NameType.EXEMPLAR_LOCATION); Collection exemplarMatches = _tznames.find(text, startIdx, nameTypes); if (exemplarMatches != null) { MatchInfo exemplarMatch = null; for (MatchInfo match : exemplarMatches) { if (startIdx + match.matchLength() > parsedPos) { exemplarMatch = match; parsedPos = startIdx + match.matchLength(); } } if (exemplarMatch != null) { tzID = getTimeZoneID(exemplarMatch.tzID(), exemplarMatch.mzID()); pos.setIndex(parsedPos); } } if (tzID == null) { pos.setErrorIndex(startIdx); } return tzID; } /** * Implements TimeZoneFormat object cache */ private static class TimeZoneFormatCache extends SoftCache { /* (non-Javadoc) * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) */ @Override protected TimeZoneFormat createInstance(ULocale key, ULocale data) { TimeZoneFormat fmt = new TimeZoneFormat(data); fmt.freeze(); return fmt; } } // ---------------------------------- // Serialization stuff //----------------------------------- /** * @serialField _locale ULocale The locale of this TimeZoneFormat object. * @serialField _tznames TimeZoneNames The time zone name data. * @serialField _gmtPattern String The pattern string for localized GMT format. * @serialField _gmtOffsetPatterns String[] The array of GMT offset patterns used by localized GMT format * (positive hour-min, positive hour-min-sec, negative hour-min, negative hour-min-sec). * @serialField _gmtOffsetDigits String[] The array of decimal digits used by localized GMT format * (the size of array is 10). * @serialField _gmtZeroFormat String The localized GMT string used for GMT(UTC). * @serialField _parseAllStyles boolean true if this TimeZoneFormat object is configure * for parsing all available names. */ private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("_locale", ULocale.class), new ObjectStreamField("_tznames", TimeZoneNames.class), new ObjectStreamField("_gmtPattern", String.class), new ObjectStreamField("_gmtOffsetPatterns", String[].class), new ObjectStreamField("_gmtOffsetDigits", String[].class), new ObjectStreamField("_gmtZeroFormat", String.class), new ObjectStreamField("_parseAllStyles", boolean.class), }; /** * * @param oos the object output stream * @throws IOException */ private void writeObject(ObjectOutputStream oos) throws IOException { ObjectOutputStream.PutField fields = oos.putFields(); fields.put("_locale", _locale); fields.put("_tznames", _tznames); fields.put("_gmtPattern", _gmtPattern); fields.put("_gmtOffsetPatterns", _gmtOffsetPatterns); fields.put("_gmtOffsetDigits", _gmtOffsetDigits); fields.put("_gmtZeroFormat", _gmtZeroFormat); fields.put("_parseAllStyles", _parseAllStyles); oos.writeFields(); } /** * * @param ois the object input stream * @throws ClassNotFoundException * @throws IOException */ private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ObjectInputStream.GetField fields = ois.readFields(); _locale = (ULocale)fields.get("_locale", null); if (_locale == null) { throw new InvalidObjectException("Missing field: locale"); } _tznames = (TimeZoneNames)fields.get("_tznames", null); if (_tznames == null) { throw new InvalidObjectException("Missing field: tznames"); } _gmtPattern = (String)fields.get("_gmtPattern", null); if (_gmtPattern == null) { throw new InvalidObjectException("Missing field: gmtPattern"); } String[] tmpGmtOffsetPatterns = (String[])fields.get("_gmtOffsetPatterns", null); if (tmpGmtOffsetPatterns == null) { throw new InvalidObjectException("Missing field: gmtOffsetPatterns"); } else if (tmpGmtOffsetPatterns.length < 4) { throw new InvalidObjectException("Incompatible field: gmtOffsetPatterns"); } _gmtOffsetPatterns = new String[6]; if (tmpGmtOffsetPatterns.length == 4) { for (int i = 0; i < 4; i++) { _gmtOffsetPatterns[i] = tmpGmtOffsetPatterns[i]; } _gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()]); _gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()]); } else { _gmtOffsetPatterns = tmpGmtOffsetPatterns; } _gmtOffsetDigits = (String[])fields.get("_gmtOffsetDigits", null); if (_gmtOffsetDigits == null) { throw new InvalidObjectException("Missing field: gmtOffsetDigits"); } else if (_gmtOffsetDigits.length != 10) { throw new InvalidObjectException("Incompatible field: gmtOffsetDigits"); } _gmtZeroFormat = (String)fields.get("_gmtZeroFormat", null); if (_gmtZeroFormat == null) { throw new InvalidObjectException("Missing field: gmtZeroFormat"); } _parseAllStyles = fields.get("_parseAllStyles", false); if (fields.defaulted("_parseAllStyles")) { throw new InvalidObjectException("Missing field: parseAllStyles"); } // Optimization for TimeZoneNames // // Note: // // com.ibm.icu.impl.TimeZoneNamesImpl is a read-only object initialized // by locale only. But it loads time zone names from resource bundles and // builds trie for parsing. We want to keep TimeZoneNamesImpl as singleton // per locale. We cannot do this for custom TimeZoneNames provided by user. // // com.ibm.icu.impl.TimeZoneGenericNames is a runtime generated object // initialized by ULocale and TimeZoneNames. Like TimeZoneNamesImpl, it // also composes time zone names and trie for parsing. We also want to keep // TimeZoneGenericNames as siongleton per locale. If TimeZoneNames is // actually a TimeZoneNamesImpl, we can reuse cached TimeZoneGenericNames // instance. if (_tznames instanceof TimeZoneNamesImpl) { _tznames = TimeZoneNames.getInstance(_locale); _gnames = null; // will be created by _locale later when necessary } else { // Custom TimeZoneNames implementation is used. We need to create // a new instance of TimeZoneGenericNames here. _gnames = new TimeZoneGenericNames(_locale, _tznames); } // Transient fields requiring initialization initGMTPattern(_gmtPattern); initGMTOffsetPatterns(_gmtOffsetPatterns); } // ---------------------------------- // Freezable stuff //----------------------------------- /** * {@inheritDoc} * @stable ICU 49 */ @Override public boolean isFrozen() { return _frozen; } /** * {@inheritDoc} * @stable ICU 49 */ @Override public TimeZoneFormat freeze() { _frozen = true; return this; } /** * {@inheritDoc} * @stable ICU 49 */ @Override public TimeZoneFormat cloneAsThawed() { TimeZoneFormat copy = (TimeZoneFormat)super.clone(); copy._frozen = false; return copy; } }