com.ibm.icu.text.TimeZoneFormat Maven / Gradle / Ivy
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2011-2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.text;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Set;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SoftCache;
import com.ibm.icu.impl.TZDBTimeZoneNames;
import com.ibm.icu.impl.TextTrieMap;
import com.ibm.icu.impl.TimeZoneGenericNames;
import com.ibm.icu.impl.TimeZoneGenericNames.GenericMatchInfo;
import com.ibm.icu.impl.TimeZoneGenericNames.GenericNameType;
import com.ibm.icu.impl.TimeZoneNamesImpl;
import com.ibm.icu.impl.ZoneMeta;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.TimeZoneNames.MatchInfo;
import com.ibm.icu.text.TimeZoneNames.NameType;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.ULocale;
/**
* TimeZoneFormat
supports time zone display name formatting and parsing.
* An instance of TimeZoneFormat works as a subformatter of {@link SimpleDateFormat},
* but you can also directly get a new instance of TimeZoneFormat
and
* formatting/parsing time zone display names.
*
* ICU implements the time zone display names defined by UTS#35
* Unicode Locale Data Markup Language (LDML). {@link TimeZoneNames} represents the
* time zone display name data model and this class implements the algorithm for actual
* formatting and parsing.
*
* @see SimpleDateFormat
* @see TimeZoneNames
* @stable ICU 49
*/
public class TimeZoneFormat extends UFormat implements Freezable, Serializable {
private static final long serialVersionUID = 2281246852693575022L;
private static final int ISO_Z_STYLE_FLAG = 0x0080;
private static final int ISO_LOCAL_STYLE_FLAG = 0x0100;
/**
* Time zone display format style enum used by format/parse APIs in TimeZoneFormat
.
*
* @see TimeZoneFormat#format(Style, TimeZone, long)
* @see TimeZoneFormat#format(Style, TimeZone, long, Output)
* @see TimeZoneFormat#parse(Style, String, ParsePosition, Output)
* @stable ICU 49
*/
public enum Style {
/**
* Generic location format, such as "United States Time (New York)" and "Italy Time".
* This style is equivalent to the LDML date format pattern "VVVV".
* @stable ICU 49
*/
GENERIC_LOCATION (0x0001),
/**
* Generic long non-location format, such as "Eastern Time".
* This style is equivalent to the LDML date format pattern "vvvv".
* @stable ICU 49
*/
GENERIC_LONG (0x0002),
/**
* Generic short non-location format, such as "ET".
* This style is equivalent to the LDML date format pattern "v".
* @stable ICU 49
*/
GENERIC_SHORT (0x0004),
/**
* Specific long format, such as "Eastern Standard Time".
* This style is equivalent to the LDML date format pattern "zzzz".
* @stable ICU 49
*/
SPECIFIC_LONG (0x0008),
/**
* Specific short format, such as "EST", "PDT".
* This style is equivalent to the LDML date format pattern "z".
* @stable ICU 49
*/
SPECIFIC_SHORT (0x0010),
/**
* Localized GMT offset format, such as "GMT-05:00", "UTC+0100"
* This style is equivalent to the LDML date format pattern "OOOO" and "ZZZZ"
* @stable ICU 49
*/
LOCALIZED_GMT (0x0020),
/**
* Short localized GMT offset format, such as "GMT-5", "UTC+1:30"
* This style is equivalent to the LDML date format pattern "O".
* @stable ICU 51
*/
LOCALIZED_GMT_SHORT (0x0040),
/**
* Short ISO 8601 local time difference (basic format) or the UTC indicator.
* For example, "-05", "+0530", and "Z"(UTC).
* This style is equivalent to the LDML date format pattern "X".
* @stable ICU 51
*/
ISO_BASIC_SHORT (ISO_Z_STYLE_FLAG),
/**
* Short ISO 8601 locale time difference (basic format).
* For example, "-05" and "+0530".
* This style is equivalent to the LDML date format pattern "x".
* @stable ICU 51
*/
ISO_BASIC_LOCAL_SHORT (ISO_LOCAL_STYLE_FLAG),
/**
* Fixed width ISO 8601 local time difference (basic format) or the UTC indicator.
* For example, "-0500", "+0530", and "Z"(UTC).
* This style is equivalent to the LDML date format pattern "XX".
* @stable ICU 51
*/
ISO_BASIC_FIXED (ISO_Z_STYLE_FLAG),
/**
* Fixed width ISO 8601 local time difference (basic format).
* For example, "-0500" and "+0530".
* This style is equivalent to the LDML date format pattern "xx".
* @stable ICU 51
*/
ISO_BASIC_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG),
/**
* ISO 8601 local time difference (basic format) with optional seconds field, or the UTC indicator.
* For example, "-0500", "+052538", and "Z"(UTC).
* This style is equivalent to the LDML date format pattern "XXXX".
* @stable ICU 51
*/
ISO_BASIC_FULL (ISO_Z_STYLE_FLAG),
/**
* ISO 8601 local time difference (basic format) with optional seconds field.
* For example, "-0500" and "+052538".
* This style is equivalent to the LDML date format pattern "xxxx".
* @stable ICU 51
*/
ISO_BASIC_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG),
/**
* Fixed width ISO 8601 local time difference (extended format) or the UTC indicator.
* For example, "-05:00", "+05:30", and "Z"(UTC).
* This style is equivalent to the LDML date format pattern "XXX".
* @stable ICU 51
*/
ISO_EXTENDED_FIXED (ISO_Z_STYLE_FLAG),
/**
* Fixed width ISO 8601 local time difference (extended format).
* For example, "-05:00" and "+05:30".
* This style is equivalent to the LDML date format pattern "xxx" and "ZZZZZ".
* @stable ICU 51
*/
ISO_EXTENDED_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG),
/**
* ISO 8601 local time difference (extended format) with optional seconds field, or the UTC indicator.
* For example, "-05:00", "+05:25:38", and "Z"(UTC).
* This style is equivalent to the LDML date format pattern "XXXXX".
* @stable ICU 51
*/
ISO_EXTENDED_FULL (ISO_Z_STYLE_FLAG),
/**
* ISO 8601 local time difference (extended format) with optional seconds field.
* For example, "-05:00" and "+05:25:38".
* This style is equivalent to the LDML date format pattern "xxxxx".
* @stable ICU 51
*/
ISO_EXTENDED_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG),
/**
* Time Zone ID, such as "America/Los_Angeles".
* @stable ICU 51
*/
ZONE_ID (0x0200),
/**
* Short Time Zone ID (BCP 47 Unicode location extension, time zone type value), such as "uslax".
* @stable ICU 51
*/
ZONE_ID_SHORT (0x0400),
/**
* Exemplar location, such as "Los Angeles" and "Paris".
* @stable ICU 51
*/
EXEMPLAR_LOCATION (0x0800);
final int flag;
private Style(int flag) {
this.flag = flag;
}
}
/**
* Offset pattern type enum.
*
* @see TimeZoneFormat#getGMTOffsetPattern(GMTOffsetPatternType)
* @see TimeZoneFormat#setGMTOffsetPattern(GMTOffsetPatternType, String)
* @stable ICU 49
*/
public enum GMTOffsetPatternType {
/**
* Positive offset with hours and minutes fields
* @stable ICU 49
*/
POSITIVE_HM ("+H:mm", "Hm", true),
/**
* Positive offset with hours, minutes and seconds fields
* @stable ICU 49
*/
POSITIVE_HMS ("+H:mm:ss", "Hms", true),
/**
* Negative offset with hours and minutes fields
* @stable ICU 49
*/
NEGATIVE_HM ("-H:mm", "Hm", false),
/**
* Negative offset with hours, minutes and seconds fields
* @stable ICU 49
*/
NEGATIVE_HMS ("-H:mm:ss", "Hms", false),
/**
* Positive offset with hours field
* @stable ICU 51
*/
POSITIVE_H ("+H", "H", true),
/**
* Negative offset with hours field
* @stable ICU 51
*/
NEGATIVE_H ("-H", "H", false);
private String _defaultPattern;
private String _required;
private boolean _isPositive;
private GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive) {
_defaultPattern = defaultPattern;
_required = required;
_isPositive = isPositive;
}
private String defaultPattern() {
return _defaultPattern;
}
private String required() {
return _required;
}
private boolean isPositive() {
return _isPositive;
}
}
/**
* Time type enum used for receiving time type (standard time, daylight time or unknown)
* in TimeZoneFormat
APIs.
*
* @stable ICU 49
*/
public enum TimeType {
/**
* Unknown
* @stable ICU 49
*/
UNKNOWN,
/**
* Standard time
* @stable ICU 49
*/
STANDARD,
/**
* Daylight saving time
* @stable ICU 49
*/
DAYLIGHT;
}
/**
* Parse option enum, used for specifying optional parse behavior.
* @stable ICU 49
*/
public enum ParseOption {
/**
* When a time zone display name is not found within a set of display names
* used for the specified style, look for the name from display names used
* by other styles.
* @stable ICU 49
*/
ALL_STYLES,
/**
* When parsing a time zone display name in {@link Style#SPECIFIC_SHORT},
* look for the IANA tz database compatible zone abbreviations in addition
* to the localized names coming from the {@link TimeZoneNames} currently
* used by the {@link TimeZoneFormat}.
* @stable ICU 54
*/
TZ_DATABASE_ABBREVIATIONS;
}
/*
* fields to be serialized
*/
private ULocale _locale;
private TimeZoneNames _tznames;
private String _gmtPattern;
private String[] _gmtOffsetPatterns;
private String[] _gmtOffsetDigits;
private String _gmtZeroFormat;
private boolean _parseAllStyles;
private boolean _parseTZDBNames;
/*
* Transient fields
*/
private transient volatile TimeZoneGenericNames _gnames;
private transient String _gmtPatternPrefix;
private transient String _gmtPatternSuffix;
private transient Object[][] _gmtOffsetPatternItems;
// cache if offset hours and minutes are abutting
private transient boolean _abuttingOffsetHoursAndMinutes;
private transient String _region;
private volatile transient boolean _frozen;
private transient volatile TimeZoneNames _tzdbNames;
/*
* Static final fields
*/
private static final String TZID_GMT = "Etc/GMT"; // canonical tzid for GMT
private static final String[] ALT_GMT_STRINGS = {"GMT", "UTC", "UT"};
private static final String DEFAULT_GMT_PATTERN = "GMT{0}";
private static final String DEFAULT_GMT_ZERO = "GMT";
private static final String[] DEFAULT_GMT_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
private static final char DEFAULT_GMT_OFFSET_SEP = ':';
private static final String ASCII_DIGITS = "0123456789";
private static final String ISO8601_UTC = "Z";
private static final String UNKNOWN_ZONE_ID = "Etc/Unknown";
private static final String UNKNOWN_SHORT_ZONE_ID = "unk";
private static final String UNKNOWN_LOCATION = "Unknown";
// Order of GMT offset pattern parsing, *_HMS must be evaluated first
// because *_HM is most likely a substring of *_HMS
private static final GMTOffsetPatternType[] PARSE_GMT_OFFSET_TYPES = {
GMTOffsetPatternType.POSITIVE_HMS, GMTOffsetPatternType.NEGATIVE_HMS,
GMTOffsetPatternType.POSITIVE_HM, GMTOffsetPatternType.NEGATIVE_HM,
GMTOffsetPatternType.POSITIVE_H, GMTOffsetPatternType.NEGATIVE_H,
};
private static final int MILLIS_PER_HOUR = 60 * 60 * 1000;
private static final int MILLIS_PER_MINUTE = 60 * 1000;
private static final int MILLIS_PER_SECOND = 1000;
// Maximum offset (exclusive) in millisecond supported by offset formats
private static final int MAX_OFFSET = 24 * MILLIS_PER_HOUR;
// Maximum values for GMT offset fields
private static final int MAX_OFFSET_HOUR = 23;
private static final int MAX_OFFSET_MINUTE = 59;
private static final int MAX_OFFSET_SECOND = 59;
private static final int UNKNOWN_OFFSET = Integer.MAX_VALUE;
private static TimeZoneFormatCache _tzfCache = new TimeZoneFormatCache();
// The filter used for searching all specific names and exemplar location names
private static final EnumSet ALL_SIMPLE_NAME_TYPES = EnumSet.of(
NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT,
NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT,
NameType.EXEMPLAR_LOCATION
);
// The filter used for searching all generic names
private static final EnumSet ALL_GENERIC_NAME_TYPES = EnumSet.of(
GenericNameType.LOCATION, GenericNameType.LONG, GenericNameType.SHORT
);
private static volatile TextTrieMap ZONE_ID_TRIE;
private static volatile TextTrieMap SHORT_ZONE_ID_TRIE;
/**
* The protected constructor for subclassing.
* @param locale the locale
* @stable ICU 49
*/
protected TimeZoneFormat(ULocale locale) {
_locale = locale;
_tznames = TimeZoneNames.getInstance(locale);
// TimeZoneGenericNames _gnames will be instantiated lazily
String gmtPattern = null;
String hourFormats = null;
_gmtZeroFormat = DEFAULT_GMT_ZERO;
try {
ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance(
ICUData.ICU_ZONE_BASE_NAME, locale);
try {
gmtPattern = bundle.getStringWithFallback("zoneStrings/gmtFormat");
} catch (MissingResourceException e) {
// fall through
}
try {
hourFormats = bundle.getStringWithFallback("zoneStrings/hourFormat");
} catch (MissingResourceException e) {
// fall through
}
try {
_gmtZeroFormat = bundle.getStringWithFallback("zoneStrings/gmtZeroFormat");
} catch (MissingResourceException e) {
// fall through
}
} catch (MissingResourceException e) {
// fall through
}
if (gmtPattern == null) {
gmtPattern = DEFAULT_GMT_PATTERN;
}
initGMTPattern(gmtPattern);
String[] gmtOffsetPatterns = new String[GMTOffsetPatternType.values().length];
if (hourFormats != null) {
String[] hourPatterns = hourFormats.split(";", 2);
gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[0]);
gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()] = hourPatterns[0];
gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[0]);
gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[1]);
gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()] = hourPatterns[1];
gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[1]);
} else {
for (GMTOffsetPatternType patType : GMTOffsetPatternType.values()) {
gmtOffsetPatterns[patType.ordinal()] = patType.defaultPattern();
}
}
initGMTOffsetPatterns(gmtOffsetPatterns);
_gmtOffsetDigits = DEFAULT_GMT_DIGITS;
NumberingSystem ns = NumberingSystem.getInstance(locale);
if (!ns.isAlgorithmic()) {
// we do not support algorithmic numbering system for GMT offset for now
_gmtOffsetDigits = toCodePoints(ns.getDescription());
}
}
/**
* Returns a frozen instance of TimeZoneFormat
for the given locale.
* Note: The instance returned by this method is frozen. If you want to
* customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a
* thawed copy first.
*
* @param locale the locale.
* @return a frozen instance of TimeZoneFormat
for the given locale.
* @stable ICU 49
*/
public static TimeZoneFormat getInstance(ULocale locale) {
if (locale == null) {
throw new NullPointerException("locale is null");
}
return _tzfCache.getInstance(locale, locale);
}
/**
* Returns a frozen instance of TimeZoneFormat
for the given
* {@link java.util.Locale}.
*
Note: The instance returned by this method is frozen. If you want to
* customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a
* thawed copy first.
*
* @param locale the {@link Locale}.
* @return a frozen instance of TimeZoneFormat
for the given locale.
* @stable ICU 54
*/
public static TimeZoneFormat getInstance(Locale locale) {
return getInstance(ULocale.forLocale(locale));
}
/**
* Returns the time zone display name data used by this instance.
*
* @return the time zone display name data.
* @see #setTimeZoneNames(TimeZoneNames)
* @stable ICU 49
*/
public TimeZoneNames getTimeZoneNames() {
return _tznames;
}
/**
* Private method returning the instance of TimeZoneGenericNames
* used by this object. The instance of TimeZoneGenericNames might
* not be available until the first use (lazy instantiation) because
* it is only required for handling generic names (that are not used
* by DateFormat's default patterns) and it requires relatively heavy
* one time initialization.
* @return the instance of TimeZoneGenericNames used by this object.
*/
private TimeZoneGenericNames getTimeZoneGenericNames() {
if (_gnames == null) { // _gnames is volatile
synchronized(this) {
if (_gnames == null) {
_gnames = TimeZoneGenericNames.getInstance(_locale);
}
}
}
return _gnames;
}
/**
* Private method returning the instance of TZDBTimeZoneNames.
* The instance if used only for parsing when {@link ParseOption#TZ_DATABASE_ABBREVIATIONS}
* is enabled.
* @return an instance of TZDBTimeZoneNames.
*/
private TimeZoneNames getTZDBTimeZoneNames() {
if (_tzdbNames == null) {
synchronized(this) {
if (_tzdbNames == null) {
_tzdbNames = new TZDBTimeZoneNames(_locale);
}
}
}
return _tzdbNames;
}
/**
* Sets the time zone display name data to this instance.
*
* @param tznames the time zone display name data.
* @return this object.
* @throws UnsupportedOperationException when this object is frozen.
* @see #getTimeZoneNames()
* @stable ICU 49
*/
public TimeZoneFormat setTimeZoneNames(TimeZoneNames tznames) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
_tznames = tznames;
// TimeZoneGenericNames must be changed to utilize the new TimeZoneNames instance.
_gnames = new TimeZoneGenericNames(_locale, _tznames);
return this;
}
/**
* Returns the localized GMT format pattern.
*
* @return the localized GMT format pattern.
* @see #setGMTPattern(String)
* @stable ICU 49
*/
public String getGMTPattern() {
return _gmtPattern;
}
/**
* Sets the localized GMT format pattern. The pattern must contain
* a single argument {0}, for example "GMT {0}".
*
* @param pattern the localized GMT format pattern string
* @return this object.
* @throws IllegalArgumentException when the pattern string does not contain "{0}"
* @throws UnsupportedOperationException when this object is frozen.
* @see #getGMTPattern()
* @stable ICU 49
*/
public TimeZoneFormat setGMTPattern(String pattern) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
initGMTPattern(pattern);
return this;
}
/**
* Returns the offset pattern used for localized GMT format.
*
* @param type the offset pattern enum
* @see #setGMTOffsetPattern(GMTOffsetPatternType, String)
* @stable ICU 49
*/
public String getGMTOffsetPattern(GMTOffsetPatternType type) {
return _gmtOffsetPatterns[type.ordinal()];
}
/**
* Sets the offset pattern for the given offset type.
*
* @param type the offset pattern.
* @param pattern the pattern string.
* @return this object.
* @throws IllegalArgumentException when the pattern string does not have required time field letters.
* @throws UnsupportedOperationException when this object is frozen.
* @see #getGMTOffsetPattern(GMTOffsetPatternType)
* @stable ICU 49
*/
public TimeZoneFormat setGMTOffsetPattern(GMTOffsetPatternType type, String pattern) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
if (pattern == null) {
throw new NullPointerException("Null GMT offset pattern");
}
Object[] parsedItems = parseOffsetPattern(pattern, type.required());
_gmtOffsetPatterns[type.ordinal()] = pattern;
_gmtOffsetPatternItems[type.ordinal()] = parsedItems;
checkAbuttingHoursAndMinutes();
return this;
}
/**
* Returns the decimal digit characters used for localized GMT format in a single string
* containing from 0 to 9 in the ascending order.
*
* @return the decimal digits for localized GMT format.
* @see #setGMTOffsetDigits(String)
* @stable ICU 49
*/
public String getGMTOffsetDigits() {
StringBuilder buf = new StringBuilder(_gmtOffsetDigits.length);
for (String digit : _gmtOffsetDigits) {
buf.append(digit);
}
return buf.toString();
}
/**
* Sets the decimal digit characters used for localized GMT format.
*
* @param digits a string contains the decimal digit characters from 0 to 9 n the ascending order.
* @return this object.
* @throws IllegalArgumentException when the string did not contain ten characters.
* @throws UnsupportedOperationException when this object is frozen.
* @see #getGMTOffsetDigits()
* @stable ICU 49
*/
public TimeZoneFormat setGMTOffsetDigits(String digits) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
if (digits == null) {
throw new NullPointerException("Null GMT offset digits");
}
String[] digitArray = toCodePoints(digits);
if (digitArray.length != 10) {
throw new IllegalArgumentException("Length of digits must be 10");
}
_gmtOffsetDigits = digitArray;
return this;
}
/**
* Returns the localized GMT format string for GMT(UTC) itself (GMT offset is 0).
*
* @return the localized GMT string string for GMT(UTC) itself.
* @see #setGMTZeroFormat(String)
* @stable ICU 49
*/
public String getGMTZeroFormat() {
return _gmtZeroFormat;
}
/**
* Sets the localized GMT format string for GMT(UTC) itself (GMT offset is 0).
*
* @param gmtZeroFormat the localized GMT format string for GMT(UTC).
* @return this object.
* @throws UnsupportedOperationException when this object is frozen.
* @see #getGMTZeroFormat()
* @stable ICU 49
*/
public TimeZoneFormat setGMTZeroFormat(String gmtZeroFormat) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
if (gmtZeroFormat == null) {
throw new NullPointerException("Null GMT zero format");
}
if (gmtZeroFormat.length() == 0) {
throw new IllegalArgumentException("Empty GMT zero format");
}
_gmtZeroFormat = gmtZeroFormat;
return this;
}
/**
* Sets the default parse options.
*
* Note: By default, an instance of TimeZoneFormat
* created by {@link #getInstance(ULocale)} has no parse options set.
*
* @param options the default parse options.
* @return this object.
* @see ParseOption
* @stable ICU 49
*/
public TimeZoneFormat setDefaultParseOptions(EnumSet options) {
_parseAllStyles = options.contains(ParseOption.ALL_STYLES);
_parseTZDBNames = options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS);
return this;
}
/**
* Returns the default parse options used by this TimeZoneFormat
instance.
* @return the default parse options.
* @see ParseOption
* @stable ICU 49
*/
public EnumSet getDefaultParseOptions() {
if (_parseAllStyles && _parseTZDBNames) {
return EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS);
} else if (_parseAllStyles) {
return EnumSet.of(ParseOption.ALL_STYLES);
} else if (_parseTZDBNames) {
return EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS);
}
return EnumSet.noneOf(ParseOption.class);
}
/**
* Returns the ISO 8601 basic time zone string for the given offset.
* For example, "-08", "-0830" and "Z"
*
* @param offset the offset from GMT(UTC) in milliseconds.
* @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0.
* @param isShort true if shortest form is used.
* @param ignoreSeconds true if non-zero offset seconds is appended.
* @return the ISO 8601 basic format.
* @throws IllegalArgumentException if the specified offset is out of supported range
* (-24 hours < offset < +24 hours).
* @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean)
* @see #parseOffsetISO8601(String, ParsePosition)
* @stable ICU 51
*/
public final String formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
return formatOffsetISO8601(offset, true, useUtcIndicator, isShort, ignoreSeconds);
}
/**
* Returns the ISO 8601 extended time zone string for the given offset.
* For example, "-08:00", "-08:30" and "Z"
*
* @param offset the offset from GMT(UTC) in milliseconds.
* @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0.
* @param isShort true if shortest form is used.
* @param ignoreSeconds true if non-zero offset seconds is appended.
* @return the ISO 8601 extended format.
* @throws IllegalArgumentException if the specified offset is out of supported range
* (-24 hours < offset < +24 hours).
* @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean)
* @see #parseOffsetISO8601(String, ParsePosition)
* @stable ICU 51
*/
public final String formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
return formatOffsetISO8601(offset, false, useUtcIndicator, isShort, ignoreSeconds);
}
/**
* Returns the localized GMT(UTC) offset format for the given offset.
* The localized GMT offset is defined by;
*
* - GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()})
*
- Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)})
*
- Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()})
*
- GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()})
*
* This format always uses 2 digit hours and minutes. When the given offset has non-zero
* seconds, 2 digit seconds field will be appended. For example,
* GMT+05:00 and GMT+05:28:06.
* @param offset the offset from GMT(UTC) in milliseconds.
* @return the localized GMT format string
* @see #parseOffsetLocalizedGMT(String, ParsePosition)
* @throws IllegalArgumentException if the specified offset is out of supported range
* (-24 hours < offset < +24 hours).
* @stable ICU 49
*/
public String formatOffsetLocalizedGMT(int offset) {
return formatOffsetLocalizedGMT(offset, false);
}
/**
* Returns the short localized GMT(UTC) offset format for the given offset.
* The short localized GMT offset is defined by;
*
* - GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()})
*
- Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)})
*
- Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()})
*
- GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()})
*
* This format uses the shortest representation of offset. The hours field does not
* have leading zero and lower fields with zero will be truncated. For example,
* GMT+5 and GMT+530.
* @param offset the offset from GMT(UTC) in milliseconds.
* @return the short localized GMT format string
* @see #parseOffsetLocalizedGMT(String, ParsePosition)
* @throws IllegalArgumentException if the specified offset is out of supported range
* (-24 hours < offset < +24 hours).
* @stable ICU 51
*/
public String formatOffsetShortLocalizedGMT(int offset) {
return formatOffsetLocalizedGMT(offset, true);
}
/**
* Returns the display name of the time zone at the given date for
* the style.
*
* Note: A style may have fallback styles defined. For example,
* when GENERIC_LONG
is requested, but there is no display name
* data available for GENERIC_LONG
style, the implementation
* may use GENERIC_LOCATION
or LOCALIZED_GMT
.
* See UTS#35 UNICODE LOCALE DATA MARKUP LANGUAGE (LDML)
* Appendix J: Time Zone Display Name
* for the details.
*
* @param style the style enum (e.g. GENERIC_LONG
, LOCALIZED_GMT
...)
* @param tz the time zone.
* @param date the date.
* @return the display name of the time zone.
* @see Style
* @see #format(Style, TimeZone, long, Output)
* @stable ICU 49
*/
public final String format(Style style, TimeZone tz, long date) {
return format(style, tz, date, null);
}
/**
* Returns the display name of the time zone at the given date for
* the style. This method takes an extra argument Output<TimeType> timeType
* in addition to the argument list of {@link #format(Style, TimeZone, long)}.
* The argument is used for receiving the time type (standard time
* or daylight saving time, or unknown) actually used for the display name.
*
* @param style the style enum (e.g. GENERIC_LONG
, LOCALIZED_GMT
...)
* @param tz the time zone.
* @param date the date.
* @param timeType the output argument for receiving the time type (standard/daylight/unknown)
* used for the display name, or specify null if the information is not necessary.
* @return the display name of the time zone.
* @see Style
* @see #format(Style, TimeZone, long)
* @stable ICU 49
*/
public String format(Style style, TimeZone tz, long date, Output timeType) {
String result = null;
if (timeType != null) {
timeType.value = TimeType.UNKNOWN;
}
boolean noOffsetFormatFallback = false;
switch (style) {
case GENERIC_LOCATION:
result = getTimeZoneGenericNames().getGenericLocationName(ZoneMeta.getCanonicalCLDRID(tz));
break;
case GENERIC_LONG:
result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.LONG, date);
break;
case GENERIC_SHORT:
result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.SHORT, date);
break;
case SPECIFIC_LONG:
result = formatSpecific(tz, NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, date, timeType);
break;
case SPECIFIC_SHORT:
result = formatSpecific(tz, NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, date, timeType);
break;
case ZONE_ID:
result = tz.getID();
noOffsetFormatFallback = true;
break;
case ZONE_ID_SHORT:
result = ZoneMeta.getShortID(tz);
if (result == null) {
result = UNKNOWN_SHORT_ZONE_ID;
}
noOffsetFormatFallback = true;
break;
case EXEMPLAR_LOCATION:
result = formatExemplarLocation(tz);
noOffsetFormatFallback = true;
break;
default:
// will be handled below
break;
}
if (result == null && !noOffsetFormatFallback) {
int[] offsets = {0, 0};
tz.getOffset(date, false, offsets);
int offset = offsets[0] + offsets[1];
switch (style) {
case GENERIC_LOCATION:
case GENERIC_LONG:
case SPECIFIC_LONG:
case LOCALIZED_GMT:
result = formatOffsetLocalizedGMT(offset);
break;
case GENERIC_SHORT:
case SPECIFIC_SHORT:
case LOCALIZED_GMT_SHORT:
result = formatOffsetShortLocalizedGMT(offset);
break;
case ISO_BASIC_SHORT:
result = formatOffsetISO8601Basic(offset, true, true, true);
break;
case ISO_BASIC_LOCAL_SHORT:
result = formatOffsetISO8601Basic(offset, false, true, true);
break;
case ISO_BASIC_FIXED:
result = formatOffsetISO8601Basic(offset, true, false, true);
break;
case ISO_BASIC_LOCAL_FIXED:
result = formatOffsetISO8601Basic(offset, false, false, true);
break;
case ISO_BASIC_FULL:
result = formatOffsetISO8601Basic(offset, true, false, false);
break;
case ISO_BASIC_LOCAL_FULL:
result = formatOffsetISO8601Basic(offset, false, false, false);
break;
case ISO_EXTENDED_FIXED:
result = formatOffsetISO8601Extended(offset, true, false, true);
break;
case ISO_EXTENDED_LOCAL_FIXED:
result = formatOffsetISO8601Extended(offset, false, false, true);
break;
case ISO_EXTENDED_FULL:
result = formatOffsetISO8601Extended(offset, true, false, false);
break;
case ISO_EXTENDED_LOCAL_FULL:
result = formatOffsetISO8601Extended(offset, false, false, false);
break;
default:
// Other cases are handled earlier and never comes into this
// switch statement.
assert false;
break;
}
// time type
if (timeType != null) {
timeType.value = (offsets[1] != 0) ? TimeType.DAYLIGHT : TimeType.STANDARD;
}
}
assert(result != null);
return result;
}
/**
* Returns offset from GMT(UTC) in milliseconds for the given ISO 8601
* basic or extended time zone string. When the given string is not an ISO 8601 time
* zone string, this method sets the current position as the error index
* to ParsePosition pos
and returns 0.
*
* @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-0800", "-08:00", and "Z")
* at the position.
* @param pos the position.
* @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style
* time zone string.
* @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean)
* @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean)
* @stable ICU 49
*/
public final int parseOffsetISO8601(String text, ParsePosition pos) {
return parseOffsetISO8601(text, pos, false, null);
}
/**
* Returns offset from GMT(UTC) in milliseconds for the given localized GMT
* offset format string. When the given string cannot be parsed, this method
* sets the current position as the error index to ParsePosition pos
* and returns 0.
*
* @param text the text contains a localized GMT offset string at the position.
* @param pos the position.
* @return the offset from GMT(UTC) in milliseconds for the given localized GMT
* offset format string.
* @see #formatOffsetLocalizedGMT(int)
* @stable ICU 49
*/
public int parseOffsetLocalizedGMT(String text, ParsePosition pos) {
return parseOffsetLocalizedGMT(text, pos, false, null);
}
/**
* Returns offset from GMT(UTC) in milliseconds for the given short localized GMT
* offset format string. When the given string cannot be parsed, this method
* sets the current position as the error index to ParsePosition pos
* and returns 0.
*
* @param text the text contains a short localized GMT offset string at the position.
* @param pos the position.
* @return the offset from GMT(UTC) in milliseconds for the given short localized GMT
* offset format string.
* @see #formatOffsetShortLocalizedGMT(int)
* @stable ICU 51
*/
public int parseOffsetShortLocalizedGMT(String text, ParsePosition pos) {
return parseOffsetLocalizedGMT(text, pos, true, null);
}
/**
* Returns a TimeZone
by parsing the time zone string according to
* the parse position, the style and the parse options.
*
* @param text the text contains a time zone string at the position.
* @param style the format style.
* @param pos the position.
* @param options the parse options.
* @param timeType The output argument for receiving the time type (standard/daylight/unknown),
* or specify null if the information is not necessary.
* @return A TimeZone
, or null if the input could not be parsed.
* @see Style
* @see #format(Style, TimeZone, long, Output)
* @stable ICU 49
*/
public TimeZone parse(Style style, String text, ParsePosition pos, EnumSet options, Output timeType) {
if (timeType == null) {
timeType = new Output<>(TimeType.UNKNOWN);
} else {
timeType.value = TimeType.UNKNOWN;
}
int startIdx = pos.getIndex();
int maxPos = text.length();
int offset;
// Styles using localized GMT format as fallback
boolean fallbackLocalizedGMT =
(style == Style.SPECIFIC_LONG || style == Style.GENERIC_LONG || style == Style.GENERIC_LOCATION);
boolean fallbackShortLocalizedGMT =
(style == Style.SPECIFIC_SHORT || style == Style.GENERIC_SHORT);
int evaluated = 0; // bit flags representing already evaluated styles
ParsePosition tmpPos = new ParsePosition(startIdx);
int parsedOffset = UNKNOWN_OFFSET; // stores successfully parsed offset for later use
int parsedPos = -1; // stores successfully parsed offset position for later use
// Try localized GMT format first if necessary
if (fallbackLocalizedGMT || fallbackShortLocalizedGMT) {
Output hasDigitOffset = new Output<>(false);
offset = parseOffsetLocalizedGMT(text, tmpPos, fallbackShortLocalizedGMT, hasDigitOffset);
if (tmpPos.getErrorIndex() == -1) {
// Even when the input text was successfully parsed as a localized GMT format text,
// we may still need to evaluate the specified style if -
// 1) GMT zero format was used, and
// 2) The input text was not completely processed
if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
parsedOffset = offset;
parsedPos = tmpPos.getIndex();
}
// Note: For now, no distinction between long/short localized GMT format in the parser.
// This might be changed in future.
// evaluated |= (fallbackLocalizedGMT ? Style.LOCALIZED_GMT.flag : Style.LOCALIZED_GMT_SHORT.flag);
evaluated |= (Style.LOCALIZED_GMT.flag | Style.LOCALIZED_GMT_SHORT.flag);
}
boolean parseTZDBAbbrev = (options == null) ?
getDefaultParseOptions().contains(ParseOption.TZ_DATABASE_ABBREVIATIONS)
: options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS);
// Try the specified style
switch (style) {
case LOCALIZED_GMT:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
offset = parseOffsetLocalizedGMT(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
// Note: For now, no distinction between long/short localized GMT format in the parser.
// This might be changed in future.
evaluated |= Style.LOCALIZED_GMT_SHORT.flag;
break;
}
case LOCALIZED_GMT_SHORT:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
offset = parseOffsetShortLocalizedGMT(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
// Note: For now, no distinction between long/short localized GMT format in the parser.
// This might be changed in future.
evaluated |= Style.LOCALIZED_GMT.flag;
break;
}
case ISO_BASIC_SHORT:
case ISO_BASIC_FIXED:
case ISO_BASIC_FULL:
case ISO_EXTENDED_FIXED:
case ISO_EXTENDED_FULL:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
offset = parseOffsetISO8601(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
break;
}
case ISO_BASIC_LOCAL_SHORT:
case ISO_BASIC_LOCAL_FIXED:
case ISO_BASIC_LOCAL_FULL:
case ISO_EXTENDED_LOCAL_FIXED:
case ISO_EXTENDED_LOCAL_FULL:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
// Exclude the case of UTC Indicator "Z" here
Output hasDigitOffset = new Output<>(false);
offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset);
if (tmpPos.getErrorIndex() == -1 && hasDigitOffset.value) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
break;
}
case SPECIFIC_LONG:
case SPECIFIC_SHORT:
{
// Specific styles
EnumSet nameTypes = null;
if (style == Style.SPECIFIC_LONG) {
nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT);
} else {
assert style == Style.SPECIFIC_SHORT;
nameTypes = EnumSet.of(NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT);
}
Collection specificMatches = _tznames.find(text, startIdx, nameTypes);
if (specificMatches != null) {
MatchInfo specificMatch = null;
for (MatchInfo match : specificMatches) {
if (startIdx + match.matchLength() > parsedPos) {
specificMatch = match;
parsedPos = startIdx + match.matchLength();
}
}
if (specificMatch != null) {
timeType.value = getTimeType(specificMatch.nameType());
pos.setIndex(parsedPos);
return TimeZone.getTimeZone(getTimeZoneID(specificMatch.tzID(), specificMatch.mzID()));
}
}
if (parseTZDBAbbrev && style == Style.SPECIFIC_SHORT) {
assert nameTypes.contains(NameType.SHORT_STANDARD);
assert nameTypes.contains(NameType.SHORT_DAYLIGHT);
Collection tzdbNameMatches =
getTZDBTimeZoneNames().find(text, startIdx, nameTypes);
if (tzdbNameMatches != null) {
MatchInfo tzdbNameMatch = null;
for (MatchInfo match : tzdbNameMatches) {
if (startIdx + match.matchLength() > parsedPos) {
tzdbNameMatch = match;
parsedPos = startIdx + match.matchLength();
}
}
if (tzdbNameMatch != null) {
timeType.value = getTimeType(tzdbNameMatch.nameType());
pos.setIndex(parsedPos);
return TimeZone.getTimeZone(getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID()));
}
}
}
break;
}
case GENERIC_LONG:
case GENERIC_SHORT:
case GENERIC_LOCATION:
{
EnumSet genericNameTypes = null;
switch (style) {
case GENERIC_LOCATION:
genericNameTypes = EnumSet.of(GenericNameType.LOCATION);
break;
case GENERIC_LONG:
genericNameTypes = EnumSet.of(GenericNameType.LONG, GenericNameType.LOCATION);
break;
case GENERIC_SHORT:
genericNameTypes = EnumSet.of(GenericNameType.SHORT, GenericNameType.LOCATION);
break;
default:
// style cannot be other than above cases
assert false;
break;
}
GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, genericNameTypes);
if (bestGeneric != null && (startIdx + bestGeneric.matchLength() > parsedPos)) {
timeType.value = bestGeneric.timeType();
pos.setIndex(startIdx + bestGeneric.matchLength());
return TimeZone.getTimeZone(bestGeneric.tzID());
}
break;
}
case ZONE_ID:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
String id = parseZoneID(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return TimeZone.getTimeZone(id);
}
break;
}
case ZONE_ID_SHORT:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
String id = parseShortZoneID(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return TimeZone.getTimeZone(id);
}
break;
}
case EXEMPLAR_LOCATION:
{
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
String id = parseExemplarLocation(text, tmpPos);
if (tmpPos.getErrorIndex() == -1) {
pos.setIndex(tmpPos.getIndex());
return TimeZone.getTimeZone(id);
}
break;
}
}
evaluated |= style.flag;
if (parsedPos > startIdx) {
// When the specified style is one of SPECIFIC_XXX or GENERIC_XXX, we tried to parse the input
// as localized GMT format earlier. If parsedOffset is positive, it means it was successfully
// parsed as localized GMT format, but offset digits were not detected (more specifically, GMT
// zero format). Then, it tried to find a match within the set of display names, but could not
// find a match. At this point, we can safely assume the input text contains the localized
// GMT format.
assert parsedOffset != UNKNOWN_OFFSET;
pos.setIndex(parsedPos);
return getTimeZoneForOffset(parsedOffset);
}
// Failed to parse the input text as the time zone format in the specified style.
// Check the longest match among other styles below.
String parsedID = null; // stores successfully parsed zone ID for later use
TimeType parsedTimeType = TimeType.UNKNOWN; // stores successfully parsed time type for later use
assert parsedPos < 0;
assert parsedOffset == UNKNOWN_OFFSET;
// ISO 8601
if (parsedPos < maxPos &&
((evaluated & ISO_Z_STYLE_FLAG) == 0 || (evaluated & ISO_LOCAL_STYLE_FLAG) == 0)) {
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
Output hasDigitOffset = new Output<>(false);
offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset);
if (tmpPos.getErrorIndex() == -1) {
if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
// Note: When ISO 8601 format contains offset digits, it should not
// collide with other formats. However, ISO 8601 UTC format "Z" (single letter)
// may collide with other names. In this case, we need to evaluate other names.
if (parsedPos < tmpPos.getIndex()) {
parsedOffset = offset;
parsedID = null;
parsedTimeType = TimeType.UNKNOWN;
parsedPos = tmpPos.getIndex();
assert parsedPos == startIdx + 1; // only when "Z" is used
}
}
}
// Localized GMT format
if (parsedPos < maxPos &&
(evaluated & Style.LOCALIZED_GMT.flag) == 0) {
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
Output hasDigitOffset = new Output<>(false);
offset = parseOffsetLocalizedGMT(text, tmpPos, false, hasDigitOffset);
if (tmpPos.getErrorIndex() == -1) {
if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
// Evaluate other names - see the comment earlier in this method.
if (parsedPos < tmpPos.getIndex()) {
parsedOffset = offset;
parsedID = null;
parsedTimeType = TimeType.UNKNOWN;
parsedPos = tmpPos.getIndex();
}
}
}
if (parsedPos < maxPos &&
(evaluated & Style.LOCALIZED_GMT_SHORT.flag) == 0) {
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
Output hasDigitOffset = new Output<>(false);
offset = parseOffsetLocalizedGMT(text, tmpPos, true, hasDigitOffset);
if (tmpPos.getErrorIndex() == -1) {
if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) {
pos.setIndex(tmpPos.getIndex());
return getTimeZoneForOffset(offset);
}
// Evaluate other names - see the comment earlier in this method.
if (parsedPos < tmpPos.getIndex()) {
parsedOffset = offset;
parsedID = null;
parsedTimeType = TimeType.UNKNOWN;
parsedPos = tmpPos.getIndex();
}
}
}
// When ParseOption.ALL_STYLES is available, we also try to look all possible display names and IDs.
// For example, when style is GENERIC_LONG, "EST" (SPECIFIC_SHORT) is never
// used for America/New_York. With parseAllStyles true, this code parses "EST"
// as America/New_York.
// Note: Adding all possible names into the trie used by the implementation is quite heavy operation,
// which we want to avoid normally (note that we cache the trie, so this is applicable to the
// first time only as long as the cache does not expire).
boolean parseAllStyles = (options == null) ?
getDefaultParseOptions().contains(ParseOption.ALL_STYLES)
: options.contains(ParseOption.ALL_STYLES);
if (parseAllStyles) {
// Try all specific names and exemplar location names
if (parsedPos < maxPos) {
Collection specificMatches = _tznames.find(text, startIdx, ALL_SIMPLE_NAME_TYPES);
MatchInfo specificMatch = null;
int matchPos = -1;
if (specificMatches != null) {
for (MatchInfo match : specificMatches) {
if (startIdx + match.matchLength() > matchPos) {
specificMatch = match;
matchPos = startIdx + match.matchLength();
}
}
}
if (parsedPos < matchPos) {
parsedPos = matchPos;
parsedID = getTimeZoneID(specificMatch.tzID(), specificMatch.mzID());
parsedTimeType = getTimeType(specificMatch.nameType());
parsedOffset = UNKNOWN_OFFSET;
}
}
if (parseTZDBAbbrev && parsedPos < maxPos && (evaluated & Style.SPECIFIC_SHORT.flag) == 0) {
Collection tzdbNameMatches =
getTZDBTimeZoneNames().find(text, startIdx, ALL_SIMPLE_NAME_TYPES);
MatchInfo tzdbNameMatch = null;
int matchPos = -1;
if (tzdbNameMatches != null) {
for (MatchInfo match : tzdbNameMatches) {
if (startIdx + match.matchLength() > matchPos) {
tzdbNameMatch = match;
matchPos = startIdx + match.matchLength();
}
}
if (parsedPos < matchPos) {
parsedPos = matchPos;
parsedID = getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID());
parsedTimeType = getTimeType(tzdbNameMatch.nameType());
parsedOffset = UNKNOWN_OFFSET;
}
}
}
// Try generic names
if (parsedPos < maxPos) {
GenericMatchInfo genericMatch = getTimeZoneGenericNames().findBestMatch(text, startIdx, ALL_GENERIC_NAME_TYPES);
if (genericMatch != null && parsedPos < startIdx + genericMatch.matchLength()) {
parsedPos = startIdx + genericMatch.matchLength();
parsedID = genericMatch.tzID();
parsedTimeType = genericMatch.timeType();
parsedOffset = UNKNOWN_OFFSET;
}
}
// Try time zone ID
if (parsedPos < maxPos && (evaluated & Style.ZONE_ID.flag) == 0) {
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
String id = parseZoneID(text, tmpPos);
if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) {
parsedPos = tmpPos.getIndex();
parsedID = id;
parsedTimeType = TimeType.UNKNOWN;
parsedOffset = UNKNOWN_OFFSET;
}
}
// Try short time zone ID
if (parsedPos < maxPos && (evaluated & Style.ZONE_ID_SHORT.flag) == 0) {
tmpPos.setIndex(startIdx);
tmpPos.setErrorIndex(-1);
String id = parseShortZoneID(text, tmpPos);
if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) {
parsedPos = tmpPos.getIndex();
parsedID = id;
parsedTimeType = TimeType.UNKNOWN;
parsedOffset = UNKNOWN_OFFSET;
}
}
}
if (parsedPos > startIdx) {
// Parsed successfully
TimeZone parsedTZ = null;
if (parsedID != null) {
parsedTZ = TimeZone.getTimeZone(parsedID);
} else {
assert parsedOffset != UNKNOWN_OFFSET;
parsedTZ = getTimeZoneForOffset(parsedOffset);
}
timeType.value = parsedTimeType;
pos.setIndex(parsedPos);
return parsedTZ;
}
pos.setErrorIndex(startIdx);
return null;
}
/**
* Returns a TimeZone
by parsing the time zone string according to
* the parse position, the style and the default parse options.
*
* Note: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output)
* parse(style, text, pos, null, timeType)}.
*
* @param text the text contains a time zone string at the position.
* @param style the format style
* @param pos the position.
* @param timeType The output argument for receiving the time type (standard/daylight/unknown),
* or specify null if the information is not necessary.
* @return A TimeZone
, or null if the input could not be parsed.
* @see Style
* @see #parse(Style, String, ParsePosition, EnumSet, Output)
* @see #format(Style, TimeZone, long, Output)
* @see #setDefaultParseOptions(EnumSet)
* @stable ICU 49
*/
public TimeZone parse(Style style, String text, ParsePosition pos, Output timeType) {
return parse(style, text, pos, null, timeType);
}
/**
* Returns a TimeZone
by parsing the time zone string according to
* the given parse position.
*
* Note: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output)
* parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), timeType)}.
*
* @param text the text contains a time zone string at the position.
* @param pos the position.
* @return A TimeZone
, or null if the input could not be parsed.
* @see #parse(Style, String, ParsePosition, EnumSet, Output)
* @stable ICU 49
*/
public final TimeZone parse(String text, ParsePosition pos) {
return parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), null);
}
/**
* Returns a TimeZone
for the given text.
*
* Note: The behavior of this method is equivalent to {@link #parse(String, ParsePosition)}.
* @param text the time zone string
* @return A TimeZone
.
* @throws ParseException when the input could not be parsed as a time zone string.
* @see #parse(String, ParsePosition)
* @stable ICU 49
*/
public final TimeZone parse(String text) throws ParseException {
ParsePosition pos = new ParsePosition(0);
TimeZone tz = parse(text, pos);
if (pos.getErrorIndex() >= 0) {
throw new ParseException("Unparseable time zone: \"" + text + "\"" , 0);
}
assert(tz != null);
return tz;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
TimeZone tz = null;
long date = System.currentTimeMillis();
if (obj instanceof TimeZone) {
tz = (TimeZone)obj;
} else if (obj instanceof Calendar) {
tz = ((Calendar)obj).getTimeZone();
date = ((Calendar)obj).getTimeInMillis();
} else {
throw new IllegalArgumentException("Cannot format given Object (" +
obj.getClass().getName() + ") as a time zone");
}
assert(tz != null);
String result = formatOffsetLocalizedGMT(tz.getOffset(date));
toAppendTo.append(result);
if (pos.getFieldAttribute() == DateFormat.Field.TIME_ZONE
|| pos.getField() == DateFormat.TIMEZONE_FIELD) {
pos.setBeginIndex(0);
pos.setEndIndex(result.length());
}
return toAppendTo;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
StringBuffer toAppendTo = new StringBuffer();
FieldPosition pos = new FieldPosition(0);
toAppendTo = format(obj, toAppendTo, pos);
// supporting only DateFormat.Field.TIME_ZONE
AttributedString as = new AttributedString(toAppendTo.toString());
as.addAttribute(DateFormat.Field.TIME_ZONE, DateFormat.Field.TIME_ZONE);
return as.getIterator();
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public Object parseObject(String source, ParsePosition pos) {
return parse(source, pos);
}
/**
* Private method used for localized GMT formatting.
* @param offset the zone's UTC offset
* @param isShort true if the short localized GMT format is desired
* @return the localized GMT string
*/
private String formatOffsetLocalizedGMT(int offset, boolean isShort) {
if (offset == 0) {
return _gmtZeroFormat;
}
StringBuilder buf = new StringBuilder();
boolean positive = true;
if (offset < 0) {
offset = -offset;
positive = false;
}
int offsetH = offset / MILLIS_PER_HOUR;
offset = offset % MILLIS_PER_HOUR;
int offsetM = offset / MILLIS_PER_MINUTE;
offset = offset % MILLIS_PER_MINUTE;
int offsetS = offset / MILLIS_PER_SECOND;
if (offsetH > MAX_OFFSET_HOUR || offsetM > MAX_OFFSET_MINUTE || offsetS > MAX_OFFSET_SECOND) {
throw new IllegalArgumentException("Offset out of range :" + offset);
}
Object[] offsetPatternItems;
if (positive) {
if (offsetS != 0) {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HMS.ordinal()];
} else if (offsetM != 0 || !isShort) {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HM.ordinal()];
} else {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_H.ordinal()];
}
} else {
if (offsetS != 0) {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()];
} else if (offsetM != 0 || !isShort) {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HM.ordinal()];
} else {
offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_H.ordinal()];
}
}
// Building the GMT format string
buf.append(_gmtPatternPrefix);
for (Object item : offsetPatternItems) {
if (item instanceof String) {
// pattern literal
buf.append((String)item);
} else if (item instanceof GMTOffsetField) {
// Hour/minute/second field
GMTOffsetField field = (GMTOffsetField)item;
switch (field.getType()) {
case 'H':
appendOffsetDigits(buf, offsetH, (isShort ? 1 : 2));
break;
case 'm':
appendOffsetDigits(buf, offsetM, 2);
break;
case 's':
appendOffsetDigits(buf, offsetS, 2);
break;
}
}
}
buf.append(_gmtPatternSuffix);
return buf.toString();
}
/**
* Numeric offset field combinations
*/
private enum OffsetFields {
H, HM, HMS
}
private String formatOffsetISO8601(int offset, boolean isBasic, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) {
int absOffset = offset < 0 ? -offset : offset;
if (useUtcIndicator && (absOffset < MILLIS_PER_SECOND || (ignoreSeconds && absOffset < MILLIS_PER_MINUTE))) {
return ISO8601_UTC;
}
OffsetFields minFields = isShort ? OffsetFields.H : OffsetFields.HM;
OffsetFields maxFields = ignoreSeconds ? OffsetFields.HM : OffsetFields.HMS;
Character sep = isBasic ? null : ':';
// Note: OffsetFields.HMS as maxFields is an ICU extension. ISO 8601 specification does
// not support seconds field.
if (absOffset >= MAX_OFFSET) {
throw new IllegalArgumentException("Offset out of range :" + offset);
}
int[] fields = new int[3];
fields[0] = absOffset / MILLIS_PER_HOUR;
absOffset = absOffset % MILLIS_PER_HOUR;
fields[1] = absOffset / MILLIS_PER_MINUTE;
absOffset = absOffset % MILLIS_PER_MINUTE;
fields[2] = absOffset / MILLIS_PER_SECOND;
assert(fields[0] >= 0 && fields[0] <= MAX_OFFSET_HOUR);
assert(fields[1] >= 0 && fields[1] <= MAX_OFFSET_MINUTE);
assert(fields[2] >= 0 && fields[2] <= MAX_OFFSET_SECOND);
int lastIdx = maxFields.ordinal();
while (lastIdx > minFields.ordinal()) {
if (fields[lastIdx] != 0) {
break;
}
lastIdx--;
}
StringBuilder buf = new StringBuilder();
char sign = '+';
if (offset < 0) {
// if all output fields are 0s, do not use negative sign
for (int idx = 0; idx <= lastIdx; idx++) {
if (fields[idx] != 0) {
sign = '-';
break;
}
}
}
buf.append(sign);
for (int idx = 0; idx <= lastIdx; idx++) {
if (sep != null && idx != 0) {
buf.append(sep);
}
if (fields[idx] < 10) {
buf.append('0');
}
buf.append(fields[idx]);
}
return buf.toString();
}
/**
* Private method returning the time zone's specific format string.
*
* @param tz the time zone
* @param stdType the name type used for standard time
* @param dstType the name type used for daylight time
* @param date the date
* @param timeType when null, actual time type is set
* @return the time zone's specific format name string
*/
private String formatSpecific(TimeZone tz, NameType stdType, NameType dstType, long date, Output timeType) {
assert(stdType == NameType.LONG_STANDARD || stdType == NameType.SHORT_STANDARD);
assert(dstType == NameType.LONG_DAYLIGHT || dstType == NameType.SHORT_DAYLIGHT);
boolean isDaylight = tz.inDaylightTime(new Date(date));
String name = isDaylight?
getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), dstType, date) :
getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), stdType, date);
if (name != null && timeType != null) {
timeType.value = isDaylight ? TimeType.DAYLIGHT : TimeType.STANDARD;
}
return name;
}
/**
* Private method returning the time zone's exemplar location string.
* This method will never return null.
*
* @param tz the time zone
* @return the time zone's exemplar location name.
*/
private String formatExemplarLocation(TimeZone tz) {
String location = getTimeZoneNames().getExemplarLocationName(ZoneMeta.getCanonicalCLDRID(tz));
if (location == null) {
// Use "unknown" location
location = getTimeZoneNames().getExemplarLocationName(UNKNOWN_ZONE_ID);
if (location == null) {
// last resort
location = UNKNOWN_LOCATION;
}
}
return location;
}
/**
* Private method returns a time zone ID. If tzID is not null, the value of tzID is returned.
* If tzID is null, then this method look up a time zone ID for the current region. This is a
* small helper method used by the parse implementation method
*
* @param tzID
* the time zone ID or null
* @param mzID
* the meta zone ID or null
* @return A time zone ID
* @throws IllegalArgumentException
* when both tzID and mzID are null
*/
private String getTimeZoneID(String tzID, String mzID) {
String id = tzID;
if (id == null) {
assert (mzID != null);
id = _tznames.getReferenceZoneID(mzID, getTargetRegion());
if (id == null) {
throw new IllegalArgumentException("Invalid mzID: " + mzID);
}
}
return id;
}
/**
* Private method returning the target region. The target regions is determined by
* the locale of this instance. When a generic name is coming from
* a meta zone, this region is used for checking if the time zone
* is a reference zone of the meta zone.
*
* @return the target region
*/
private synchronized String getTargetRegion() {
if (_region == null) {
_region = _locale.getCountry();
if (_region.length() == 0) {
ULocale tmp = ULocale.addLikelySubtags(_locale);
_region = tmp.getCountry();
if (_region.length() == 0) {
_region = "001";
}
}
}
return _region;
}
/**
* Returns the time type for the given name type
* @param nameType the name type
* @return the time type (unknown/standard/daylight)
*/
private TimeType getTimeType(NameType nameType) {
switch (nameType) {
case LONG_STANDARD:
case SHORT_STANDARD:
return TimeType.STANDARD;
case LONG_DAYLIGHT:
case SHORT_DAYLIGHT:
return TimeType.DAYLIGHT;
default:
return TimeType.UNKNOWN;
}
}
/**
* Parses the localized GMT pattern string and initialize
* localized gmt pattern fields including {@link #_gmtPatternTokens}.
* This method must be also called at deserialization time.
*
* @param gmtPattern the localized GMT pattern string such as "GMT {0}"
* @throws IllegalArgumentException when the pattern string does not contain "{0}"
*/
private void initGMTPattern(String gmtPattern) {
// This implementation not perfect, but sufficient practically.
int idx = gmtPattern.indexOf("{0}");
if (idx < 0) {
throw new IllegalArgumentException("Bad localized GMT pattern: " + gmtPattern);
}
_gmtPattern = gmtPattern;
_gmtPatternPrefix = unquote(gmtPattern.substring(0, idx));
_gmtPatternSuffix = unquote(gmtPattern.substring(idx + 3));
}
/**
* Unquotes the message format style pattern.
*
* @param s the pattern
* @return the unquoted pattern string
*/
private static String unquote(String s) {
if (s.indexOf('\'') < 0) {
return s;
}
boolean isPrevQuote = false;
boolean inQuote = false;
StringBuilder buf = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\'') {
if (isPrevQuote) {
buf.append(c);
isPrevQuote = false;
} else {
isPrevQuote = true;
}
inQuote = !inQuote;
} else {
isPrevQuote = false;
buf.append(c);
}
}
return buf.toString();
}
/**
* Initialize localized GMT format offset hour/min/sec patterns.
* This method parses patterns into optimized run-time format.
* This method must be called at deserialization time.
*
* @param gmtOffsetPatterns patterns, String[4]
* @throws IllegalArgumentException when patterns are not valid
*/
private void initGMTOffsetPatterns(String[] gmtOffsetPatterns) {
int size = GMTOffsetPatternType.values().length;
if (gmtOffsetPatterns.length < size) {
throw new IllegalArgumentException("Insufficient number of elements in gmtOffsetPatterns");
}
Object[][] gmtOffsetPatternItems = new Object[size][];
for (GMTOffsetPatternType t : GMTOffsetPatternType.values()) {
int idx = t.ordinal();
// Note: parseOffsetPattern will validate the given pattern and throws
// IllegalArgumentException when pattern is not valid
Object[] parsedItems = parseOffsetPattern(gmtOffsetPatterns[idx], t.required());
gmtOffsetPatternItems[idx] = parsedItems;
}
_gmtOffsetPatterns = new String[size];
System.arraycopy(gmtOffsetPatterns, 0, _gmtOffsetPatterns, 0, size);
_gmtOffsetPatternItems = gmtOffsetPatternItems;
checkAbuttingHoursAndMinutes();
}
private void checkAbuttingHoursAndMinutes() {
_abuttingOffsetHoursAndMinutes = false;
for (Object[] items : _gmtOffsetPatternItems) {
boolean afterH = false;
for (Object item : items) {
if (item instanceof GMTOffsetField) {
GMTOffsetField fld = (GMTOffsetField)item;
if (afterH) {
_abuttingOffsetHoursAndMinutes = true;
} else if (fld.getType() == 'H') {
afterH = true;
}
} else if (afterH) {
break;
}
}
}
}
/**
* Used for representing localized GMT time fields in the parsed pattern object.
* @see TimeZoneFormat#parseOffsetPattern(String, String)
*/
private static class GMTOffsetField {
final char _type;
final int _width;
GMTOffsetField(char type, int width) {
_type = type;
_width = width;
}
char getType() {
return _type;
}
@SuppressWarnings("unused")
int getWidth() {
return _width;
}
static boolean isValid(char type, int width) {
return (width == 1 || width == 2);
}
}
/**
* Parse the GMT offset pattern into runtime optimized format
*
* @param pattern the offset pattern string
* @param letters the required pattern letters such as "Hm"
* @return An array of Object. Each array entry is either String (representing
* pattern literal) or GMTOffsetField (hour/min/sec field)
*/
private static Object[] parseOffsetPattern(String pattern, String letters) {
boolean isPrevQuote = false;
boolean inQuote = false;
StringBuilder text = new StringBuilder();
char itemType = 0; // 0 for string literal, otherwise time pattern character
int itemLength = 1;
boolean invalidPattern = false;
List