diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java index e3562c803f..9c4c55b4be 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java @@ -351,8 +351,7 @@ public class SimpleDateFormat extends DateFormat { // We need to preserve time zone type when parsing specific // time zone text (xxx Standard Time vs xxx Daylight Time) - private static final int TZTYPE_UNK = 0, TZTYPE_STD = 1, TZTYPE_DST = 2; - private transient int tztype = TZTYPE_UNK; + private transient TimeType tztype = TimeType.UNKNOWN; private static final int millisPerHour = 60 * 60 * 1000; @@ -1416,7 +1415,7 @@ public class SimpleDateFormat extends DateFormat { int start = pos; // Reset tztype - tztype = TZTYPE_UNK; + tztype = TimeType.UNKNOWN; boolean[] ambiguousYear = { false }; // item index for the first numeric field within a contiguous numeric run @@ -1606,7 +1605,7 @@ public class SimpleDateFormat extends DateFormat { // front or the back of the default century. This only works because we adjust // the year correctly to start with in other cases -- see subParse(). try { - if (ambiguousYear[0] || tztype != TZTYPE_UNK) { + if (ambiguousYear[0] || tztype != TimeType.UNKNOWN) { // We need a copy of the fields, and we need to avoid triggering a call to // complete(), which will recalculate the fields. Since we can't access // the fields[] array in Calendar, we clone the entire object. This will @@ -1620,7 +1619,7 @@ public class SimpleDateFormat extends DateFormat { cal.set(Calendar.YEAR, getDefaultCenturyStartYear() + 100); } } - if (tztype != TZTYPE_UNK) { + if (tztype != TimeType.UNKNOWN) { copy = (Calendar)cal.clone(); TimeZone tz = copy.getTimeZone(); BasicTimeZone btz = null; @@ -1637,7 +1636,7 @@ public class SimpleDateFormat extends DateFormat { // matches the rule used by the parsed time zone. int[] offsets = new int[2]; if (btz != null) { - if (tztype == TZTYPE_STD) { + if (tztype == TimeType.STANDARD) { btz.getOffsetFromLocal(localMillis, BasicTimeZone.LOCAL_STD, BasicTimeZone.LOCAL_STD, offsets); } else { @@ -1649,8 +1648,8 @@ public class SimpleDateFormat extends DateFormat { // but following code work in most case. tz.getOffset(localMillis, true, offsets); - if (tztype == TZTYPE_STD && offsets[1] != 0 - || tztype == TZTYPE_DST && offsets[1] == 0) { + if (tztype == TimeType.STANDARD && offsets[1] != 0 + || tztype == TimeType.DAYLIGHT && offsets[1] == 0) { // Roll back one day and try it again. // Note: This code assumes 1. timezone transition only happens // once within 24 hours at max @@ -1663,7 +1662,7 @@ public class SimpleDateFormat extends DateFormat { // Now, compare the results with parsed type, either standard or // daylight saving time int resolvedSavings = offsets[1]; - if (tztype == TZTYPE_STD) { + if (tztype == TimeType.STANDARD) { if (offsets[1] != 0) { // Override DST_OFFSET = 0 in the result calendar resolvedSavings = 0; @@ -2212,11 +2211,7 @@ public class SimpleDateFormat extends DateFormat { Style style = (count < 4) ? Style.SPECIFIC_SHORT : Style.SPECIFIC_LONG; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { - if (tzTimeType.value == TimeType.STANDARD) { - tztype = TZTYPE_STD; - } else if (tzTimeType.value == TimeType.DAYLIGHT) { - tztype = TZTYPE_DST; - } + tztype = tzTimeType.value; cal.setTimeZone(tz); return pos.getIndex(); } @@ -2228,11 +2223,7 @@ public class SimpleDateFormat extends DateFormat { Style style = (count < 4) ? Style.RFC822 : ((count == 5) ? Style.ISO8601 : Style.LOCALIZED_GMT); TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { - if (tzTimeType.value == TimeType.STANDARD) { - tztype = TZTYPE_STD; - } else if (tzTimeType.value == TimeType.DAYLIGHT) { - tztype = TZTYPE_DST; - } + tztype = tzTimeType.value; cal.setTimeZone(tz); return pos.getIndex(); } @@ -2245,11 +2236,7 @@ public class SimpleDateFormat extends DateFormat { Style style = (count < 4) ? Style.GENERIC_SHORT : Style.GENERIC_LONG; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { - if (tzTimeType.value == TimeType.STANDARD) { - tztype = TZTYPE_STD; - } else if (tzTimeType.value == TimeType.DAYLIGHT) { - tztype = TZTYPE_DST; - } + tztype = tzTimeType.value; cal.setTimeZone(tz); return pos.getIndex(); } @@ -2262,11 +2249,7 @@ public class SimpleDateFormat extends DateFormat { Style style = (count < 4) ? Style.SPECIFIC_SHORT : Style.GENERIC_LOCATION; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { - if (tzTimeType.value == TimeType.STANDARD) { - tztype = TZTYPE_STD; - } else if (tzTimeType.value == TimeType.DAYLIGHT) { - tztype = TZTYPE_DST; - } + tztype = tzTimeType.value; cal.setTimeZone(tz); return pos.getIndex(); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/TimeZoneFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/TimeZoneFormat.java index a0802c28ad..4b5e4b3efb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/TimeZoneFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/TimeZoneFormat.java @@ -244,7 +244,8 @@ public class TimeZoneFormat extends UFormat implements Freezable */ private transient volatile TimeZoneGenericNames _gnames; - private transient String[] _gmtPatternTokens; + private transient String _gmtPatternPrefix; + private transient String _gmtPatternSuffix; private transient Object[][] _gmtOffsetPatternItems; private transient String _region; @@ -273,15 +274,18 @@ public class TimeZoneFormat extends UFormat implements Freezable GMTOffsetPatternType.POSITIVE_HM, GMTOffsetPatternType.NEGATIVE_HM, }; + 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 MILLIS_PER_HOUR = 60 * 60 * 1000; - private static final int MILLIS_PER_MINUTE = 60 * 1000; - private static final int MILLIS_PER_SECOND = 1000; - private static final int UNKNOWN_OFFSET = Integer.MAX_VALUE; private static TimeZoneFormatCache _tzfCache = new TimeZoneFormatCache(); @@ -621,13 +625,15 @@ public class TimeZoneFormat extends UFormat implements Freezable * @param offset the offset from GMT(UTC) in milliseconds. * @return the RFC822 style GMT(UTC) offset format. * @see #parseOffsetRFC822(String, ParsePosition) + * @throws IllegalArgumentException if the specified offset is out of supported range + * (-24 hours < offset < +24 hours). * @draft ICU 49 * @provisional This API might change or be removed in a future release. */ public final String formatOffsetRFC822(int offset) { // Note: OffsetFields.HMS as maxFields is an ICU extension. RFC822 specification // defines exactly 4 digits for the offset field in HHss format. - return formatOffsetWithASCIIDigits(offset, null, OffsetFields.HM, OffsetFields.HMS); + return formatOffsetWithAsciiDigits(offset, null, OffsetFields.HM, OffsetFields.HMS); } /** @@ -637,6 +643,8 @@ public class TimeZoneFormat extends UFormat implements Freezable * @param offset the offset from GMT(UTC) in milliseconds. * @return the ISO 8601 style GMT(UTC) offset format. * @see #parseOffsetISO8601(String, ParsePosition) + * @throws IllegalArgumentException if the specified offset is out of supported range + * (-24 hours < offset < +24 hours). * @draft ICU 49 * @provisional This API might change or be removed in a future release. */ @@ -646,7 +654,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } // Note: OffsetFields.HMS as maxFields is an ICU extension. ISO 8601 specification does // not support second field. - return formatOffsetWithASCIIDigits(offset, ':', OffsetFields.HM, OffsetFields.HMS); + return formatOffsetWithAsciiDigits(offset, ':', OffsetFields.HM, OffsetFields.HMS); } /** @@ -661,6 +669,8 @@ public class TimeZoneFormat extends UFormat implements Freezable * @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). * @draft ICU 49 * @provisional This API might change or be removed in a future release. */ @@ -698,7 +708,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } // Building the GMT format string - buf.append(_gmtPatternTokens[0]); + buf.append(_gmtPatternPrefix); for (Object item : offsetPatternItems) { if (item instanceof String) { @@ -720,7 +730,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } } } - buf.append(_gmtPatternTokens[1]); + buf.append(_gmtPatternSuffix); return buf.toString(); } @@ -858,7 +868,7 @@ public class TimeZoneFormat extends UFormat implements Freezable // Parse digits pos.setIndex(start + 1); - int offset = parseContiguousAsciiDigitOffset(text, pos, OffsetFields.H, OffsetFields.HMS, false); + int offset = parseAbuttingAsciiOffsetFields(text, pos, OffsetFields.H, OffsetFields.HMS, false); if (pos.getErrorIndex() != -1) { pos.setIndex(start); // reset @@ -906,206 +916,6 @@ public class TimeZoneFormat extends UFormat implements Freezable return parseOffsetLocalizedGMT(text, pos, null); } - /** - * Returns a TimeZone by parsing the time zone string according to - * the specified parse position, the style and the parse options. - *

- * Note:When the input text does not match the specified style, this method - * evaluate the input using the following order and return the longest match. - *

    - *
  1. ISO 8601 style time zone format
  2. - *
  3. RFC822 style time zone format
  4. - *
  5. Localized GMT offset format
  6. - *
  7. Time zone display names available for the given style argument
  8. - *
  9. When {@link ParseOption#ALL_STYLES} is enabled in the parse options, all time zone - * display names other than the style
  10. - *
- * @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 to be used, or null to use the default 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 ParseOption - * @draft ICU 49 - * @provisional This API might change or be removed in a future release. - */ - public TimeZone parseX(Style style, String text, ParsePosition pos, EnumSet options, Output timeType) { - boolean parseAllStyles; - if (options == null) { - parseAllStyles = _parseAllStyles; - } else { - parseAllStyles = options.contains(ParseOption.ALL_STYLES); - } - - if (timeType != null) { - timeType.value = TimeType.UNKNOWN; - } - - int startIdx = pos.getIndex(); - ParsePosition tmpPos = new ParsePosition(startIdx); - - // try RFC822 - int offset = parseOffsetRFC822(text, tmpPos); - if (tmpPos.getErrorIndex() < 0) { - pos.setIndex(tmpPos.getIndex()); - return getTimeZoneForOffset(offset); - } - - // try Localized GMT - int gmtZeroLen = 0; - tmpPos.setErrorIndex(-1); - tmpPos.setIndex(pos.getIndex()); - Output hasDigitOffset = new Output(false); - offset = parseOffsetLocalizedGMT(text, tmpPos, hasDigitOffset); - if (tmpPos.getErrorIndex() < 0) { - if (hasDigitOffset.value || style == Style.LOCALIZED_GMT || style == Style.RFC822 || tmpPos.getIndex() == text.length()) { - // When GMT zero format was detected, we won't try other styles if; - // 1) LOCALIZED_GMT or RFC822 was requested. - // 2) The input text was fully consumed. - // - // Note: Localized GMT format with offset numbers (such as "GMT+03:00") won't collide with other type of names - // practically. But GMT zero formats (localized one + global ones - "GMT", "UTC", "UT") could - for example, - // if a locale has a time zone name like "Utah Time", it should not be detected as GMT ("UT" matches the first - // 2 letters). - pos.setIndex(tmpPos.getIndex()); - return getTimeZoneForOffset(offset); - } else { - // Preserve the length of GMT zero format. - // If no better matches are found later, GMT should be returned. - gmtZeroLen = tmpPos.getIndex() - startIdx; - } - } - - if (!parseAllStyles && (style == Style.RFC822 || style == Style.LOCALIZED_GMT)) { - pos.setErrorIndex(pos.getErrorIndex()); - return null; - } - - // Find the best match within names which are possibly produced by the style - if (style == Style.SPECIFIC_LONG || style == Style.SPECIFIC_SHORT) { - // Specific styles - EnumSet nameTypes = null; - switch (style) { - case SPECIFIC_LONG: - nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT); - break; - case SPECIFIC_SHORT: - nameTypes = EnumSet.of(NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT); - break; - } - Collection specificMatches = _tznames.find(text, startIdx, nameTypes); - if (specificMatches != null) { - int matchLen = 0; - MatchInfo bestSpecific = null; - for (MatchInfo match : specificMatches) { - if (bestSpecific == null || match.matchLength() > matchLen) { - bestSpecific = match; - matchLen = match.matchLength(); - } - } - if (bestSpecific != null) { - if (timeType != null) { - timeType.value = getTimeType(bestSpecific.nameType()); - } - pos.setIndex(startIdx + bestSpecific.matchLength()); - return TimeZone.getTimeZone(getTimeZoneID(bestSpecific.tzID(), bestSpecific.mzID())); - } - } - } else { - // Generic styles - assert(style == Style.GENERIC_LOCATION || style == Style.GENERIC_LONG || style == Style.GENERIC_SHORT); - 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; - } - GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, genericNameTypes); - if (bestGeneric != null) { - if (timeType != null) { - timeType.value = bestGeneric.timeType(); - } - pos.setIndex(startIdx + bestGeneric.matchLength()); - return TimeZone.getTimeZone(bestGeneric.tzID()); - } - } - - // If GMT zero format was detected at the beginning, but there was no better match found - // in names available for the given style, then GMT is returned here. - // This should be done before evaluating other names even parseAllStyles is true, because - // all styles (except RFC822 and LOCALIZED_GMT itself) use LOCALIZED_GMT as the final - // fallback. - if (gmtZeroLen > 0) { - pos.setIndex(startIdx + gmtZeroLen); - return getTimeZoneForOffset(0); - } - - // If no match was found above, check if parseAllStyle is enabled. - // If so, find the longest match in all possible names. - - // 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. - if (parseAllStyles) { - int maxMatchLength = text.length() - startIdx; - - // Try specific names first - Collection specificMatches = _tznames.find(text, startIdx, ALL_SPECIFIC_NAME_TYPES); - MatchInfo bestSpecific = null; - if (specificMatches != null) { - int matchLen = 0; - for (MatchInfo match : specificMatches) { - if (bestSpecific == null || match.matchLength() > matchLen) { - bestSpecific = match; - matchLen = match.matchLength(); - } - } - if (bestSpecific != null && bestSpecific.matchLength() == maxMatchLength) { - // complete match - if (timeType != null) { - timeType.value = getTimeType(bestSpecific.nameType()); - } - pos.setIndex(startIdx + bestSpecific.matchLength()); - return TimeZone.getTimeZone(getTimeZoneID(bestSpecific.tzID(), bestSpecific.mzID())); - } - } - - // Then generic names - GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, ALL_GENERIC_NAME_TYPES); - - if (bestSpecific != null || bestGeneric != null) { - if (bestGeneric == null || - (bestSpecific != null && bestSpecific.matchLength() > bestGeneric.matchLength())) { - // the best specific match - if (timeType != null) { - timeType.value = getTimeType(bestSpecific.nameType()); - } - pos.setIndex(startIdx + bestSpecific.matchLength()); - return TimeZone.getTimeZone(getTimeZoneID(bestSpecific.tzID(), bestSpecific.mzID())); - } else if (bestGeneric != null){ - // the best generic match - if (timeType != null) { - timeType.value = bestGeneric.timeType(); - } - pos.setIndex(startIdx + bestGeneric.matchLength()); - return TimeZone.getTimeZone(bestGeneric.tzID()); - } - } - } - - pos.setErrorIndex(startIdx); - return null; - } - public TimeZone parse(Style style, String text, ParsePosition pos, EnumSet options, Output timeType) { if (timeType == null) { timeType = new Output(TimeType.UNKNOWN); @@ -1189,13 +999,11 @@ public class TimeZoneFormat extends UFormat implements Freezable { // Specific styles EnumSet nameTypes = null; - switch (style) { - case SPECIFIC_LONG: - nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT); - break; - case SPECIFIC_SHORT: + 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); - break; } Collection specificMatches = _tznames.find(text, startIdx, nameTypes); if (specificMatches != null) { @@ -1600,13 +1408,12 @@ public class TimeZoneFormat extends UFormat implements Freezable throw new IllegalArgumentException("Bad localized GMT pattern: " + gmtPattern); } _gmtPattern = gmtPattern; - _gmtPatternTokens = new String[2]; - _gmtPatternTokens[0] = unquote(gmtPattern.substring(0, idx)); - _gmtPatternTokens[1] = unquote(gmtPattern.substring(idx + 3)); + _gmtPatternPrefix = unquote(gmtPattern.substring(0, idx)); + _gmtPatternSuffix = unquote(gmtPattern.substring(idx + 3)); } /** - * Unquotes the message format style pattern + * Unquotes the message format style pattern. * * @param s the pattern * @return the unquoted pattern string @@ -1760,8 +1567,8 @@ public class TimeZoneFormat extends UFormat implements Freezable } itemType = ch; itemLength = 1; + checkBits.set(patFieldIdx); } - checkBits.set(patFieldIdx); } else { // a string literal if (itemType != 0) { @@ -1879,26 +1686,25 @@ public class TimeZoneFormat extends UFormat implements Freezable do { // Prefix part - int len = _gmtPatternTokens[0].length(); - if (len > 0 && !text.regionMatches(true, idx, _gmtPatternTokens[0], 0, len)) { + int len = _gmtPatternPrefix.length(); + if (len > 0 && !text.regionMatches(true, idx, _gmtPatternPrefix, 0, len)) { // prefix match failed break; } idx += len; // Offset part - int[] tmpOffset = new int[1]; - int offsetLen = parseGMTOffset(text, idx, false, tmpOffset); - if (offsetLen == 0) { + int[] offsetLen = new int[1]; + offset = parseOffsetFields(text, idx, false, offsetLen); + if (offsetLen[0] == 0) { // offset field match failed break; } - offset = tmpOffset[0]; - idx += offsetLen; + idx += offsetLen[0]; // Suffix part - len = _gmtPatternTokens[1].length(); - if (len > 0 && !text.regionMatches(true, idx, _gmtPatternTokens[1], 0, len)) { + len = _gmtPatternSuffix.length(); + if (len > 0 && !text.regionMatches(true, idx, _gmtPatternSuffix, 0, len)) { // no suffix match break; } @@ -1910,17 +1716,19 @@ public class TimeZoneFormat extends UFormat implements Freezable if (parsed) { if (hasDigitOffset != null) { hasDigitOffset.value = true; - } pos.setIndex(idx); + } + pos.setIndex(idx); return offset; } // Try the default patterns int[] parsedLength = {0}; - offset = parseDefaultGMT(text, start, parsedLength); + offset = parseOffsetDefaultLocalizedGMT(text, start, parsedLength); if (parsedLength[0] > 0) { if (hasDigitOffset != null) { hasDigitOffset.value = true; - } pos.setIndex(start + parsedLength[0]); + } + pos.setIndex(start + parsedLength[0]); return offset; } @@ -1944,24 +1752,30 @@ public class TimeZoneFormat extends UFormat implements Freezable } /** - * Parses localized GMT string into offset. + * Parses localized GMT offset fields into offset. * * @param text the input text * @param start the start index - * @param minimumHourWidth the minimum hour width, 1 or 2. - * @param offset the result offset set to offset[0] - * @return parsed length + * @param minimumHourWidth true if the parser allows hour field width to be 1 + * @param parsedLen the parsed length, or 0 on failure. + * @return the parsed offset in milliseconds. */ - private int parseGMTOffset(String text, int start, boolean minimumHourWidth, int[] offset) { - int parsedLen = 0; - int[] tmpParsedLen = new int[1]; - offset[0] = 0; + private int parseOffsetFields(String text, int start, boolean minimumHourWidth, int[] parsedLen) { + int outLen = 0; + int[] tmpParsedLen = {0}; + int offset = 0; boolean sawVarHourAndAbuttingField = false; + if (parsedLen != null && parsedLen.length >= 1) { + parsedLen[0] = 0; + } + for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) { int offsetH = 0, offsetM = 0, offsetS = 0; int idx = start; Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()]; + assert items != null; + boolean failed = false; for (int i = 0; i < items.length; i++) { if (items[i] instanceof String) { @@ -1980,15 +1794,15 @@ public class TimeZoneFormat extends UFormat implements Freezable int minDigits = 1; int maxDigits = minimumHourWidth ? 1 : 2; if (!minimumHourWidth && !sawVarHourAndAbuttingField) { - if (i + 1 < items.length && (items[i] instanceof GMTOffsetField)) { + if (i + 1 < items.length && (items[i + 1] instanceof GMTOffsetField)) { sawVarHourAndAbuttingField = true; } } - offsetH = parseOffsetDigits(text, idx, minDigits, maxDigits, 0, MAX_OFFSET_HOUR, tmpParsedLen); + offsetH = parseOffsetFieldWithLocalizedDigits(text, idx, minDigits, maxDigits, 0, MAX_OFFSET_HOUR, tmpParsedLen); } else if (fieldType == 'm') { - offsetM = parseOffsetDigits(text, idx, 2, 2, 0, MAX_OFFSET_MINUTE, tmpParsedLen); + offsetM = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_MINUTE, tmpParsedLen); } else if (fieldType == 's') { - offsetS = parseOffsetDigits(text, idx, 2, 2, 0, MAX_OFFSET_SECOND, tmpParsedLen); + offsetS = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_SECOND, tmpParsedLen); } if (tmpParsedLen[0] == 0) { @@ -2000,13 +1814,13 @@ public class TimeZoneFormat extends UFormat implements Freezable } if (!failed) { int sign = gmtPatType.isPositive() ? 1 : -1; - offset[0] = ((((offsetH * 60) + offsetM) * 60) + offsetS) * 1000 * sign; - parsedLen = idx - start; + offset = ((((offsetH * 60) + offsetM) * 60) + offsetS) * 1000 * sign; + outLen = idx - start; break; } } - if (parsedLen == 0 && sawVarHourAndAbuttingField && !minimumHourWidth) { + if (outLen == 0 && sawVarHourAndAbuttingField && !minimumHourWidth) { // When hour field is variable width and another non-literal pattern // field follows, the parse loop above might eat up the digit from // the abutting field. For example, with pattern "-Hmm" and input "-100", @@ -2019,14 +1833,24 @@ public class TimeZoneFormat extends UFormat implements Freezable // the option is designed for supporting the case like "GMT+5". In this case, // we should get better result for parsing hour digits as much as possible. - return parseGMTOffset(text, start, true, offset); + return parseOffsetFields(text, start, true, parsedLen); } - return parsedLen; + if (parsedLen != null && parsedLen.length >= 1) { + parsedLen[0] = outLen; + } + return offset; } - private int parseDefaultGMT(String text, int start, int[] parsedLength) { - int 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 { @@ -2085,32 +1909,40 @@ public class TimeZoneFormat extends UFormat implements Freezable parsed = idx - start; } while (false); - parsedLength[0] = parsed; + parsedLen[0] = parsed; return offset; } - private int parseDefaultOffsetFields(String text, int start, char separator, int[] parsedLength) { + /** + * 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 = parseOffsetDigits(text, idx, 1, 2, 0, MAX_OFFSET_HOUR, len); + 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 = parseOffsetDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_MINUTE, len); + 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 = parseOffsetDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_SECOND, len); + sec = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_SECOND, len); if (len[0] == 0) { break; } @@ -2120,15 +1952,22 @@ public class TimeZoneFormat extends UFormat implements Freezable } while (false); if (idx == start) { - parsedLength[0] = 0; + parsedLen[0] = 0; return 0; } - parsedLength[0] = idx - start; + parsedLen[0] = idx - start; return hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND; } - private int parseAbuttingOffsetFields(String text, int start, int[] parsedLength) { + /** + * 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 @@ -2138,7 +1977,7 @@ public class TimeZoneFormat extends UFormat implements Freezable int[] len = {0}; int numDigits = 0; for (int i = 0; i < MAXDIGITS; i++) { - digits[i] = parseSingleDigit(text, idx, len); + digits[i] = parseSingleLocalizedDigit(text, idx, len); if (digits[i] < 0) { break; } @@ -2148,7 +1987,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } if (numDigits == 0) { - parsedLength[0] = 0; + parsedLen[0] = 0; return 0; } @@ -2188,7 +2027,7 @@ public class TimeZoneFormat extends UFormat implements Freezable 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; - parsedLength[0] = parsed[numDigits - 1]; + parsedLen[0] = parsed[numDigits - 1]; break; } numDigits--; @@ -2197,7 +2036,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } /** - * Read an offset field number. This method will stop parsing when + * Reads an offset field value. This method will stop parsing when * 1) number of digits reaches maxDigits * 2) just before already parsed number exceeds maxVal * @@ -2207,20 +2046,20 @@ public class TimeZoneFormat extends UFormat implements Freezable * @param maxDigits the maximum number of digits * @param minVal the minimum value * @param maxVal the maximum value - * @param parsedLength the actual parsed length is set to parsedLength[0], must not be null. + * @param parsedLen the actual parsed length is set to parsedLen[0], must not be null. * @return the integer value parsed */ - private int parseOffsetDigits(String text, int start, int minDigits, int maxDigits, - int minVal, int maxVal, int[] parsedLength) { + private int parseOffsetFieldWithLocalizedDigits(String text, int start, int minDigits, int maxDigits, + int minVal, int maxVal, int[] parsedLen) { - parsedLength[0] = 0; + parsedLen[0] = 0; int decVal = 0; int numDigits = 0; int idx = start; int[] digitLen = {0}; while (idx < text.length() && numDigits < maxDigits) { - int digit = parseSingleDigit(text, idx, digitLen); + int digit = parseSingleLocalizedDigit(text, idx, digitLen); if (digit < 0) { break; } @@ -2238,18 +2077,27 @@ public class TimeZoneFormat extends UFormat implements Freezable decVal = -1; numDigits = 0; } else { - parsedLength[0] = idx - start; + parsedLen[0] = idx - start; } return decVal; } - private int parseSingleDigit(String text, int offset, int[] len) { + /** + * 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 (offset < text.length()) { - int cp = Character.codePointAt(text, offset); + 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++) { @@ -2335,13 +2183,13 @@ public class TimeZoneFormat extends UFormat implements Freezable return 0; } ParsePosition posOffset = new ParsePosition(start + 1); - int offset = parseAsciiDigitOffsetWithSeparators(text, posOffset, ':', OffsetFields.H, OffsetFields.HMS, false); - if (posOffset.getErrorIndex() == -1 && !extendedOnly) { + int offset = parseAsciiOffsetFields(text, posOffset, ':', OffsetFields.H, OffsetFields.HMS, false); + 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 = parseContiguousAsciiDigitOffset(text, posBasic, OffsetFields.H, OffsetFields.HMS, false); + int tmpOffset = parseAbuttingAsciiOffsetFields(text, posBasic, OffsetFields.H, OffsetFields.HMS, false); if (posBasic.getErrorIndex() == -1 && posBasic.getIndex() > posOffset.getIndex()) { offset = tmpOffset; posOffset.setIndex(posBasic.getIndex()); @@ -2368,16 +2216,22 @@ public class TimeZoneFormat extends UFormat implements Freezable } /** - * Format offset using ASCII digits + * Formats offset using ASCII digits * @param offset The offset * @param sep The field separator character or null if not required * @param minFields The minimum fields * @param maxFields The maximum fields * @return The offset string + * @throws IllegalArgumentException if the specified offset is out of supported range + * (-24 hours < offset < +24 hours). */ - private static String formatOffsetWithASCIIDigits(int offset, Character sep, OffsetFields minFields, OffsetFields maxFields) { + private static String formatOffsetWithAsciiDigits(int offset, Character sep, OffsetFields minFields, OffsetFields maxFields) { assert maxFields.ordinal() >= minFields.ordinal(); + if (Math.abs(offset) >= MAX_OFFSET) { + throw new IllegalArgumentException("Offset out of range :" + offset); + } + StringBuilder buf = new StringBuilder(); char sign = '+'; if (offset < 0) { @@ -2393,9 +2247,9 @@ public class TimeZoneFormat extends UFormat implements Freezable offset = offset % MILLIS_PER_MINUTE; fields[2] = offset / MILLIS_PER_SECOND; - assert(fields[0] >= 0 && fields[0] < 100); - assert(fields[1] >= 0 && fields[1] < 60); - assert(fields[2] >= 0 && fields[2] < 60); + 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()) { @@ -2418,7 +2272,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } /** - * Parse offset represented by contiguous ASCII digits + * Parses offset represented by contiguous ASCII digits *

* Note: This method expects the input position is already at the start of * ASCII digits and does not parse sign (+/-). @@ -2427,14 +2281,14 @@ public class TimeZoneFormat extends UFormat implements Freezable * @param pos The parse position * @param minFields The minimum Fields to be parsed * @param maxFields The maximum Fields to be parsed - * @param fixedHourWitdh true if hour field must be width of 2 + * @param fixedHourWidth true if hour field must be width of 2 * @return Parsed offset, 0 or positive number. */ - private int parseContiguousAsciiDigitOffset(String text, ParsePosition pos, - OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWitdh) { + private static int parseAbuttingAsciiOffsetFields(String text, ParsePosition pos, + OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWidth) { int start = pos.getIndex(); - int minDigits = 2 * (minFields.ordinal() + 1) - (fixedHourWitdh ? 0 : 1); + int minDigits = 2 * (minFields.ordinal() + 1) - (fixedHourWidth ? 0 : 1); int maxDigits = 2 * (maxFields.ordinal() + 1); int[] digits = new int[maxDigits]; @@ -2450,7 +2304,7 @@ public class TimeZoneFormat extends UFormat implements Freezable idx++; } - if (fixedHourWitdh && (numDigits % 2 != 0)) { + if (fixedHourWidth && ((numDigits & 1) != 0)) { // Fixed digits, so the number of digits must be even number. Truncating. numDigits--; } @@ -2497,7 +2351,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } // Truncating - numDigits -= (fixedHourWitdh ? 2 : 1); + numDigits -= (fixedHourWidth ? 2 : 1); hour = min = sec = 0; } @@ -2510,7 +2364,7 @@ public class TimeZoneFormat extends UFormat implements Freezable } /** - * Parse offset represented by ASCII digits and separators. + * Parses offset represented by ASCII digits and separators. *

* Note: This method expects the input position is already at the start of * ASCII digits and does not parse sign (+/-). @@ -2520,10 +2374,10 @@ public class TimeZoneFormat extends UFormat implements Freezable * @param sep The separator character * @param minFields The minimum Fields to be parsed * @param maxFields The maximum Fields to be parsed - * @param fixedHourWitdh true if hour field must be width of 2 + * @param fixedHourWidth true if hour field must be width of 2 * @return Parsed offset, 0 or positive number. */ - private int parseAsciiDigitOffsetWithSeparators(String text, ParsePosition pos, + private static int parseAsciiOffsetFields(String text, ParsePosition pos, char sep, OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWidth) { int start = pos.getIndex(); int[] fieldVal = {0, 0, 0};