/*
* Copyright (C) 2014 The Android Open Source Project
* Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* (C) Copyright Taligent, Inc. 1996 - All Rights Reserved
* (C) Copyright IBM Corp. 1996-1998 - All Rights Reserved
*
* The original version of this source code and documentation is copyrighted
* and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These
* materials are provided under terms of a License Agreement between Taligent
* and Sun. This technology is protected by multiple US and International
* patents. This notice and attribution to Taligent may not be removed.
* Taligent is a registered trademark of Taligent, Inc.
*
*/
package java.text;
import android.icu.text.TimeZoneNames;
import android.icu.util.ULocale;
import com.android.icu.text.ExtendedTimeZoneNames;
import com.android.icu.text.ExtendedTimeZoneNames.Match;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import libcore.icu.SimpleDateFormatData;
import sun.util.calendar.CalendarUtils;
import static java.text.DateFormatSymbols.*;
// Android-changed: Added supported API level, removed unnecessary
// Android-changed: Clarified info about X symbol time zone parsing
// Android-changed: Changed MMMMM to MMMM in month format example (ICU behavior).
// http://b/147860740
/**
* {@code SimpleDateFormat} is a concrete class for formatting and
* parsing dates in a locale-sensitive manner. It allows for formatting
* (date → text), parsing (text → date), and normalization.
*
*
* {@code SimpleDateFormat} allows you to start by choosing * any user-defined patterns for date-time formatting. However, you * are encouraged to create a date-time formatter with either * {@code getTimeInstance}, {@code getDateInstance}, or * {@code getDateTimeInstance} in {@code DateFormat}. Each * of these class methods can return a date/time formatter initialized * with a default format pattern. You may modify the format pattern * using the {@code applyPattern} methods as desired. * For more information on using these methods, see * {@link DateFormat}. * *
* Date and time formats are specified by date and time pattern * strings. * Within date and time pattern strings, unquoted letters from * {@code 'A'} to {@code 'Z'} and from {@code 'a'} to * {@code 'z'} are interpreted as pattern letters representing the * components of a date or time string. * Text can be quoted using single quotes ({@code '}) to avoid * interpretation. * {@code "''"} represents a single quote. * All other characters are not interpreted; they're simply copied into the * output string during formatting or matched against the input string * during parsing. *
* The following pattern letters are defined (all other characters from * {@code 'A'} to {@code 'Z'} and from {@code 'a'} to * {@code 'z'} are reserved): *
** Pattern letters are usually repeated, as their number determines the * exact presentation: **
* * ** * *Letter * Date or Time Component * Presentation * Examples * Supported (API Levels) * * {@code G} * Era designator * Text * {@code AD} * 1+ ** {@code y} * Year * Year * {@code 1996}; {@code 96} * 1+ ** {@code Y} * Week year * Year * {@code 2009}; {@code 09} * 24+ ** {@code M} * Month in year (context sensitive) * Month * {@code July}; {@code Jul}; {@code 07} * 1+ ** {@code L} * Month in year (standalone form) * Month * {@code July}; {@code Jul}; {@code 07} * TBD ** {@code w} * Week in year * Number * {@code 27} * 1+ ** {@code W} * Week in month * Number * {@code 2} * 1+ ** {@code D} * Day in year * Number * {@code 189} * 1+ ** {@code d} * Day in month * Number * {@code 10} * 1+ ** {@code F} * Day of week in month * Number * {@code 2} * 1+ ** {@code E} * Day name in week * Text * {@code Tuesday}; {@code Tue} * 1+ ** {@code u} * Day number of week (1 = Monday, ..., 7 = Sunday) * Number * {@code 1} * 24+ ** {@code a} * Am/pm marker * Text * {@code PM} * 1+ ** {@code H} * Hour in day (0-23) * Number * {@code 0} * 1+ ** {@code k} * Hour in day (1-24) * Number * {@code 24} * 1+ ** {@code K} * Hour in am/pm (0-11) * Number * {@code 0} * 1+ ** {@code h} * Hour in am/pm (1-12) * Number * {@code 12} * 1+ ** {@code m} * Minute in hour * Number * {@code 30} * 1+ ** {@code s} * Second in minute * Number * {@code 55} * 1+ ** {@code S} * Millisecond * Number * {@code 978} * 1+ ** {@code z} * Time zone * General time zone * {@code Pacific Standard Time}; {@code PST}; {@code GMT-08:00} * 1+ ** {@code Z} * Time zone * RFC 822 time zone * {@code -0800} * 1+ ** *{@code X} * Time zone * ISO 8601 time zone * {@code -08}; {@code -0800}; {@code -08:00} * 24+ *
* GMTOffsetTimeZone: * {@code GMT} Sign Hours {@code :} Minutes * Sign: one of * {@code + -} * Hours: * Digit * Digit Digit * Minutes: * Digit Digit * Digit: one of * {@code 0 1 2 3 4 5 6 7 8 9}* Hours must be between 0 and 23, and Minutes must be between * 00 and 59. The format is locale independent and digits must be taken * from the Basic Latin block of the Unicode standard. *
For parsing, RFC 822 time zones are also * accepted.
* RFC822TimeZone: * Sign TwoDigitHours Minutes * TwoDigitHours: * Digit Digit* TwoDigitHours must be between 00 and 23. Other definitions * are as for general time zones. * *
For parsing, general time zones are also * accepted. *
* ISO8601TimeZone: * OneLetterISO8601TimeZone * TwoLetterISO8601TimeZone * ThreeLetterISO8601TimeZone * OneLetterISO8601TimeZone: * Sign TwoDigitHours * {@code Z} * TwoLetterISO8601TimeZone: * Sign TwoDigitHours Minutes * {@code Z} * ThreeLetterISO8601TimeZone: * Sign TwoDigitHours {@code :} Minutes * {@code Z}* Other definitions are as for general time zones or * RFC 822 time zones. * *
For formatting, if the offset value from GMT is 0, {@code "Z"} is * produced. If the number of pattern letters is 1, any fraction of an hour * is ignored. For example, if the pattern is {@code "X"} and the time zone is * {@code "GMT+05:30"}, {@code "+05"} is produced. * *
For parsing, the letter {@code "Z"} is parsed as the UTC time zone designator (therefore * {@code "09:30Z"} is parsed as {@code "09:30 UTC"}. * General time zones are not accepted. *
If the number of {@code "X"} pattern letters is 4 or more (e.g. {@code XXXX}), {@link * IllegalArgumentException} is thrown when constructing a {@code * SimpleDateFormat} or {@linkplain #applyPattern(String) applying a * pattern}. *
** **
* * ** * *Date and Time Pattern * Result * * {@code "yyyy.MM.dd G 'at' HH:mm:ss z"} * {@code 2001.07.04 AD at 12:08:56 PDT} * * {@code "EEE, MMM d, ''yy"} * {@code Wed, Jul 4, '01} * * {@code "h:mm a"} * {@code 12:08 PM} * * {@code "hh 'o''clock' a, zzzz"} * {@code 12 o'clock PM, Pacific Daylight Time} * * {@code "K:mm a, z"} * {@code 0:08 PM, PDT} * * {@code "yyyyy.MMMMM.dd GGG hh:mm aaa"} * {@code 02001.July.04 AD 12:08 PM} * * {@code "EEE, d MMM yyyy HH:mm:ss Z"} * {@code Wed, 4 Jul 2001 12:08:56 -0700} * * {@code "yyMMddHHmmssZ"} * {@code 010704120856-0700} * * {@code "yyyy-MM-dd'T'HH:mm:ss.SSSZ"} * {@code 2001-07-04T12:08:56.235-0700} * * {@code "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"} * {@code 2001-07-04T12:08:56.235-07:00} * * *{@code "YYYY-'W'ww-u"} * {@code 2001-W27-3} *
* Date formats are not synchronized. * It is recommended to create separate format instances for each thread. * If multiple threads access a format concurrently, it must be synchronized * externally. * @apiNote Consider using {@link java.time.format.DateTimeFormatter} as an * immutable and thread-safe alternative. * * @see Java Tutorial * @see java.util.Calendar * @see java.util.TimeZone * @see DateFormat * @see DateFormatSymbols * @see java.time.format.DateTimeFormatter * @author Mark Davis, Chen-Lieh Huang, Alan Liu * @since 1.1 */ public class SimpleDateFormat extends DateFormat { // the official serial version ID which says cryptically // which version we're compatible with @java.io.Serial static final long serialVersionUID = 4774881970558875024L; // the internal serial version which says which version was written // - 0 (default) for version up to JDK 1.1.3 // - 1 for version from JDK 1.1.4, which includes a new field static final int currentSerialVersion = 1; /** * The version of the serialized data on the stream. Possible values: *
SimpleDateFormat
using the given date and time formatting styles.
* @param timeStyle the given date formatting style.
* @param dateStyle the given time formatting style.
* @param locale the locale whose pattern and date format symbols should be used
*/
SimpleDateFormat(int timeStyle, int dateStyle, Locale locale) {
this(getDateTimeFormat(timeStyle, dateStyle, locale), locale);
}
private static String getDateTimeFormat(int timeStyle, int dateStyle, Locale locale) {
SimpleDateFormatData data = SimpleDateFormatData.getInstance(locale);
if ((timeStyle >= 0) && (dateStyle >= 0)) {
Object[] dateTimeArgs = {
data.getDateFormat(dateStyle),
data.getTimeFormat(timeStyle),
};
return MessageFormat.format("{0} {1}", dateTimeArgs);
} else if (timeStyle >= 0) {
return data.getTimeFormat(timeStyle);
} else if (dateStyle >= 0) {
return data.getDateFormat(dateStyle);
} else {
throw new IllegalArgumentException("No date or time style specified");
}
}
// END Android-added: Ctor used by DateFormat to remove use of LocaleProviderAdapter.
/**
* Constructs a {@code SimpleDateFormat} using the given pattern and
* the default date format symbols for the default
* {@link java.util.Locale.Category#FORMAT FORMAT} locale.
* Note: This constructor may not support all locales.
* For full coverage, use the factory methods in the {@link DateFormat}
* class.
* This is equivalent to calling * {@link #SimpleDateFormat(String, Locale) * SimpleDateFormat(pattern, Locale.getDefault(Locale.Category.FORMAT))}. * * @see java.util.Locale#getDefault(java.util.Locale.Category) * @see java.util.Locale.Category#FORMAT * @param pattern the pattern describing the date and time format * @throws NullPointerException if the given pattern is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleDateFormat(String pattern) { this(pattern, Locale.getDefault(Locale.Category.FORMAT)); } /** * Constructs a {@code SimpleDateFormat} using the given pattern and * the default date format symbols for the given locale. * Note: This constructor may not support all locales. * For full coverage, use the factory methods in the {@link DateFormat} * class. * * @param pattern the pattern describing the date and time format * @param locale the locale whose date format symbols should be used * @throws NullPointerException if the given pattern or locale is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleDateFormat(String pattern, Locale locale) { if (pattern == null || locale == null) { throw new NullPointerException(); } initializeCalendar(locale); this.pattern = pattern; this.formatData = DateFormatSymbols.getInstanceRef(locale); this.locale = locale; initialize(locale); } /** * Constructs a {@code SimpleDateFormat} using the given pattern and * date format symbols. * * @param pattern the pattern describing the date and time format * @param formatSymbols the date format symbols to be used for formatting * @throws NullPointerException if the given pattern or formatSymbols is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleDateFormat(String pattern, DateFormatSymbols formatSymbols) { if (pattern == null || formatSymbols == null) { throw new NullPointerException(); } this.pattern = pattern; this.formatData = (DateFormatSymbols) formatSymbols.clone(); this.locale = Locale.getDefault(Locale.Category.FORMAT); initializeCalendar(this.locale); initialize(this.locale); useDateFormatSymbols = true; } /* Initialize compiledPattern and numberFormat fields */ private void initialize(Locale loc) { // Verify and compile the given pattern. compiledPattern = compile(pattern); /* try the cache first */ numberFormat = cachedNumberFormatData.get(loc); if (numberFormat == null) { /* cache miss */ numberFormat = NumberFormat.getIntegerInstance(loc); numberFormat.setGroupingUsed(false); /* update cache */ cachedNumberFormatData.putIfAbsent(loc, numberFormat); } numberFormat = (NumberFormat) numberFormat.clone(); initializeDefaultCentury(); } private void initializeCalendar(Locale loc) { if (calendar == null) { assert loc != null; // The format object must be constructed using the symbols for this zone. // However, the calendar should use the current default TimeZone. // If this is not contained in the locale zone strings, then the zone // will be formatted using generic GMT+/-H:MM nomenclature. calendar = Calendar.getInstance(loc); } } /** * Returns the compiled form of the given pattern. The syntax of * the compiled pattern is: *
* CompiledPattern: * EntryList * EntryList: * Entry * EntryList Entry * Entry: * TagField * TagField data * TagField: * Tag Length * TaggedData * Tag: * pattern_char_index * TAG_QUOTE_CHARS * Length: * short_length * long_length * TaggedData: * TAG_QUOTE_ASCII_CHAR ascii_char * ** * where `short_length' is an 8-bit unsigned integer between 0 and * 254. `long_length' is a sequence of an 8-bit integer 255 and a * 32-bit signed integer value which is split into upper and lower * 16-bit fields in two char's. `pattern_char_index' is an 8-bit * integer between 0 and 18. `ascii_char' is an 7-bit ASCII * character value. `data' depends on its Tag value. *
* If Length is short_length, Tag and short_length are packed in a * single char, as illustrated below. *
* char[0] = (Tag << 8) | short_length; ** * If Length is long_length, Tag and 255 are packed in the first * char and a 32-bit integer, as illustrated below. *
* char[0] = (Tag << 8) | 255; * char[1] = (char) (long_length >>> 16); * char[2] = (char) (long_length & 0xffff); **
* If Tag is a pattern_char_index, its Length is the number of * pattern characters. For example, if the given pattern is * "yyyy", Tag is 1 and Length is 4, followed by no data. *
* If Tag is TAG_QUOTE_CHARS, its Length is the number of char's
* following the TagField. For example, if the given pattern is
* "'o''clock'", Length is 7 followed by a char sequence of
* o&nbs;'&nbs;c&nbs;l&nbs;o&nbs;c&nbs;k
.
*
* TAG_QUOTE_ASCII_CHAR is a special tag and has an ASCII
* character in place of Length. For example, if the given pattern
* is "'o'", the TaggedData entry is
* ((TAG_QUOTE_ASCII_CHAR&nbs;<<&nbs;8)&nbs;|&nbs;'o')
.
*
* @throws NullPointerException if the given pattern is null
* @throws IllegalArgumentException if the given pattern is invalid
*/
private char[] compile(String pattern) {
int length = pattern.length();
boolean inQuote = false;
StringBuilder compiledCode = new StringBuilder(length * 2);
StringBuilder tmpBuffer = null;
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
int count = 0, tagcount = 0;
int lastTag = -1, prevTag = -1;
*/
int count = 0;
int lastTag = -1;
// END Android-removed: App compat for formatting pattern letter M.
for (int i = 0; i < length; i++) {
char c = pattern.charAt(i);
if (c == '\'') {
// '' is treated as a single quote regardless of being
// in a quoted section.
if ((i + 1) < length) {
c = pattern.charAt(i + 1);
if (c == '\'') {
i++;
if (count != 0) {
encode(lastTag, count, compiledCode);
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
tagcount++;
prevTag = lastTag;
*/
// END Android-removed: App compat for formatting pattern letter M.
lastTag = -1;
count = 0;
}
if (inQuote) {
tmpBuffer.append(c);
} else {
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
}
continue;
}
}
if (!inQuote) {
if (count != 0) {
encode(lastTag, count, compiledCode);
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
tagcount++;
prevTag = lastTag;
*/
// END Android-removed: App compat for formatting pattern letter M.
lastTag = -1;
count = 0;
}
if (tmpBuffer == null) {
tmpBuffer = new StringBuilder(length);
} else {
tmpBuffer.setLength(0);
}
inQuote = true;
} else {
int len = tmpBuffer.length();
if (len == 1) {
char ch = tmpBuffer.charAt(0);
if (ch < 128) {
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | ch));
} else {
compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | 1));
compiledCode.append(ch);
}
} else {
encode(TAG_QUOTE_CHARS, len, compiledCode);
compiledCode.append(tmpBuffer);
}
inQuote = false;
}
continue;
}
if (inQuote) {
tmpBuffer.append(c);
continue;
}
if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
if (count != 0) {
encode(lastTag, count, compiledCode);
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
tagcount++;
prevTag = lastTag;
*/
// END Android-removed: App compat for formatting pattern letter M.
lastTag = -1;
count = 0;
}
if (c < 128) {
// In most cases, c would be a delimiter, such as ':'.
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
} else {
// Take any contiguous non-ASCII alphabet characters and
// put them in a single TAG_QUOTE_CHARS.
int j;
for (j = i + 1; j < length; j++) {
char d = pattern.charAt(j);
if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) {
break;
}
}
encode(TAG_QUOTE_CHARS, j - i, compiledCode);
for (; i < j; i++) {
compiledCode.append(pattern.charAt(i));
}
i--;
}
continue;
}
int tag;
if ((tag = DateFormatSymbols.patternChars.indexOf(c)) == -1) {
throw new IllegalArgumentException("Illegal pattern character " +
"'" + c + "'");
}
if (lastTag == -1 || lastTag == tag) {
lastTag = tag;
count++;
continue;
}
encode(lastTag, count, compiledCode);
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
tagcount++;
prevTag = lastTag;
*/
// END Android-removed: App compat for formatting pattern letter M.
lastTag = tag;
count = 1;
}
if (inQuote) {
throw new IllegalArgumentException("Unterminated quote");
}
if (count != 0) {
encode(lastTag, count, compiledCode);
// BEGIN Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
/*
tagcount++;
prevTag = lastTag;
*/
// END Android-removed: App compat for formatting pattern letter M.
}
// Android-removed: App compat for formatting pattern letter M.
// See forceStandaloneForm field
// forceStandaloneForm = (tagcount == 1 && prevTag == PATTERN_MONTH);
// Copy the compiled pattern to a char array
int len = compiledCode.length();
char[] r = new char[len];
compiledCode.getChars(0, len, r, 0);
return r;
}
/**
* Encodes the given tag and length and puts encoded char(s) into buffer.
*/
private static void encode(int tag, int length, StringBuilder buffer) {
if (tag == PATTERN_ISO_ZONE && length >= 4) {
throw new IllegalArgumentException("invalid ISO 8601 format: length=" + length);
}
if (length < 255) {
buffer.append((char)(tag << 8 | length));
} else {
buffer.append((char)((tag << 8) | 0xff));
buffer.append((char)(length >>> 16));
buffer.append((char)(length & 0xffff));
}
}
/* Initialize the fields we use to disambiguate ambiguous years. Separate
* so we can call it from readObject().
*/
private void initializeDefaultCentury() {
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add( Calendar.YEAR, -80 );
parseAmbiguousDatesAsAfter(calendar.getTime());
}
/* Define one-century window into which to disambiguate dates using
* two-digit years.
*/
private void parseAmbiguousDatesAsAfter(Date startDate) {
defaultCenturyStart = startDate;
calendar.setTime(startDate);
defaultCenturyStartYear = calendar.get(Calendar.YEAR);
}
/**
* Sets the 100-year period 2-digit years will be interpreted as being in
* to begin on the date the user specifies.
*
* @param startDate During parsing, two digit years will be placed in the range
* {@code startDate} to {@code startDate + 100 years}.
* @see #get2DigitYearStart
* @throws NullPointerException if {@code startDate} is {@code null}.
* @since 1.2
*/
public void set2DigitYearStart(Date startDate) {
parseAmbiguousDatesAsAfter(new Date(startDate.getTime()));
}
/**
* Returns the beginning date of the 100-year period 2-digit years are interpreted
* as being within.
*
* @return the start of the 100-year period into which two digit years are
* parsed
* @see #set2DigitYearStart
* @since 1.2
*/
public Date get2DigitYearStart() {
return (Date) defaultCenturyStart.clone();
}
/**
* Formats the given {@code Date} into a date/time string and appends
* the result to the given {@code StringBuffer}.
*
* @param date the date-time value to be formatted into a date-time string.
* @param toAppendTo where the new date-time text is to be appended.
* @param pos keeps track on the position of the field within
* the returned string. For example, given a date-time text
* {@code "1996.07.10 AD at 15:08:56 PDT"}, if the given {@code fieldPosition}
* is {@link DateFormat#YEAR_FIELD}, the begin index and end index of
* {@code fieldPosition} will be set to 0 and 4, respectively.
* Notice that if the same date-time field appears more than once in a
* pattern, the {@code fieldPosition} will be set for the first occurrence
* of that date-time field. For instance, formatting a {@code Date} to the
* date-time string {@code "1 PM PDT (Pacific Daylight Time)"} using the
* pattern {@code "h a z (zzzz)"} and the alignment field
* {@link DateFormat#TIMEZONE_FIELD}, the begin index and end index of
* {@code fieldPosition} will be set to 5 and 8, respectively, for the
* first occurrence of the timezone pattern character {@code 'z'}.
* @return the formatted date-time string.
* @throws NullPointerException if any of the parameters is {@code null}.
*/
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition pos)
{
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
/**
* Formats an Object producing an {@code AttributedCharacterIterator}.
* You can use the returned {@code AttributedCharacterIterator}
* to build the resulting String, as well as to determine information
* about the resulting String.
*
* Each attribute key of the AttributedCharacterIterator will be of type * {@code DateFormat.Field}, with the corresponding attribute value * being the same as the attribute key. * * @throws NullPointerException if obj is null. * @throws IllegalArgumentException if the Format cannot format the * given object, or if the Format's pattern string is invalid. * @param obj The object to format * @return AttributedCharacterIterator describing the formatted value. * @since 1.4 */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object obj) { StringBuffer sb = new StringBuffer(); CharacterIteratorFieldDelegate delegate = new CharacterIteratorFieldDelegate(); if (obj instanceof Date) { format((Date)obj, sb, delegate); } else if (obj instanceof Number) { format(new Date(((Number)obj).longValue()), sb, delegate); } else if (obj == null) { throw new NullPointerException( "formatToCharacterIterator must be passed non-null object"); } else { throw new IllegalArgumentException( "Cannot format given Object as a Date"); } return delegate.getIterator(sb.toString()); } // Map index into pattern character string to Calendar field number private static final int[] PATTERN_INDEX_TO_CALENDAR_FIELD = { Calendar.ERA, Calendar.YEAR, Calendar.MONTH, Calendar.DATE, Calendar.HOUR_OF_DAY, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND, Calendar.DAY_OF_WEEK, Calendar.DAY_OF_YEAR, Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH, Calendar.AM_PM, Calendar.HOUR, Calendar.HOUR, Calendar.ZONE_OFFSET, Calendar.ZONE_OFFSET, CalendarBuilder.WEEK_YEAR, // Pseudo Calendar field CalendarBuilder.ISO_DAY_OF_WEEK, // Pseudo Calendar field Calendar.ZONE_OFFSET, Calendar.MONTH, // Android-added: 'c' for standalone day of week. Calendar.DAY_OF_WEEK, // Android-added: Support for 'b'/'B' (day period). Calendar.AM_PM is just used as a // placeholder in the absence of full support for day period. Calendar.AM_PM, Calendar.AM_PM }; // Map index into pattern character string to DateFormat field number private static final int[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD = { DateFormat.ERA_FIELD, DateFormat.YEAR_FIELD, DateFormat.MONTH_FIELD, DateFormat.DATE_FIELD, DateFormat.HOUR_OF_DAY1_FIELD, DateFormat.HOUR_OF_DAY0_FIELD, DateFormat.MINUTE_FIELD, DateFormat.SECOND_FIELD, DateFormat.MILLISECOND_FIELD, DateFormat.DAY_OF_WEEK_FIELD, DateFormat.DAY_OF_YEAR_FIELD, DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD, DateFormat.WEEK_OF_YEAR_FIELD, DateFormat.WEEK_OF_MONTH_FIELD, DateFormat.AM_PM_FIELD, DateFormat.HOUR1_FIELD, DateFormat.HOUR0_FIELD, DateFormat.TIMEZONE_FIELD, DateFormat.TIMEZONE_FIELD, DateFormat.YEAR_FIELD, DateFormat.DAY_OF_WEEK_FIELD, DateFormat.TIMEZONE_FIELD, DateFormat.MONTH_FIELD, // Android-added: 'c' for standalone day of week. DateFormat.DAY_OF_WEEK_FIELD, // Android-added: Support for 'b'/'B' (day period). DateFormat.AM_PM_FIELD is just used as a // placeholder in the absence of full support for day period. DateFormat.AM_PM_FIELD, DateFormat.AM_PM_FIELD }; // Maps from DecimalFormatSymbols index to Field constant private static final Field[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD_ID = { Field.ERA, Field.YEAR, Field.MONTH, Field.DAY_OF_MONTH, Field.HOUR_OF_DAY1, Field.HOUR_OF_DAY0, Field.MINUTE, Field.SECOND, Field.MILLISECOND, Field.DAY_OF_WEEK, Field.DAY_OF_YEAR, Field.DAY_OF_WEEK_IN_MONTH, Field.WEEK_OF_YEAR, Field.WEEK_OF_MONTH, Field.AM_PM, Field.HOUR1, Field.HOUR0, Field.TIME_ZONE, Field.TIME_ZONE, Field.YEAR, Field.DAY_OF_WEEK, Field.TIME_ZONE, Field.MONTH, // Android-added: 'c' for standalone day of week. Field.DAY_OF_WEEK, // Android-added: Support for 'b'/'B' (day period). Field.AM_PM is just used as a // placeholder in the absence of full support for day period. Field.AM_PM, Field.AM_PM }; /** * Private member function that does the real date/time formatting. */ private void subFormat(int patternCharIndex, int count, FieldDelegate delegate, StringBuffer buffer, boolean useDateFormatSymbols) { int maxIntCount = Integer.MAX_VALUE; String current = null; int beginOffset = buffer.length(); int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; int value; if (field == CalendarBuilder.WEEK_YEAR) { if (calendar.isWeekDateSupported()) { value = calendar.getWeekYear(); } else { // use calendar year 'y' instead patternCharIndex = PATTERN_YEAR; field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; value = calendar.get(field); } } else if (field == CalendarBuilder.ISO_DAY_OF_WEEK) { value = CalendarBuilder.toISODayOfWeek(calendar.get(Calendar.DAY_OF_WEEK)); } else { value = calendar.get(field); } int style = (count >= 4) ? Calendar.LONG : Calendar.SHORT; if (!useDateFormatSymbols && field != CalendarBuilder.ISO_DAY_OF_WEEK) { current = calendar.getDisplayName(field, style, locale); } // Note: zeroPaddingNumber() assumes that maxDigits is either // 2 or maxIntCount. If we make any changes to this, // zeroPaddingNumber() must be fixed. switch (patternCharIndex) { case PATTERN_ERA: // 'G' if (useDateFormatSymbols) { String[] eras = formatData.getEras(); if (value < eras.length) { current = eras[value]; } } if (current == null) { current = ""; } break; case PATTERN_WEEK_YEAR: // 'Y' case PATTERN_YEAR: // 'y' if (calendar instanceof GregorianCalendar) { if (count != 2) { zeroPaddingNumber(value, count, maxIntCount, buffer); } else { zeroPaddingNumber(value, 2, 2, buffer); } // clip 1996 to 96 } else { if (current == null) { zeroPaddingNumber(value, style == Calendar.LONG ? 1 : count, maxIntCount, buffer); } } break; case PATTERN_MONTH: // 'M' (context sensitive) // BEGIN Android-changed: formatMonth() method to format using ICU data. /* if (useDateFormatSymbols) { String[] months; if (count >= 4) { months = formatData.getMonths(); current = months[value]; } else if (count == 3) { months = formatData.getShortMonths(); current = months[value]; } } else { if (count < 3) { current = null; } else if (forceStandaloneForm) { current = calendar.getDisplayName(field, style | 0x8000, locale); if (current == null) { current = calendar.getDisplayName(field, style, locale); } } } if (current == null) { zeroPaddingNumber(value+1, count, maxIntCount, buffer); } */ current = formatMonth(count, value, maxIntCount, buffer, useDateFormatSymbols, false /* standalone */, field, style); // END Android-changed: formatMonth() method to format using ICU data. break; case PATTERN_MONTH_STANDALONE: // 'L' // BEGIN Android-changed: formatMonth() method to format using ICU data. /* assert current == null; if (locale == null) { String[] months; if (count >= 4) { months = formatData.getMonths(); current = months[value]; } else if (count == 3) { months = formatData.getShortMonths(); current = months[value]; } } else { if (count >= 3) { current = calendar.getDisplayName(field, style | 0x8000, locale); } } if (current == null) { zeroPaddingNumber(value+1, count, maxIntCount, buffer); } */ current = formatMonth(count, value, maxIntCount, buffer, useDateFormatSymbols, true /* standalone */, field, style); // END Android-changed: formatMonth() method to format using ICU data. break; case PATTERN_HOUR_OF_DAY1: // 'k' 1-based. eg, 23:59 + 1 hour =>> 24:59 if (current == null) { if (value == 0) { zeroPaddingNumber(calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1, count, maxIntCount, buffer); } else { zeroPaddingNumber(value, count, maxIntCount, buffer); } } break; case PATTERN_DAY_OF_WEEK: // 'E' // BEGIN Android-removed: App compat for formatting pattern letter M. // See forceStandaloneForm field /* if (useDateFormatSymbols) { String[] weekdays; if (count >= 4) { weekdays = formatData.getWeekdays(); current = weekdays[value]; } else { // count < 4, use abbreviated form if exists weekdays = formatData.getShortWeekdays(); current = weekdays[value]; } } */ if (current == null) { current = formatWeekday(count, value, useDateFormatSymbols, false /* standalone */); } // END Android-removed: App compat for formatting pattern letter M. break; // BEGIN Android-added: support for 'c' (standalone day of week). case PATTERN_STANDALONE_DAY_OF_WEEK: // 'c' if (current == null) { current = formatWeekday(count, value, useDateFormatSymbols, true /* standalone */); } break; // END Android-added: support for 'c' (standalone day of week). case PATTERN_AM_PM: // 'a' if (useDateFormatSymbols) { String[] ampm = formatData.getAmPmStrings(); current = ampm[value]; } break; // Android-added: Ignore 'b' and 'B' introduced in CLDR 32+ pattern data. http://b/68139386 // Not currently supported here. case PATTERN_DAY_PERIOD: case PATTERN_FLEXIBLE_DAY_PERIOD: current = ""; break; case PATTERN_HOUR1: // 'h' 1-based. eg, 11PM + 1 hour =>> 12 AM if (current == null) { if (value == 0) { zeroPaddingNumber(calendar.getLeastMaximum(Calendar.HOUR) + 1, count, maxIntCount, buffer); } else { zeroPaddingNumber(value, count, maxIntCount, buffer); } } break; case PATTERN_ZONE_NAME: // 'z' if (current == null) { // BEGIN Android-changed: Format time zone name using ICU. /* if (formatData.locale == null || formatData.isZoneStringsSet) { int zoneIndex = formatData.getZoneIndex(calendar.getTimeZone().getID()); if (zoneIndex == -1) { value = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); buffer.append(ZoneInfoFile.toCustomID(value)); } else { int index = (calendar.get(Calendar.DST_OFFSET) == 0) ? 1: 3; if (count < 4) { // Use the short name index++; } String[][] zoneStrings = formatData.getZoneStringsWrapper(); buffer.append(zoneStrings[zoneIndex][index]); } } else { TimeZone tz = calendar.getTimeZone(); boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0); int tzstyle = (count < 4 ? TimeZone.SHORT : TimeZone.LONG); buffer.append(tz.getDisplayName(daylight, tzstyle, formatData.locale)); } */ TimeZone tz = calendar.getTimeZone(); boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0); String zoneString; if (formatData.isZoneStringsSet) { // DateFormatSymbols.setZoneStrings() has be used, use those values instead of // ICU code. int tzstyle = count < 4 ? TimeZone.SHORT : TimeZone.LONG; zoneString = libcore.icu.TimeZoneNames.getDisplayName( formatData.getZoneStringsWrapper(), tz.getID(), daylight, tzstyle); } else { TimeZoneNames.NameType nameType; if (count < 4) { nameType = daylight ? TimeZoneNames.NameType.SHORT_DAYLIGHT : TimeZoneNames.NameType.SHORT_STANDARD; } else { nameType = daylight ? TimeZoneNames.NameType.LONG_DAYLIGHT : TimeZoneNames.NameType.LONG_STANDARD; } String canonicalID = android.icu.util.TimeZone.getCanonicalID(tz.getID()); zoneString = getTimeZoneNames() .getDisplayName(canonicalID, nameType, calendar.getTimeInMillis()); } if (zoneString != null) { buffer.append(zoneString); } else { int offsetMillis = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); buffer.append(TimeZone.createGmtOffsetString(true, true, offsetMillis)); } // END Android-changed: Format time zone name using ICU. } break; case PATTERN_ZONE_VALUE: // 'Z' ("-/+hhmm" form) // BEGIN Android-changed: Use shared code in TimeZone for zone offset string. /* value = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / 60000; int width = 4; if (value >= 0) { buffer.append('+'); } else { width++; } int num = (value / 60) * 100 + (value % 60); CalendarUtils.sprintf0d(buffer, num, width); */ value = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); final boolean includeSeparator = (count >= 4); final boolean includeGmt = (count == 4); buffer.append(TimeZone.createGmtOffsetString(includeGmt, includeSeparator, value)); break; // END Android-changed: Use shared code in TimeZone for zone offset string. case PATTERN_ISO_ZONE: // 'X' value = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); if (value == 0) { buffer.append('Z'); break; } value /= 60000; if (value >= 0) { buffer.append('+'); } else { buffer.append('-'); value = -value; } CalendarUtils.sprintf0d(buffer, value / 60, 2); if (count == 1) { break; } if (count == 3) { buffer.append(':'); } CalendarUtils.sprintf0d(buffer, value % 60, 2); break; // BEGIN Android-added: Better UTS#35 conformity for fractional seconds. case PATTERN_MILLISECOND: // 'S' // Fractional seconds must be treated specially. We must always convert the parsed // value into a fractional second [0, 1) and then widen it out to the appropriate // formatted size. For example, an initial value of 789 will be converted // 0.789 and then become ".7" (S) or ".78" (SS) or "0.789" (SSS) or "0.7890" (SSSS) // in the resulting formatted output. if (current == null) { value = (int) (((double) value / 1000) * Math.pow(10, count)); zeroPaddingNumber(value, count, count, buffer); } break; // END Android-added: Better UTS#35 conformity for fractional seconds. default: // case PATTERN_DAY_OF_MONTH: // 'd' // case PATTERN_HOUR_OF_DAY0: // 'H' 0-based. eg, 23:59 + 1 hour =>> 00:59 // case PATTERN_MINUTE: // 'm' // case PATTERN_SECOND: // 's' // Android-removed: PATTERN_MILLISECONDS handled in an explicit case above. //// case PATTERN_MILLISECOND: // 'S' // case PATTERN_DAY_OF_YEAR: // 'D' // case PATTERN_DAY_OF_WEEK_IN_MONTH: // 'F' // case PATTERN_WEEK_OF_YEAR: // 'w' // case PATTERN_WEEK_OF_MONTH: // 'W' // case PATTERN_HOUR0: // 'K' eg, 11PM + 1 hour =>> 0 AM // case PATTERN_ISO_DAY_OF_WEEK: // 'u' pseudo field, Monday = 1, ..., Sunday = 7 if (current == null) { zeroPaddingNumber(value, count, maxIntCount, buffer); } break; } // switch (patternCharIndex) if (current != null) { buffer.append(current); } int fieldID = PATTERN_INDEX_TO_DATE_FORMAT_FIELD[patternCharIndex]; Field f = PATTERN_INDEX_TO_DATE_FORMAT_FIELD_ID[patternCharIndex]; delegate.formatted(fieldID, f, f, beginOffset, buffer.length(), buffer); } // BEGIN Android-added: formatWeekday() and formatMonth() methods to format using ICU data. private String formatWeekday(int count, int value, boolean useDateFormatSymbols, boolean standalone) { if (useDateFormatSymbols) { final String[] weekdays; if (count == 4) { weekdays = standalone ? formatData.getStandAloneWeekdays() : formatData.getWeekdays(); } else if (count == 5) { weekdays = standalone ? formatData.getTinyStandAloneWeekdays() : formatData.getTinyWeekdays(); } else { // count < 4, use abbreviated form if exists weekdays = standalone ? formatData.getShortStandAloneWeekdays() : formatData.getShortWeekdays(); } return weekdays[value]; } return null; } private String formatMonth(int count, int value, int maxIntCount, StringBuffer buffer, boolean useDateFormatSymbols, boolean standalone, int field, int style) { String current = null; if (useDateFormatSymbols) { final String[] months; if (count == 4) { months = standalone ? formatData.getStandAloneMonths() : formatData.getMonths(); } else if (count == 5) { months = standalone ? formatData.getTinyStandAloneMonths() : formatData.getTinyMonths(); } else if (count == 3) { months = standalone ? formatData.getShortStandAloneMonths() : formatData.getShortMonths(); } else { months = null; } if (months != null) { current = months[value]; } } else { if (count < 3) { current = null; } else { if (standalone) { style = Calendar.toStandaloneStyle(style); } current = calendar.getDisplayName(field, style, locale); } } if (current == null) { zeroPaddingNumber(value+1, count, maxIntCount, buffer); } return current; } // END Android-added: formatWeekday() and formatMonth() methods to format using ICU data. /** * Formats a number with the specified minimum and maximum number of digits. */ private void zeroPaddingNumber(int value, int minDigits, int maxDigits, StringBuffer buffer) { // Optimization for 1, 2 and 4 digit numbers. This should // cover most cases of formatting date/time related items. // Note: This optimization code assumes that maxDigits is // either 2 or Integer.MAX_VALUE (maxIntCount in format()). try { if (zeroDigit == 0) { zeroDigit = ((DecimalFormat)numberFormat).getDecimalFormatSymbols().getZeroDigit(); } if (value >= 0) { if (value < 100 && minDigits >= 1 && minDigits <= 2) { if (value < 10) { if (minDigits == 2) { buffer.append(zeroDigit); } buffer.append((char)(zeroDigit + value)); } else { buffer.append((char)(zeroDigit + value / 10)); buffer.append((char)(zeroDigit + value % 10)); } return; } else if (value >= 1000 && value < 10000) { if (minDigits == 4) { buffer.append((char)(zeroDigit + value / 1000)); value %= 1000; buffer.append((char)(zeroDigit + value / 100)); value %= 100; buffer.append((char)(zeroDigit + value / 10)); buffer.append((char)(zeroDigit + value % 10)); return; } if (minDigits == 2 && maxDigits == 2) { zeroPaddingNumber(value % 100, 2, 2, buffer); return; } } } } catch (Exception e) { } numberFormat.setMinimumIntegerDigits(minDigits); numberFormat.setMaximumIntegerDigits(maxDigits); numberFormat.format((long)value, buffer, DontCareFieldPosition.INSTANCE); } /** * Parses text from a string to produce a {@code Date}. *
* The method attempts to parse text starting at the index given by * {@code pos}. * If parsing succeeds, then the index of {@code pos} is updated * to the index after the last character used (parsing does not necessarily * use all characters up to the end of the string), and the parsed * date is returned. The updated {@code pos} can be used to * indicate the starting point for the next call to this method. * If an error occurs, then the index of {@code pos} is not * changed, the error index of {@code pos} is set to the index of * the character where the error occurred, and null is returned. * *
This parsing operation uses the {@link DateFormat#calendar
* calendar} to produce a {@code Date}. All of the {@code
* calendar}'s date-time fields are {@linkplain Calendar#clear()
* cleared} before parsing, and the {@code calendar}'s default
* values of the date-time fields are used for any missing
* date-time information. For example, the year value of the
* parsed {@code Date} is 1970 with {@link GregorianCalendar} if
* no year value is given from the parsing operation. The {@code
* TimeZone} value may be overwritten, depending on the given
* pattern and the time zone value in {@code text}. Any {@code
* TimeZone} value that has previously been set by a call to
* {@link #setTimeZone(java.util.TimeZone) setTimeZone} may need
* to be restored for further operations.
*
* @param text A {@code String}, part of which should be parsed.
* @param pos A {@code ParsePosition} object with index and error
* index information as described above.
* @return A {@code Date} parsed from the string. In case of
* error, returns null.
* @throws NullPointerException if {@code text} or {@code pos} is null.
*/
@Override
public Date parse(String text, ParsePosition pos) {
// BEGIN Android-changed: extract parseInternal() and avoid modifying timezone during parse.
// Make sure the timezone associated with this dateformat instance (set via
// {@code setTimeZone} isn't change as a side-effect of parsing a date.
final TimeZone tz = getTimeZone();
try {
return parseInternal(text, pos);
} finally {
setTimeZone(tz);
}
}
private Date parseInternal(String text, ParsePosition pos)
{
// END Android-changed: extract parseInternal() and avoid modifying timezone during parse.
checkNegativeNumberExpression();
int start = pos.index;
int oldStart = start;
int textLength = text.length();
boolean[] ambiguousYear = {false};
CalendarBuilder calb = new CalendarBuilder();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
if (start >= textLength || text.charAt(start) != (char)count) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
break;
case TAG_QUOTE_CHARS:
while (count-- > 0) {
if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
}
break;
default:
// Peek the next pattern to determine if we need to
// obey the number of pattern letters for
// parsing. It's required when parsing contiguous
// digit text (e.g., "20010704") with a pattern which
// has no delimiters between fields, like "yyyyMMdd".
boolean obeyCount = false;
// In Arabic, a minus sign for a negative number is put after
// the number. Even in another locale, a minus sign can be
// put after a number using DateFormat.setNumberFormat().
// If both the minus sign and the field-delimiter are '-',
// subParse() needs to determine whether a '-' after a number
// in the given text is a delimiter or is a minus sign for the
// preceding number. We give subParse() a clue based on the
// information in compiledPattern.
boolean useFollowingMinusSignAsDelimiter = false;
if (i < compiledPattern.length) {
int nextTag = compiledPattern[i] >>> 8;
int nextCount = compiledPattern[i] & 0xff;
obeyCount = shouldObeyCount(nextTag, nextCount);
if (hasFollowingMinusSign &&
(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
if (nextTag != TAG_QUOTE_ASCII_CHAR) {
nextCount = compiledPattern[i+1];
}
if (nextCount == minusSign) {
useFollowingMinusSignAsDelimiter = true;
}
}
}
start = subParse(text, start, tag, count, obeyCount,
ambiguousYear, pos,
useFollowingMinusSignAsDelimiter, calb);
if (start < 0) {
pos.index = oldStart;
return null;
}
}
}
// At this point the fields of Calendar have been set. Calendar
// will fill in default values for missing fields when the time
// is computed.
pos.index = start;
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
/* If the next tag/pattern is a