| // © 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; |
| |
| /** |
| * <code>TimeZoneFormat</code> 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 <code>TimeZoneFormat</code> and |
| * formatting/parsing time zone display names. |
| * <p> |
| * ICU implements the time zone display names defined by <a href="http://www.unicode.org/reports/tr35/">UTS#35 |
| * Unicode Locale Data Markup Language (LDML)</a>. {@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<TimeZoneFormat>, 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 <code>TimeZoneFormat</code>. |
| * |
| * @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 <code>TimeZoneFormat</code> 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<NameType> 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<GenericNameType> ALL_GENERIC_NAME_TYPES = EnumSet.of( |
| GenericNameType.LOCATION, GenericNameType.LONG, GenericNameType.SHORT |
| ); |
| |
| private static volatile TextTrieMap<String> ZONE_ID_TRIE; |
| private static volatile TextTrieMap<String> 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 <code>TimeZoneFormat</code> for the given locale. |
| * <p><b>Note</b>: 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 <code>TimeZoneFormat</code> 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 <code>TimeZoneFormat</code> for the given |
| * {@link java.util.Locale}. |
| * <p><b>Note</b>: 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 <code>TimeZoneFormat</code> 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. |
| * <p> |
| * <b>Note:</b> By default, an instance of <code>TimeZoneFormat</code> |
| * 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<ParseOption> options) { |
| _parseAllStyles = options.contains(ParseOption.ALL_STYLES); |
| _parseTZDBNames = options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS); |
| return this; |
| } |
| |
| /** |
| * Returns the default parse options used by this <code>TimeZoneFormat</code> instance. |
| * @return the default parse options. |
| * @see ParseOption |
| * @stable ICU 49 |
| */ |
| public EnumSet<ParseOption> 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; |
| * <ul> |
| * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) |
| * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) |
| * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) |
| * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) |
| * </ul> |
| * 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; |
| * <ul> |
| * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) |
| * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) |
| * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) |
| * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) |
| * </ul> |
| * 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. |
| * |
| * <p><b>Note</b>: A style may have fallback styles defined. For example, |
| * when <code>GENERIC_LONG</code> is requested, but there is no display name |
| * data available for <code>GENERIC_LONG</code> style, the implementation |
| * may use <code>GENERIC_LOCATION</code> or <code>LOCALIZED_GMT</code>. |
| * See UTS#35 UNICODE LOCALE DATA MARKUP LANGUAGE (LDML) |
| * <a href="http://www.unicode.org/reports/tr35/#Time_Zone_Fallback">Appendix J: Time Zone Display Name</a> |
| * for the details. |
| * |
| * @param style the style enum (e.g. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...) |
| * @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 <code>Output<TimeType> timeType</code> |
| * 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. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...) |
| * @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> 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 <code>ParsePosition pos</code> 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 <code>ParsePosition pos</code> |
| * 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 <code>ParsePosition pos</code> |
| * 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 <code>TimeZone</code> 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 <code>TimeZone</code>, 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<ParseOption> options, Output<TimeType> timeType) { |
| if (timeType == null) { |
| timeType = new Output<TimeType>(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<Boolean> hasDigitOffset = new Output<Boolean>(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<Boolean> hasDigitOffset = new Output<Boolean>(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<NameType> 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<MatchInfo> 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<MatchInfo> 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<GenericNameType> 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<Boolean> hasDigitOffset = new Output<Boolean>(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<Boolean> hasDigitOffset = new Output<Boolean>(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<Boolean> hasDigitOffset = new Output<Boolean>(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<MatchInfo> 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<MatchInfo> 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 <code>TimeZone</code> by parsing the time zone string according to |
| * the parse position, the style and the default parse options. |
| * <p> |
| * <b>Note</b>: 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 <code>TimeZone</code>, 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> timeType) { |
| return parse(style, text, pos, null, timeType); |
| } |
| |
| /** |
| * Returns a <code>TimeZone</code> by parsing the time zone string according to |
| * the given parse position. |
| * <p> |
| * <b>Note</b>: 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 <code>TimeZone</code>, 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 <code>TimeZone</code> for the given text. |
| * <p> |
| * <b>Note</b>: The behavior of this method is equivalent to {@link #parse(String, ParsePosition)}. |
| * @param text the time zone string |
| * @return A <code>TimeZone</code>. |
| * @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> 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<Object> items = new ArrayList<Object>(); |
| 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. |
| * <p> |
| * 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 <code>ParsePosition pos</code> |
| * 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<Boolean> 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 <code>maxDigits</code> |
| * 2) just before already parsed number exceeds <code>maxVal</code> |
| * |
| * @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 <code>ParsePosition pos</code> 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 <code>true</code> if parsing the text as ISO 8601 extended offset format (e.g. "-08:00"), |
| * or <code>false</code> 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<Boolean> 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 |
| * <p> |
| * 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. |
| * <p> |
| * 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<String> trie = new TextTrieMap<String>(true); |
| String[] ids = TimeZone.getAvailableIDs(); |
| for (String id : ids) { |
| trie.put(id, id); |
| } |
| ZONE_ID_TRIE = trie; |
| } |
| } |
| } |
| |
| TextTrieMap.Output trieOutput = new TextTrieMap.Output(); |
| Iterator<String> 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<String> trie = new TextTrieMap<String>(true); |
| Set<String> 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<String> 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<NameType> nameTypes = EnumSet.of(NameType.EXEMPLAR_LOCATION); |
| Collection<MatchInfo> 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 <code>TimeZoneFormat</code> object cache |
| */ |
| private static class TimeZoneFormatCache extends SoftCache<ULocale, TimeZoneFormat, ULocale> { |
| |
| /* (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 <code>true</code> 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; |
| } |
| } |
| |