/* * Copyright (c) 1996, 2019, 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 java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import static java.text.DateFormatSymbols.*; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.Map; import java.util.SimpleTimeZone; import java.util.SortedMap; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import sun.util.calendar.CalendarUtils; import sun.util.calendar.ZoneInfoFile; import sun.util.locale.provider.LocaleProviderAdapter; import sun.util.locale.provider.TimeZoneNameUtility; /** * {@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 * * {@code G} * Era designator * Text * {@code AD} * * {@code y} * Year * Year * {@code 1996}; {@code 96} * * {@code Y} * Week year * Year * {@code 2009}; {@code 09} * * {@code M} * Month in year (context sensitive) * Month * {@code July}; {@code Jul}; {@code 07} * * {@code L} * Month in year (standalone form) * Month * {@code July}; {@code Jul}; {@code 07} * * {@code w} * Week in year * Number * {@code 27} * * {@code W} * Week in month * Number * {@code 2} * * {@code D} * Day in year * Number * {@code 189} * * {@code d} * Day in month * Number * {@code 10} * * {@code F} * Day of week in month * Number * {@code 2} * * {@code E} * Day name in week * Text * {@code Tuesday}; {@code Tue} * * {@code u} * Day number of week (1 = Monday, ..., 7 = Sunday) * Number * {@code 1} * * {@code a} * Am/pm marker * Text * {@code PM} * * {@code H} * Hour in day (0-23) * Number * {@code 0} * * {@code k} * Hour in day (1-24) * Number * {@code 24} * * {@code K} * Hour in am/pm (0-11) * Number * {@code 0} * * {@code h} * Hour in am/pm (1-12) * Number * {@code 12} * * {@code m} * Minute in hour * Number * {@code 30} * * {@code s} * Second in minute * Number * {@code 55} * * {@code S} * Millisecond * Number * {@code 978} * * {@code z} * Time zone * General time zone * {@code Pacific Standard Time}; {@code PST}; {@code GMT-08:00} * * {@code Z} * Time zone * RFC 822 time zone * {@code -0800} * * *{@code X} * Time zone * ISO 8601 time zone * {@code -08}; {@code -0800}; {@code -08:00} *
* 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, {@code "Z"} is parsed as the UTC time zone designator. * General time zones are not accepted. * *
If the number of pattern letters is 4 or more, {@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. * * @see Java Tutorial * @see java.util.Calendar * @see java.util.TimeZone * @see DateFormat * @see DateFormatSymbols * @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: *
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;
int count = 0, tagcount = 0;
int lastTag = -1, prevTag = -1;
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);
tagcount++;
prevTag = lastTag;
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);
tagcount++;
prevTag = lastTag;
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);
tagcount++;
prevTag = lastTag;
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);
tagcount++;
prevTag = lastTag;
lastTag = tag;
count = 1;
}
if (inQuote) {
throw new IllegalArgumentException("Unterminated quote");
}
if (count != 0) {
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
}
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 }; // 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 }; // 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 }; /** * 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 < Calendar.ZONE_OFFSET && patternCharIndex != PATTERN_MONTH_STANDALONE) { 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) 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); } break; case PATTERN_MONTH_STANDALONE: // 'L' 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); } 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' 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]; } } break; case PATTERN_AM_PM: // 'a' if (useDateFormatSymbols) { String[] ampm = formatData.getAmPmStrings(); current = ampm[value]; } 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) { 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)); } } break; case PATTERN_ZONE_VALUE: // 'Z' ("-/+hhmm" form) 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); break; 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; 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' // 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); } /** * 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)
{
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