blob: e2422adb6d9b523501f2fdbfe96da4ad30c936f5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// BEGIN android-note
// changed from ICU to resource bundles and Java parsing/formatting
// END android-note
package java.text;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
// BEGIN android-added
import java.util.SimpleTimeZone;
import java.util.TimeZone;
// END android-added
import java.util.Vector;
import com.ibm.icu4jni.util.LocaleData;
import com.ibm.icu4jni.util.Resources;
import org.apache.harmony.text.internal.nls.Messages;
/**
* A concrete class for formatting and parsing dates in a locale-sensitive
* manner. It allows for formatting (date to text), parsing (text to date) and
* normalization.
* <p>
* {@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}.
* <h4>Time Format Syntax</h4>
* <p>
* To specify the time format, use a <em>time pattern</em> string. In this
* pattern, all ASCII letters are reserved as pattern letters, which are defined
* as follows:
* <table border=0 cellspacing=3 cellpadding=0>
* <tr bgcolor="#ccccff">
* <th>Symbol</th>
* <th>Meaning</th>
* <th>Presentation</th>
* <th>Example</th>
* </tr>
* <tr valign=top>
* <td>G</td>
* <td>era designator</td>
* <td>(Text)</td>
* <td>AD</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>y</td>
* <td>year</td>
* <td>(Number)</td>
* <td>1996</td>
* </tr>
* <tr valign=top>
* <td>M</td>
* <td>month in year</td>
* <td>(Text &amp; Number)</td>
* <td>July &amp; 07</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>d</td>
* <td>day in month</td>
* <td>(Number)</td>
* <td>10</td>
* </tr>
* <tr valign=top>
* <td>h</td>
* <td>hour in am/pm (1&tilde;12)</td>
* <td>(Number)</td>
* <td>12</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>H</td>
* <td>hour in day (0&tilde;23)</td>
* <td>(Number)</td>
* <td>0</td>
* </tr>
* <tr valign=top>
* <td>m</td>
* <td>minute in hour</td>
* <td>(Number)</td>
* <td>30</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>s</td>
* <td>second in minute</td>
* <td>(Number)</td>
* <td>55</td>
* </tr>
* <tr valign=top>
* <td>S</td>
* <td>fractional second</td>
* <td>(Number)</td>
* <td>978</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>E</td>
* <td>day of week</td>
* <td>(Text)</td>
* <td>Tuesday</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>D</td>
* <td>day in year</td>
* <td>(Number)</td>
* <td>189</td>
* </tr>
* <tr valign=top>
* <td>F</td>
* <td>day of week in month</td>
* <td>(Number)</td>
* <td>2 (2nd Wed in July)</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>w</td>
* <td>week in year</td>
* <td>(Number)</td>
* <td>27</td>
* </tr>
* <tr valign=top>
* <td>W</td>
* <td>week in month</td>
* <td>(Number)</td>
* <td>2</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>a</td>
* <td>am/pm marker</td>
* <td>(Text)</td>
* <td>PM</td>
* </tr>
* <tr valign=top>
* <td>k</td>
* <td>hour in day (1&tilde;24)</td>
* <td>(Number)</td>
* <td>24</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>K</td>
* <td>hour in am/pm (0&tilde;11)</td>
* <td>(Number)</td>
* <td>0</td>
* </tr>
* <tr valign=top>
* <td>z</td>
* <td>time zone</td>
* <td>(Text)</td>
* <td>Pacific Standard Time</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>Z</td>
* <td>time zone (RFC 822)</td>
* <td>(Number)</td>
* <td>-0800</td>
* </tr>
* <tr valign=top>
* <td>v</td>
* <td>time zone (generic)</td>
* <td>(Text)</td>
* <td>Pacific Time</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>V</td>
* <td>time zone (location)</td>
* <td>(Text)</td>
* <td>United States (Los Angeles)</td>
* </tr>
* <tr valign=top>
* <td>'</td>
* <td>escape for text</td>
* <td>(Delimiter)</td>
* <td>'Date='</td>
* </tr>
* <tr valign=top bgcolor="#eeeeff">
* <td>''</td>
* <td>single quote</td>
* <td>(Literal)</td>
* <td>'o''clock'</td>
* </tr>
* </table>
* <p>
* The count of pattern letters determines the format:
* <p>
* <strong>(Text)</strong>: 4 or more pattern letters &rarr; use the full form,
* less than 4 pattern letters &rarr; use a short or abbreviated form if one
* exists.
* <p>
* <strong>(Number)</strong>: the minimum number of digits. Shorter numbers are
* zero-padded to this amount. Year is handled specially; that is, if the count
* of 'y' is 2, the year will be truncated to 2 digits. (if "yyyy" produces
* "1997", "yy" produces "97".) Unlike other fields, fractional seconds are
* padded on the right with zero.
* <p>
* <strong>(Text & Number)</strong>: 3 or over, use text, otherwise use number.
* <p>
* Any characters in the pattern that are not in the ranges of ['a'..'z'] and
* ['A'..'Z'] will be treated as quoted text. For instance, characters like ':',
* '.', ' ', '#' and '@' will appear in the resulting time text even they are
* not embraced within single quotes.
* <p>
* A pattern containing any invalid pattern letter will result in an exception
* thrown during formatting or parsing.
* <h4>Examples Using the US Locale</h4> <blockquote>
*
* <pre>
* Format Pattern Result
* -------------- -------
* "yyyy.MM.dd G 'at' HH:mm:ss vvvv" &rarr; 1996.07.10 AD at 15:08:56 Pacific Time
* "EEE, MMM d, ''yy" &rarr; Wed, July 10, '96
* "h:mm a" &rarr; 12:08 PM
* "hh 'o''clock' a, zzzz" &rarr; 12 o'clock PM, Pacific Daylight Time
* "K:mm a, vvv" &rarr; 0:00 PM, PT
* "yyyyy.MMMMM.dd GGG hh:mm aaa" &rarr; 01996.July.10 AD 12:08 PM
* </pre>
*
* </blockquote> <h4>Code Sample:</h4> <blockquote>
*
* <pre>
* SimpleTimeZone pdt = new SimpleTimeZone(-8 * 60 * 60 * 1000, "PST");
* pdt.setStartRule(Calendar.APRIL, 1, Calendar.SUNDAY, 2 * 60 * 60 * 1000);
* pdt.setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2 * 60 * 60 * 1000);
*
* // Format the current time.
* SimpleDateFormat formatter = new SimpleDateFormat(
* "yyyy.MM.dd G 'at' hh:mm:ss a zzz");
* Date currentTime_1 = new Date();
* String dateString = formatter.format(currentTime_1);
*
* // Parse the previous string back into a Date.
* ParsePosition pos = new ParsePosition(0);
* Date currentTime_2 = formatter.parse(dateString, pos);
* </pre>
*
* </blockquote>
* <p>
* In the example, the time value {@code currentTime_2} obtained from parsing
* will be equal to {@code currentTime_1}. However, they may not be equal if the
* am/pm marker 'a' is left out from the format pattern while the
* "hour in am/pm" pattern symbol is used. This information loss can happen when
* formatting the time in PM.
* <p>
* When parsing a date string using the abbreviated year pattern ("yy"), {@code
* SimpleDateFormat} must interpret the abbreviated year relative to some
* century. It does this by adjusting dates to be within 80 years before and 20
* years after the time the {@code SimpleDateFormat} instance is created. For
* example, using a pattern of "MM/dd/yy" and a {@code SimpleDateFormat}
* instance created on Jan 1, 1997, the string "01/11/12" would be interpreted
* as Jan 11, 2012 while the string "05/04/64" would be interpreted as May 4,
* 1964. During parsing, only strings consisting of exactly two digits, as
* defined by {@link java.lang.Character#isDigit(char)}, will be parsed into the
* default century. Any other numeric string, such as a one digit string, a
* three or more digit string, or a two digit string that isn't all digits (for
* example, "-1"), is interpreted literally. So "01/02/3" or "01/02/003" are
* parsed, using the same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is
* parsed as Jan 2, 4 BC.
* <p>
* If the year pattern does not have exactly two 'y' characters, the year is
* interpreted literally, regardless of the number of digits. So using the
* pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
* <p>
* When numeric fields are adjacent directly, with no intervening delimiter
* characters, they constitute a run of adjacent numeric fields. Such runs are
* parsed specially. For example, the format "HHmmss" parses the input text
* "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and fails to
* parse "1234". In other words, the leftmost field of the run is flexible,
* while the others keep a fixed width. If the parse fails anywhere in the run,
* then the leftmost field is shortened by one character, and the entire run is
* parsed again. This is repeated until either the parse succeeds or the
* leftmost field is one character in length. If the parse still fails at that
* point, the parse of the run fails.
* <p>
* For time zones that have no names, use the strings "GMT+hours:minutes" or
* "GMT-hours:minutes".
* <p>
* The calendar defines the first day of the week, the first week of the year,
* whether hours are zero based or not (0 vs. 12 or 24) and the time zone. There
* is one common decimal format to handle all the numbers; the digit count is
* handled programmatically according to the pattern.
* <h4>Synchronization</h4> 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 Calendar
* @see GregorianCalendar
* @see java.util.TimeZone
* @see DateFormat
* @see DateFormatSymbols
* @see DecimalFormat
*/
public class SimpleDateFormat extends DateFormat {
private static final long serialVersionUID = 4774881970558875024L;
// BEGIN android-changed
static final String patternChars = "GyMdkHmsSEDFwWahKzZ"; //$NON-NLS-1$
// END android-changed
private String pattern;
private DateFormatSymbols formatData;
transient private int creationYear;
private Date defaultCenturyStart;
// BEGIN android-removed
// private transient String tzId;
//
// private transient com.ibm.icu.text.SimpleDateFormat icuFormat;
// END android-removed
/**
* Constructs a new {@code SimpleDateFormat} for formatting and parsing
* dates and times in the {@code SHORT} style for the default locale.
*/
public SimpleDateFormat() {
this(Locale.getDefault());
// BEGIN android-changed
pattern = defaultPattern();
// END android-changed
formatData = new DateFormatSymbols(Locale.getDefault());
}
/**
* Constructs a new {@code SimpleDateFormat} using the specified
* non-localized pattern and the {@code DateFormatSymbols} and {@code
* Calendar} for the default locale.
*
* @param pattern
* the pattern.
* @throws NullPointerException
* if the pattern is {@code null}.
* @throws IllegalArgumentException
* if {@code pattern} is not considered to be usable by this
* formatter.
*/
public SimpleDateFormat(String pattern) {
this(pattern, Locale.getDefault());
}
/**
* Validates the format character.
*
* @param format
* the format character
*
* @throws IllegalArgumentException
* when the format character is invalid
*/
private void validateFormat(char format) {
int index = patternChars.indexOf(format);
if (index == -1) {
// text.03=Unknown pattern character - '{0}'
throw new IllegalArgumentException(Messages.getString(
"text.03", format)); //$NON-NLS-1$
}
}
/**
* Validates the pattern.
*
* @param template
* the pattern to validate.
*
* @throws NullPointerException
* if the pattern is null
* @throws IllegalArgumentException
* if the pattern is invalid
*/
private void validatePattern(String template) {
boolean quote = false;
int next, last = -1, count = 0;
final int patternLength = template.length();
for (int i = 0; i < patternLength; i++) {
next = (template.charAt(i));
if (next == '\'') {
if (count > 0) {
validateFormat((char) last);
count = 0;
}
if (last == next) {
last = -1;
} else {
last = next;
}
quote = !quote;
continue;
}
if (!quote
&& (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
if (last == next) {
count++;
} else {
if (count > 0) {
validateFormat((char) last);
}
last = next;
count = 1;
}
} else {
if (count > 0) {
validateFormat((char) last);
count = 0;
}
last = -1;
}
}
if (count > 0) {
validateFormat((char) last);
}
if (quote) {
// text.04=Unterminated quote {0}
throw new IllegalArgumentException(Messages.getString("text.04")); //$NON-NLS-1$
}
}
/**
* Constructs a new {@code SimpleDateFormat} using the specified
* non-localized pattern and {@code DateFormatSymbols} and the {@code
* Calendar} for the default locale.
*
* @param template
* the pattern.
* @param value
* the DateFormatSymbols.
* @throws NullPointerException
* if the pattern is {@code null}.
* @throws IllegalArgumentException
* if the pattern is invalid.
*/
public SimpleDateFormat(String template, DateFormatSymbols value) {
this(Locale.getDefault());
validatePattern(template);
// BEGIN android-removed
// icuFormat = new com.ibm.icu.text.SimpleDateFormat(template, Locale.getDefault());
// icuFormat.setTimeZone(com.ibm.icu.util.TimeZone.getTimeZone(tzId));
// END android-removed
pattern = template;
formatData = (DateFormatSymbols) value.clone();
}
// BEGIN android-removed
// private void copySymbols(DateFormatSymbols value, com.ibm.icu.text.DateFormatSymbols icuSymbols) {
// icuSymbols.setAmPmStrings(value.getAmPmStrings());
// icuSymbols.setEras(value.getEras());
// icuSymbols.setLocalPatternChars(value.getLocalPatternChars());
// icuSymbols.setMonths(value.getMonths());
// icuSymbols.setShortMonths(value.getShortMonths());
// icuSymbols.setShortWeekdays(value.getShortWeekdays());
// icuSymbols.setWeekdays(value.getWeekdays());
// icuSymbols.setZoneStrings(value.getZoneStrings());
// }
// END android-removed
/**
* Constructs a new {@code SimpleDateFormat} using the specified
* non-localized pattern and the {@code DateFormatSymbols} and {@code
* Calendar} for the specified locale.
*
* @param template
* the pattern.
* @param locale
* the locale.
* @throws NullPointerException
* if the pattern is {@code null}.
* @throws IllegalArgumentException
* if the pattern is invalid.
*/
public SimpleDateFormat(String template, Locale locale) {
this(locale);
validatePattern(template);
// BEGIN android-removed
// icuFormat = new com.ibm.icu.text.SimpleDateFormat(template, locale);
// icuFormat.setTimeZone(com.ibm.icu.util.TimeZone.getTimeZone(tzId));
// END android-removed
pattern = template;
// BEGIN android-changed
formatData = new DateFormatSymbols(locale);
// END android-changed
}
// BEGIN android-removed
// SimpleDateFormat(Locale locale, com.ibm.icu.text.SimpleDateFormat icuFormat){
// this(locale);
// this.icuFormat = icuFormat;
// this.icuFormat.setTimeZone(com.ibm.icu.util.TimeZone.getTimeZone(tzId));
// pattern = (String)Format.getInternalField("pattern", icuFormat); //$NON-NLS-1$
// formatData = new DateFormatSymbols(locale);
// }
// END android-removed
private SimpleDateFormat(Locale locale) {
numberFormat = NumberFormat.getInstance(locale);
numberFormat.setParseIntegerOnly(true);
numberFormat.setGroupingUsed(false);
calendar = new GregorianCalendar(locale);
calendar.add(Calendar.YEAR, -80);
// BEGIN android-removed
// tzId = calendar.getTimeZone().getID();
// END android-removed
creationYear = calendar.get(Calendar.YEAR);
defaultCenturyStart = calendar.getTime();
}
/**
* Changes the pattern of this simple date format to the specified pattern
* which uses localized pattern characters.
*
* @param template
* the localized pattern.
*/
public void applyLocalizedPattern(String template) {
// BEGIN android-changed
pattern = convertPattern(template, formatData.getLocalPatternChars(),
patternChars, true);
// END android-changed
}
/**
* Changes the pattern of this simple date format to the specified pattern
* which uses non-localized pattern characters.
*
* @param template
* the non-localized pattern.
* @throws NullPointerException
* if the pattern is {@code null}.
* @throws IllegalArgumentException
* if the pattern is invalid.
*/
public void applyPattern(String template) {
validatePattern(template);
// BEGIN android-removed
// /*
// * ICU spec explicitly mentions that "ICU interprets a single 'y'
// * differently than Java." We need to do a trick here to follow Java
// * spec.
// */
// String templateForICU = patternForICU(template);
// icuFormat.applyPattern(templateForICU);
// END android-removed
pattern = template;
}
/**
* Converts the Java-spec pattern into an equivalent pattern used by ICU.
*
* @param p
* the Java-spec style pattern.
* @return the ICU-style pattern.
*/
@SuppressWarnings("nls")
private String patternForICU(String p) {
String[] subPatterns = p.split("'");
boolean quote = false;
boolean first = true;
StringBuilder result = new StringBuilder();
for (String subPattern : subPatterns) {
if (!quote) {
// replace 'y' with 'yy' for ICU to follow Java spec
result.append((first ? "" : "'")
+ subPattern.replaceAll("(?<!y)y(?!y)", "yy"));
first = false;
} else {
result.append("'" + subPattern);
}
quote = !quote;
}
if (p.endsWith("'")) {
result.append("'");
}
return result.toString();
}
/**
* Returns a new {@code SimpleDateFormat} with the same pattern and
* properties as this simple date format.
*
* @return a shallow copy of this simple date format.
* @see java.lang.Cloneable
*/
@Override
public Object clone() {
SimpleDateFormat clone = (SimpleDateFormat) super.clone();
clone.formatData = (DateFormatSymbols) formatData.clone();
clone.defaultCenturyStart = new Date(defaultCenturyStart.getTime());
// BEGIN android-removed
// clone.tzId = tzId;
// END android-removed
return clone;
}
// BEGIN android-added
private static String defaultPattern() {
LocaleData localeData = com.ibm.icu4jni.util.Resources.getLocaleData(Locale.getDefault());
return localeData.getDateFormat(SHORT) + " " + localeData.getTimeFormat(SHORT);
}
// END android-added
/**
* Compares the specified object with this simple date format and indicates
* if they are equal. In order to be equal, {@code object} must be an
* instance of {@code SimpleDateFormat} and have the same {@code DateFormat}
* properties, pattern, {@code DateFormatSymbols} and creation year.
*
* @param object
* the object to compare with this object.
* @return {@code true} if the specified object is equal to this simple date
* format; {@code false} otherwise.
* @see #hashCode
*/
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof SimpleDateFormat)) {
return false;
}
SimpleDateFormat simple = (SimpleDateFormat) object;
return super.equals(object) && pattern.equals(simple.pattern)
&& formatData.equals(simple.formatData);
}
/**
* Formats the specified object using the rules of this simple date format
* and returns an {@code AttributedCharacterIterator} with the formatted
* date and attributes.
*
* @param object
* the object to format.
* @return an {@code AttributedCharacterIterator} with the formatted date
* and attributes.
* @throws NullPointerException
* if the object is {@code null}.
* @throws IllegalArgumentException
* if the object cannot be formatted by this simple date
* format.
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object object) {
if (object == null) {
throw new NullPointerException();
}
if (object instanceof Date) {
return formatToCharacterIteratorImpl((Date) object);
}
if (object instanceof Number) {
return formatToCharacterIteratorImpl(new Date(((Number) object)
.longValue()));
}
throw new IllegalArgumentException();
}
private AttributedCharacterIterator formatToCharacterIteratorImpl(Date date) {
StringBuffer buffer = new StringBuffer();
Vector<FieldPosition> fields = new Vector<FieldPosition>();
// format the date, and find fields
formatImpl(date, buffer, null, fields);
// create and AttributedString with the formatted buffer
AttributedString as = new AttributedString(buffer.toString());
// add DateFormat field attributes to the AttributedString
for (int i = 0; i < fields.size(); i++) {
FieldPosition pos = fields.elementAt(i);
Format.Field attribute = pos.getFieldAttribute();
as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos
.getEndIndex());
}
// return the CharacterIterator from AttributedString
return as.getIterator();
}
/**
* Formats the date.
* <p>
* If the FieldPosition {@code field} is not null, and the field
* specified by this FieldPosition is formatted, set the begin and end index
* of the formatted field in the FieldPosition.
* <p>
* If the Vector {@code fields} is not null, find fields of this
* date, set FieldPositions with these fields, and add them to the fields
* vector.
*
* @param date
* Date to Format
* @param buffer
* StringBuffer to store the resulting formatted String
* @param field
* FieldPosition to set begin and end index of the field
* specified, if it is part of the format for this date
* @param fields
* Vector used to store the FieldPositions for each field in this
* date
* @return the formatted Date
* @throws IllegalArgumentException
* if the object cannot be formatted by this Format.
*/
private StringBuffer formatImpl(Date date, StringBuffer buffer,
FieldPosition field, Vector<FieldPosition> fields) {
boolean quote = false;
int next, last = -1, count = 0;
calendar.setTime(date);
if (field != null) {
field.clear();
}
final int patternLength = pattern.length();
for (int i = 0; i < patternLength; i++) {
next = (pattern.charAt(i));
if (next == '\'') {
if (count > 0) {
append(buffer, field, fields, (char) last, count);
count = 0;
}
if (last == next) {
buffer.append('\'');
last = -1;
} else {
last = next;
}
quote = !quote;
continue;
}
if (!quote
&& (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
if (last == next) {
count++;
} else {
if (count > 0) {
append(buffer, field, fields, (char) last, count);
}
last = next;
count = 1;
}
} else {
if (count > 0) {
append(buffer, field, fields, (char) last, count);
count = 0;
}
last = -1;
buffer.append((char) next);
}
}
if (count > 0) {
append(buffer, field, fields, (char) last, count);
}
return buffer;
}
private void append(StringBuffer buffer, FieldPosition position,
Vector<FieldPosition> fields, char format, int count) {
int field = -1;
int index = patternChars.indexOf(format);
if (index == -1) {
// text.03=Unknown pattern character - '{0}'
throw new IllegalArgumentException(Messages.getString(
"text.03", format)); //$NON-NLS-1$
}
int beginPosition = buffer.length();
Field dateFormatField = null;
switch (index) {
case ERA_FIELD:
dateFormatField = Field.ERA;
buffer.append(formatData.eras[calendar.get(Calendar.ERA)]);
break;
case YEAR_FIELD:
dateFormatField = Field.YEAR;
int year = calendar.get(Calendar.YEAR);
// BEGIN android-changed
// According to Unicode CLDR TR35(http://unicode.org/reports/tr35/) :
// If date pattern is "yy", display the last 2 digits of year.
// Otherwise, display the actual year with minimum digit count.
// Therefore, if the pattern is "y", the display value for year 1234 is '1234' not '34'.
if (count == 2) {
appendNumber(buffer, 2, year %= 100);
} else {
appendNumber(buffer, count, year);
}
// END android-changed
break;
case MONTH_FIELD:
dateFormatField = Field.MONTH;
int month = calendar.get(Calendar.MONTH);
if (count <= 2) {
appendNumber(buffer, count, month + 1);
} else if (count == 3) {
buffer.append(formatData.shortMonths[month]);
} else {
buffer.append(formatData.months[month]);
}
break;
case DATE_FIELD:
dateFormatField = Field.DAY_OF_MONTH;
field = Calendar.DATE;
break;
case HOUR_OF_DAY1_FIELD: // k
dateFormatField = Field.HOUR_OF_DAY1;
int hour = calendar.get(Calendar.HOUR_OF_DAY);
appendNumber(buffer, count, hour == 0 ? 24 : hour);
break;
case HOUR_OF_DAY0_FIELD: // H
dateFormatField = Field.HOUR_OF_DAY0;
field = Calendar.HOUR_OF_DAY;
break;
case MINUTE_FIELD:
dateFormatField = Field.MINUTE;
field = Calendar.MINUTE;
break;
case SECOND_FIELD:
dateFormatField = Field.SECOND;
field = Calendar.SECOND;
break;
case MILLISECOND_FIELD:
dateFormatField = Field.MILLISECOND;
int value = calendar.get(Calendar.MILLISECOND);
appendNumber(buffer, count, value);
break;
case DAY_OF_WEEK_FIELD:
dateFormatField = Field.DAY_OF_WEEK;
int day = calendar.get(Calendar.DAY_OF_WEEK);
if (count < 4) {
buffer.append(formatData.shortWeekdays[day]);
} else {
buffer.append(formatData.weekdays[day]);
}
break;
case DAY_OF_YEAR_FIELD:
dateFormatField = Field.DAY_OF_YEAR;
field = Calendar.DAY_OF_YEAR;
break;
case DAY_OF_WEEK_IN_MONTH_FIELD:
dateFormatField = Field.DAY_OF_WEEK_IN_MONTH;
field = Calendar.DAY_OF_WEEK_IN_MONTH;
break;
case WEEK_OF_YEAR_FIELD:
dateFormatField = Field.WEEK_OF_YEAR;
field = Calendar.WEEK_OF_YEAR;
break;
case WEEK_OF_MONTH_FIELD:
dateFormatField = Field.WEEK_OF_MONTH;
field = Calendar.WEEK_OF_MONTH;
break;
case AM_PM_FIELD:
dateFormatField = Field.AM_PM;
buffer.append(formatData.ampms[calendar.get(Calendar.AM_PM)]);
break;
case HOUR1_FIELD: // h
dateFormatField = Field.HOUR1;
hour = calendar.get(Calendar.HOUR);
appendNumber(buffer, count, hour == 0 ? 12 : hour);
break;
case HOUR0_FIELD: // K
dateFormatField = Field.HOUR0;
field = Calendar.HOUR;
break;
case TIMEZONE_FIELD: // z
dateFormatField = Field.TIME_ZONE;
appendTimeZone(buffer, count, true);
break;
// BEGIN android-changed
case (TIMEZONE_FIELD + 1): // Z
dateFormatField = Field.TIME_ZONE;
appendNumericTimeZone(buffer, false);
break;
// END android-changed
}
if (field != -1) {
appendNumber(buffer, count, calendar.get(field));
}
if (fields != null) {
position = new FieldPosition(dateFormatField);
position.setBeginIndex(beginPosition);
position.setEndIndex(buffer.length());
fields.add(position);
} else {
// Set to the first occurrence
if ((position.getFieldAttribute() == dateFormatField || (position
.getFieldAttribute() == null && position.getField() == index))
&& position.getEndIndex() == 0) {
position.setBeginIndex(beginPosition);
position.setEndIndex(buffer.length());
}
}
}
/**
* Append a representation of the time zone of 'calendar' to 'buffer'.
*
* @param count the number of z or Z characters in the format string; "zzz" would be 3,
* for example.
* @param generalTimeZone true if we should use a display name ("PDT") if available;
* false implies that we should use RFC 822 format ("-0800") instead. This corresponds to 'z'
* versus 'Z' in the format string.
*/
private void appendTimeZone(StringBuffer buffer, int count, boolean generalTimeZone) {
// BEGIN android-changed: optimized.
if (generalTimeZone) {
TimeZone tz = calendar.getTimeZone();
boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0);
int style = count < 4 ? TimeZone.SHORT : TimeZone.LONG;
if (!formatData.customZoneStrings) {
buffer.append(tz.getDisplayName(daylight, style, formatData.locale));
return;
}
// We can't call TimeZone.getDisplayName() because it would not use
// the custom DateFormatSymbols of this SimpleDateFormat.
String custom = Resources.lookupDisplayTimeZone(formatData.zoneStrings, tz.getID(), daylight, style);
if (custom != null) {
buffer.append(custom);
return;
}
}
// We didn't find what we were looking for, so default to a numeric time zone.
appendNumericTimeZone(buffer, generalTimeZone);
// END android-changed
}
// BEGIN android-added: factored out duplication.
/**
* @param generalTimeZone "GMT-08:00" rather than "-0800".
*/
private void appendNumericTimeZone(StringBuffer buffer, boolean generalTimeZone) {
int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
char sign = '+';
if (offset < 0) {
sign = '-';
offset = -offset;
}
if (generalTimeZone) {
buffer.append("GMT");
}
buffer.append(sign);
appendNumber(buffer, 2, offset / 3600000);
if (generalTimeZone) {
buffer.append(':');
}
appendNumber(buffer, 2, (offset % 3600000) / 60000);
}
// END android-added
private void appendNumber(StringBuffer buffer, int count, int value) {
int minimumIntegerDigits = numberFormat.getMinimumIntegerDigits();
numberFormat.setMinimumIntegerDigits(count);
numberFormat.format(Integer.valueOf(value), buffer, new FieldPosition(0));
numberFormat.setMinimumIntegerDigits(minimumIntegerDigits);
}
// BEGIN android-added
private Date error(ParsePosition position, int offset, TimeZone zone) {
position.setErrorIndex(offset);
calendar.setTimeZone(zone);
return null;
}
// END android-added
/**
* Formats the specified date as a string using the pattern of this date
* format and appends the string to the specified string buffer.
* <p>
* If the {@code field} member of {@code field} contains a value specifying
* a format field, then its {@code beginIndex} and {@code endIndex} members
* will be updated with the position of the first occurrence of this field
* in the formatted text.
*
* @param date
* the date to format.
* @param buffer
* the target string buffer to append the formatted date/time to.
* @param fieldPos
* on input: an optional alignment field; on output: the offsets
* of the alignment field in the formatted text.
* @return the string buffer.
* @throws IllegalArgumentException
* if there are invalid characters in the pattern.
*/
@Override
public StringBuffer format(Date date, StringBuffer buffer,
FieldPosition fieldPos) {
// BEGIN android-changed
// Harmony delegates to ICU's SimpleDateFormat, we implement it directly
return formatImpl(date, buffer, fieldPos, null);
// END android-changed
}
// BEGIN android-removed
// private com.ibm.icu.text.DateFormat.Field toICUField(
// DateFormat.Field attribute) {
// ...
// }
// END android-removed
/**
* Answers the Date which is the start of the one hundred year period for
* two digits year values.
*
* @return a Date
*/
public Date get2DigitYearStart() {
return defaultCenturyStart;
}
/**
* Returns the {@code DateFormatSymbols} used by this simple date format.
*
* @return the {@code DateFormatSymbols} object.
*/
public DateFormatSymbols getDateFormatSymbols() {
// Return a clone so the arrays in the ResourceBundle are not modified
return (DateFormatSymbols) formatData.clone();
}
@Override
public int hashCode() {
return super.hashCode() + pattern.hashCode() + formatData.hashCode()
+ creationYear;
}
// BEGIN android-added
private int parse(String string, int offset, char format, int count) {
int index = patternChars.indexOf(format);
if (index == -1) {
// text.03=Unknown pattern character - '{0}'
throw new IllegalArgumentException(Messages.getString(
"text.03", format)); //$NON-NLS-1$
}
int field = -1;
int absolute = 0;
if (count < 0) {
count = -count;
absolute = count;
}
switch (index) {
case ERA_FIELD:
return parseText(string, offset, formatData.eras, Calendar.ERA);
case YEAR_FIELD:
if (count >= 3) {
field = Calendar.YEAR;
} else {
ParsePosition position = new ParsePosition(offset);
Number result = parseNumber(absolute, string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
int year = result.intValue();
// A two digit year must be exactly two digits, i.e. 01
if ((position.getIndex() - offset) == 2 && year >= 0) {
year += creationYear / 100 * 100;
if (year < creationYear) {
year += 100;
}
}
calendar.set(Calendar.YEAR, year);
return position.getIndex();
}
break;
case MONTH_FIELD:
if (count <= 2) {
return parseNumber(absolute, string, offset,
Calendar.MONTH, -1);
}
index = parseText(string, offset, formatData.months,
Calendar.MONTH);
if (index < 0) {
return parseText(string, offset, formatData.shortMonths,
Calendar.MONTH);
}
return index;
case DATE_FIELD:
field = Calendar.DATE;
break;
case HOUR_OF_DAY1_FIELD:
ParsePosition position = new ParsePosition(offset);
Number result = parseNumber(absolute, string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
int hour = result.intValue();
if (hour == 24) {
hour = 0;
}
calendar.set(Calendar.HOUR_OF_DAY, hour);
return position.getIndex();
case HOUR_OF_DAY0_FIELD:
field = Calendar.HOUR_OF_DAY;
break;
case MINUTE_FIELD:
field = Calendar.MINUTE;
break;
case SECOND_FIELD:
field = Calendar.SECOND;
break;
case MILLISECOND_FIELD:
field = Calendar.MILLISECOND;
break;
case DAY_OF_WEEK_FIELD:
index = parseText(string, offset, formatData.weekdays,
Calendar.DAY_OF_WEEK);
if (index < 0) {
return parseText(string, offset, formatData.shortWeekdays,
Calendar.DAY_OF_WEEK);
}
return index;
case DAY_OF_YEAR_FIELD:
field = Calendar.DAY_OF_YEAR;
break;
case DAY_OF_WEEK_IN_MONTH_FIELD:
field = Calendar.DAY_OF_WEEK_IN_MONTH;
break;
case WEEK_OF_YEAR_FIELD:
field = Calendar.WEEK_OF_YEAR;
break;
case WEEK_OF_MONTH_FIELD:
field = Calendar.WEEK_OF_MONTH;
break;
case AM_PM_FIELD:
return parseText(string, offset, formatData.ampms,
Calendar.AM_PM);
case HOUR1_FIELD:
position = new ParsePosition(offset);
result = parseNumber(absolute, string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
hour = result.intValue();
if (hour == 12) {
hour = 0;
}
calendar.set(Calendar.HOUR, hour);
return position.getIndex();
case HOUR0_FIELD:
field = Calendar.HOUR;
break;
case TIMEZONE_FIELD:
return parseTimeZone(string, offset);
case (TIMEZONE_FIELD + 1):
return parseTimeZone(string, offset);
}
if (field != -1) {
return parseNumber(absolute, string, offset, field, 0);
}
return offset;
}
// END android-added
/**
* Parses a date from the specified string starting at the index specified
* by {@code position}. If the string is successfully parsed then the index
* of the {@code ParsePosition} is updated to the index following the parsed
* text. On error, the index is unchanged and the error index of {@code
* ParsePosition} is set to the index where the error occurred.
*
* @param string
* the string to parse using the pattern of this simple date
* format.
* @param position
* input/output parameter, specifies the start index in {@code
* string} from where to start parsing. If parsing is successful,
* it is updated with the index following the parsed text; on
* error, the index is unchanged and the error index is set to
* the index where the error occurred.
* @return the date resulting from the parse, or {@code null} if there is an
* error.
* @throws IllegalArgumentException
* if there are invalid characters in the pattern.
*/
@Override
public Date parse(String string, ParsePosition position) {
// BEGIN android-changed
// Harmony delegates to ICU's SimpleDateFormat, we implement it directly
boolean quote = false;
int next, last = -1, count = 0, offset = position.getIndex();
int length = string.length();
calendar.clear();
TimeZone zone = calendar.getTimeZone();
final int patternLength = pattern.length();
for (int i = 0; i < patternLength; i++) {
next = pattern.charAt(i);
if (next == '\'') {
if (count > 0) {
if ((offset = parse(string, offset, (char) last, count)) < 0) {
return error(position, -offset - 1, zone);
}
count = 0;
}
if (last == next) {
if (offset >= length || string.charAt(offset) != '\'') {
return error(position, offset, zone);
}
offset++;
last = -1;
} else {
last = next;
}
quote = !quote;
continue;
}
if (!quote
&& (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
if (last == next) {
count++;
} else {
if (count > 0) {
if ((offset = parse(string, offset, (char) last, -count)) < 0) {
return error(position, -offset - 1, zone);
}
}
last = next;
count = 1;
}
} else {
if (count > 0) {
if ((offset = parse(string, offset, (char) last, count)) < 0) {
return error(position, -offset - 1, zone);
}
count = 0;
}
last = -1;
if (offset >= length || string.charAt(offset) != next) {
return error(position, offset, zone);
}
offset++;
}
}
if (count > 0) {
if ((offset = parse(string, offset, (char) last, count)) < 0) {
return error(position, -offset - 1, zone);
}
}
Date date;
try {
date = calendar.getTime();
} catch (IllegalArgumentException e) {
return error(position, offset, zone);
}
position.setIndex(offset);
calendar.setTimeZone(zone);
return date;
// END android-changed
}
// BEGIN android-added
private Number parseNumber(int max, String string, ParsePosition position) {
int digit, length = string.length(), result = 0;
int index = position.getIndex();
if (max > 0 && max < length - index) {
length = index + max;
}
while (index < length
&& (string.charAt(index) == ' ' || string.charAt(index) == '\t')) {
index++;
}
if (max == 0) {
position.setIndex(index);
return numberFormat.parse(string, position);
}
while (index < length
&& (digit = Character.digit(string.charAt(index), 10)) != -1) {
index++;
result = result * 10 + digit;
}
if (index == position.getIndex()) {
position.setErrorIndex(index);
return null;
}
position.setIndex(index);
return Integer.valueOf(result);
}
private int parseNumber(int max, String string, int offset, int field,
int skew) {
ParsePosition position = new ParsePosition(offset);
Number result = parseNumber(max, string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
calendar.set(field, result.intValue() + skew);
return position.getIndex();
}
private int parseText(String string, int offset, String[] text, int field) {
int found = -1;
for (int i = 0; i < text.length; i++) {
if (text[i].length() == 0) {
continue;
}
if (string
.regionMatches(true, offset, text[i], 0, text[i].length())) {
// Search for the longest match, in case some fields are subsets
if (found == -1 || text[i].length() > text[found].length()) {
found = i;
}
}
}
if (found != -1) {
calendar.set(field, found);
return offset + text[found].length();
}
return -offset - 1;
}
private int parseTimeZone(String string, int offset) {
// BEGIN android-changed
String[][] zones = formatData.internalZoneStrings();
// END android-changed
boolean foundGMT = string.regionMatches(offset, "GMT", 0, 3); //$NON-NLS-1$
if (foundGMT) {
offset += 3;
}
char sign;
if (offset < string.length()
&& ((sign = string.charAt(offset)) == '+' || sign == '-')) {
ParsePosition position = new ParsePosition(offset + 1);
Number result = numberFormat.parse(string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
int hour = result.intValue();
int raw = hour * 3600000;
int index = position.getIndex();
if (index < string.length() && string.charAt(index) == ':') {
position.setIndex(index + 1);
result = numberFormat.parse(string, position);
if (result == null) {
return -position.getErrorIndex() - 1;
}
int minute = result.intValue();
raw += minute * 60000;
} else if (hour >= 24) {
raw = (hour / 100 * 3600000) + (hour % 100 * 60000);
}
if (sign == '-') {
raw = -raw;
}
calendar.setTimeZone(new SimpleTimeZone(raw, "")); //$NON-NLS-1$
return position.getIndex();
}
if (foundGMT) {
calendar.setTimeZone(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
return offset;
}
for (String[] element : zones) {
for (int j = 1; j < 5; j++) {
if (string.regionMatches(true, offset, element[j], 0,
element[j].length())) {
TimeZone zone = TimeZone.getTimeZone(element[0]);
if (zone == null) {
return -offset - 1;
}
int raw = zone.getRawOffset();
if (j >= 3 && zone.useDaylightTime()) {
raw += 3600000;
}
calendar.setTimeZone(new SimpleTimeZone(raw, "")); //$NON-NLS-1$
return offset + element[j].length();
}
}
}
return -offset - 1;
}
// END android-added
/**
* Sets the date which is the start of the one hundred year period for two
* digits year values.
*
* @param date
* the new date.
*/
public void set2DigitYearStart(Date date) {
// BEGIN android-removed
// icuFormat.set2DigitYearStart(date);
// END android-removed
defaultCenturyStart = date;
Calendar cal = new GregorianCalendar();
cal.setTime(date);
creationYear = cal.get(Calendar.YEAR);
}
/**
* Sets the {@code DateFormatSymbols} used by this simple date format.
*
* @param value
* the new {@code DateFormatSymbols} object.
*/
public void setDateFormatSymbols(DateFormatSymbols value) {
// BEGIN android-removed
// com.ibm.icu.text.DateFormatSymbols icuSymbols = new com.ibm.icu.text.DateFormatSymbols();
// copySymbols(value, icuSymbols);
// icuFormat.setDateFormatSymbols(icuSymbols);
// END android-removed
formatData = (DateFormatSymbols) value.clone();
}
/**
* Returns the pattern of this simple date format using localized pattern
* characters.
*
* @return the localized pattern.
*/
public String toLocalizedPattern() {
// BEGIN android-changed
return convertPattern(pattern, patternChars, formatData
.getLocalPatternChars(), false);
// END android-changed
}
/**
* Returns the pattern of this simple date format using non-localized
* pattern characters.
*
* @return the non-localized pattern.
*/
public String toPattern() {
return pattern;
}
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("defaultCenturyStart", Date.class), //$NON-NLS-1$
new ObjectStreamField("formatData", DateFormatSymbols.class), //$NON-NLS-1$
new ObjectStreamField("pattern", String.class), //$NON-NLS-1$
new ObjectStreamField("serialVersionOnStream", Integer.TYPE), }; //$NON-NLS-1$
private void writeObject(ObjectOutputStream stream) throws IOException {
ObjectOutputStream.PutField fields = stream.putFields();
fields.put("defaultCenturyStart", defaultCenturyStart); //$NON-NLS-1$
fields.put("formatData", formatData); //$NON-NLS-1$
fields.put("pattern", pattern); //$NON-NLS-1$
fields.put("serialVersionOnStream", 1); //$NON-NLS-1$
stream.writeFields();
}
private void readObject(ObjectInputStream stream) throws IOException,
ClassNotFoundException {
ObjectInputStream.GetField fields = stream.readFields();
int version = fields.get("serialVersionOnStream", 0); //$NON-NLS-1$
Date date;
if (version > 0) {
date = (Date) fields.get("defaultCenturyStart", new Date()); //$NON-NLS-1$
} else {
date = new Date();
}
set2DigitYearStart(date);
formatData = (DateFormatSymbols) fields.get("formatData", null); //$NON-NLS-1$
pattern = (String) fields.get("pattern", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
}