/* * 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. */ package java.text; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamField; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.SimpleTimeZone; import java.util.TimeZone; import libcore.icu.LocaleData; import libcore.icu.TimeZoneNames; /** * Formats and parses dates in a locale-sensitive manner. Formatting turns a {@link Date} into * a {@link String}, and parsing turns a {@code String} into a {@code Date}. * *

Time Pattern Syntax

*

You can supply a Unicode UTS #35 * pattern describing what strings are produced/accepted, but almost all * callers should use {@link DateFormat#getDateInstance}, {@link DateFormat#getDateTimeInstance}, * or {@link DateFormat#getTimeInstance} to get a ready-made instance suitable for the user's * locale. In cases where the system does not provide a suitable pattern, see * {@link android.text.format.DateFormat#getBestDateTimePattern} which lets you specify * the elements you'd like in a pattern and get back a pattern suitable for any given locale. * *

The main reason you'd create an instance this class directly is because you need to * format/parse a specific machine-readable format, in which case you almost certainly want * to explicitly ask for {@link Locale#US} to ensure that you get ASCII digits (rather than, * say, Arabic digits). * (See "Be wary of the default locale".) * The most useful non-localized pattern is {@code "yyyy-MM-dd HH:mm:ss.SSSZ"}, which corresponds * to the ISO 8601 international standard date format. * *

To specify the time format, use a time pattern string. In this * string, any character from {@code 'A'} to {@code 'Z'} or {@code 'a'} to {@code 'z'} is * treated specially. All other characters are passed through verbatim. The interpretation of each * of the ASCII letters is given in the table below. ASCII letters not appearing in the table are * reserved for future use, and it is an error to attempt to use them. * *

The number of consecutive copies (the "count") of a pattern character further influences * the format, as shown in the table. For fields of kind "number", the count is the minimum number * of digits; shorter values are zero-padded to the given width and longer values overflow it. * *

* * * * * * * * * * * * * * * * * * * * * * * * * *
Symbol Meaning Kind Example
{@code D} day in year (Number) 189
{@code E} day of week (Text) {@code E}/{@code EE}/{@code EEE}:Tue, {@code EEEE}:Tuesday, {@code EEEEE}:T
{@code F} day of week in month (Number) 2 (2nd Wed in July)
{@code G} era designator (Text) AD
{@code H} hour in day (0-23) (Number) 0
{@code K} hour in am/pm (0-11) (Number) 0
{@code L} stand-alone month (Text) {@code L}:1 {@code LL}:01 {@code LLL}:Jan {@code LLLL}:January {@code LLLLL}:J
{@code M} month in year (Text) {@code M}:1 {@code MM}:01 {@code MMM}:Jan {@code MMMM}:January {@code MMMMM}:J
{@code S} fractional seconds (Number) 978
{@code W} week in month (Number) 2
{@code Z} time zone (RFC 822) (Time Zone) {@code Z}/{@code ZZ}/{@code ZZZ}:-0800 {@code ZZZZ}:GMT-08:00 {@code ZZZZZ}:-08:00
{@code a} am/pm marker (Text) PM
{@code c} stand-alone day of week (Text) {@code c}/{@code cc}/{@code ccc}:Tue, {@code cccc}:Tuesday, {@code ccccc}:T
{@code d} day in month (Number) 10
{@code h} hour in am/pm (1-12) (Number) 12
{@code k} hour in day (1-24) (Number) 24
{@code m} minute in hour (Number) 30
{@code s} second in minute (Number) 55
{@code w} week in year (Number) 27
{@code y} year (Number) {@code yy}:10 {@code y}/{@code yyy}/{@code yyyy}:2010
{@code z} time zone (Timezone) {@code z}/{@code zz}/{@code zzz}:PST {@code zzzz}:Pacific Standard Time
{@code '} escape for text (Delimiter) {@code 'Date='}:Date=
{@code ''} single quote (Literal) {@code 'o''clock'}:o'clock
* *

Fractional seconds are handled specially: they're zero-padded on the right. * *

The two pattern characters {@code L} and {@code c} are ICU-compatible extensions, not * available in the RI or in Android before Android 2.3 "Gingerbread" (API level 9). These * extensions are necessary for correct localization in languages such as Russian * that make a grammatical distinction between, say, the word "June" in the sentence "June" and * in the sentence "June 10th"; the former is the stand-alone form, the latter the regular * form (because the usual case is to format a complete date). The relationship between {@code E} * and {@code c} is equivalent, but for weekday names. * *

Five-count patterns (such as "MMMMM") used for the shortest non-numeric * representation of a field were introduced in Jelly Bean MR2 (API level 18). * *

When two numeric fields are directly adjacent 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. * *

See {@link #set2DigitYearStart} for more about handling two-digit years. * *

Sample Code

*

If you're formatting for human use, you should use an instance returned from * {@link DateFormat} as described above. This code: *

 * DateFormat[] formats = new DateFormat[] {
 *   DateFormat.getDateInstance(),
 *   DateFormat.getDateTimeInstance(),
 *   DateFormat.getTimeInstance(),
 * };
 * for (DateFormat df : formats) {
 *   System.out.println(df.format(new Date(0)));
 * }
 * 
* *

Produces this output when run on an {@code en_US} device in the America/Los_Angeles time zone: *

 * Dec 31, 1969
 * Dec 31, 1969 4:00:00 PM
 * 4:00:00 PM
 * 
* And will produce similarly appropriate localized human-readable output on any user's system. * *

If you're formatting for machine use, consider this code: *

 * String[] formats = new String[] {
 *   "yyyy-MM-dd",
 *   "yyyy-MM-dd HH:mm",
 *   "yyyy-MM-dd HH:mmZ",
 *   "yyyy-MM-dd HH:mm:ss.SSSZ",
 *   "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
 * };
 * for (String format : formats) {
 *   SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
 *   System.out.format("%30s %s\n", format, sdf.format(new Date(0)));
 *   sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
 *   System.out.format("%30s %s\n", format, sdf.format(new Date(0)));
 * }
 * 
* *

Which produces this output when run in the America/Los_Angeles time zone: *

 *                     yyyy-MM-dd 1969-12-31
 *                     yyyy-MM-dd 1970-01-01
 *               yyyy-MM-dd HH:mm 1969-12-31 16:00
 *               yyyy-MM-dd HH:mm 1970-01-01 00:00
 *              yyyy-MM-dd HH:mmZ 1969-12-31 16:00-0800
 *              yyyy-MM-dd HH:mmZ 1970-01-01 00:00+0000
 *       yyyy-MM-dd HH:mm:ss.SSSZ 1969-12-31 16:00:00.000-0800
 *       yyyy-MM-dd HH:mm:ss.SSSZ 1970-01-01 00:00:00.000+0000
 *     yyyy-MM-dd'T'HH:mm:ss.SSSZ 1969-12-31T16:00:00.000-0800
 *     yyyy-MM-dd'T'HH:mm:ss.SSSZ 1970-01-01T00:00:00.000+0000
 * 
* *

As this example shows, each {@code SimpleDateFormat} instance has a {@link TimeZone}. * This is because it's called upon to format instances of {@code Date}, which represents an * absolute time in UTC. That is, {@code Date} does not carry time zone information. * By default, {@code SimpleDateFormat} will use the system's default time zone. This is * appropriate for human-readable output (for which, see the previous sample instead), but * generally inappropriate for machine-readable output, where ambiguity is a problem. Note that * in this example, the output that included a time but no time zone cannot be parsed back into * the original {@code Date}. For this * reason it is almost always necessary and desirable to include the timezone in the output. * It may also be desirable to set the formatter's time zone to UTC (to ease comparison, or to * make logs more readable, for example). It is often best to avoid formatting completely when * writing dates/times in machine-readable form. Simply sending the "Unix time" as a {@code long} * or as the string corresponding to the long is cheaper and unambiguous, and can be formatted any * way the recipient deems appropriate. * *

Synchronization

* {@code SimpleDateFormat} is not thread-safe. Users should create a separate instance for * each thread. * * @see java.util.Calendar * @see java.util.Date * @see java.util.TimeZone * @see java.text.DateFormat */ public class SimpleDateFormat extends DateFormat { private static final long serialVersionUID = 4774881970558875024L; // 'L' and 'c' are ICU-compatible extensions for stand-alone month and stand-alone weekday. static final String PATTERN_CHARS = "GyMdkHmsSEDFwWahKzZLc"; // The index of 'Z' in the PATTERN_CHARS string. This pattern character is supported by the RI, // but has no corresponding public constant. private static final int RFC_822_TIMEZONE_FIELD = 18; // The index of 'L' (cf. 'M') in the PATTERN_CHARS string. This is an ICU-compatible extension // necessary for correct localization in various languages (http://b/2633414). private static final int STAND_ALONE_MONTH_FIELD = 19; // The index of 'c' (cf. 'E') in the PATTERN_CHARS string. This is an ICU-compatible extension // necessary for correct localization in various languages (http://b/2633414). private static final int STAND_ALONE_DAY_OF_WEEK_FIELD = 20; private String pattern; private DateFormatSymbols formatData; transient private int creationYear; private Date defaultCenturyStart; /** * Constructs a new {@code SimpleDateFormat} for formatting and parsing * dates and times in the {@code SHORT} style for the user's default locale. * See "Be wary of the default locale". */ public SimpleDateFormat() { this(Locale.getDefault()); this.pattern = defaultPattern(); this.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 user's default locale. * See "Be wary of 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 = PATTERN_CHARS.indexOf(format); if (index == -1) { throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); } } /** * 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) { throw new IllegalArgumentException("Unterminated quote"); } } /** * Constructs a new {@code SimpleDateFormat} using the specified * non-localized pattern and {@code DateFormatSymbols} and the {@code * Calendar} for the user's default locale. * See "Be wary of 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); pattern = template; formatData = (DateFormatSymbols) value.clone(); } /** * 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); pattern = template; formatData = new DateFormatSymbols(locale); } private SimpleDateFormat(Locale locale) { numberFormat = NumberFormat.getInstance(locale); numberFormat.setParseIntegerOnly(true); numberFormat.setGroupingUsed(false); calendar = new GregorianCalendar(locale); calendar.add(Calendar.YEAR, -80); 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) { pattern = convertPattern(template, formatData.getLocalPatternChars(), PATTERN_CHARS, true); } /** * 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); pattern = template; } /** * Returns a new {@code SimpleDateFormat} with the same pattern and * properties as this simple date format. */ @Override public Object clone() { SimpleDateFormat clone = (SimpleDateFormat) super.clone(); clone.formatData = (DateFormatSymbols) formatData.clone(); clone.defaultCenturyStart = new Date(defaultCenturyStart.getTime()); return clone; } private static String defaultPattern() { LocaleData localeData = LocaleData.get(Locale.getDefault()); return localeData.getDateFormat(SHORT) + " " + localeData.getTimeFormat(SHORT); } /** * 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("object == null"); } if (object instanceof Date) { return formatToCharacterIteratorImpl((Date) object); } if (object instanceof Number) { return formatToCharacterIteratorImpl(new Date(((Number) object).longValue())); } throw new IllegalArgumentException("Bad class: " + object.getClass()); } private AttributedCharacterIterator formatToCharacterIteratorImpl(Date date) { StringBuffer buffer = new StringBuffer(); ArrayList fields = new ArrayList(); // 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 (FieldPosition pos : fields) { Format.Field attribute = pos.getFieldAttribute(); as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos.getEndIndex()); } // return the CharacterIterator from AttributedString return as.getIterator(); } /** * Formats the date. *

* 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. *

* If the list {@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 * list 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, List 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, List fields, char format, int count) { int field = -1; int index = PATTERN_CHARS.indexOf(format); if (index == -1) { throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); } 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); /* * For 'y' and 'yyy', we're consistent with Unicode and previous releases * of Android. But this means we're inconsistent with the RI. * http://unicode.org/reports/tr35/ */ if (count == 2) { appendNumber(buffer, 2, year % 100); } else { appendNumber(buffer, count, year); } break; case STAND_ALONE_MONTH_FIELD: // 'L' dateFormatField = Field.MONTH; appendMonth(buffer, count, true); break; case MONTH_FIELD: // 'M' dateFormatField = Field.MONTH; appendMonth(buffer, count, false); 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 STAND_ALONE_DAY_OF_WEEK_FIELD: dateFormatField = Field.DAY_OF_WEEK; appendDayOfWeek(buffer, count, true); break; case DAY_OF_WEEK_FIELD: dateFormatField = Field.DAY_OF_WEEK; appendDayOfWeek(buffer, count, false); 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; case RFC_822_TIMEZONE_FIELD: // 'Z' dateFormatField = Field.TIME_ZONE; appendNumericTimeZone(buffer, count, false); break; } 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()); } } } // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. private void appendDayOfWeek(StringBuffer buffer, int count, boolean standAlone) { String[] days; LocaleData ld = formatData.localeData; if (count == 4) { days = standAlone ? ld.longStandAloneWeekdayNames : formatData.weekdays; } else if (count == 5) { days = standAlone ? ld.tinyStandAloneWeekdayNames : formatData.localeData.tinyWeekdayNames; } else { days = standAlone ? ld.shortStandAloneWeekdayNames : formatData.shortWeekdays; } buffer.append(days[calendar.get(Calendar.DAY_OF_WEEK)]); } // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. private void appendMonth(StringBuffer buffer, int count, boolean standAlone) { int month = calendar.get(Calendar.MONTH); if (count <= 2) { appendNumber(buffer, count, month + 1); return; } String[] months; LocaleData ld = formatData.localeData; if (count == 4) { months = standAlone ? ld.longStandAloneMonthNames : formatData.months; } else if (count == 5) { months = standAlone ? ld.tinyStandAloneMonthNames : ld.tinyMonthNames; } else { months = standAlone ? ld.shortStandAloneMonthNames : formatData.shortMonths; } buffer.append(months[month]); } /** * 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) { 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 = TimeZoneNames.getDisplayName(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, count, generalTimeZone); } // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. // @param generalTimeZone "GMT-08:00" rather than "-0800". private void appendNumericTimeZone(StringBuffer buffer, int count, boolean generalTimeZone) { int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); char sign = '+'; if (offset < 0) { sign = '-'; offset = -offset; } if (generalTimeZone || count == 4) { buffer.append("GMT"); } buffer.append(sign); appendNumber(buffer, 2, offset / 3600000); if (generalTimeZone || count >= 4) { buffer.append(':'); } appendNumber(buffer, 2, (offset % 3600000) / 60000); } private void appendNumber(StringBuffer buffer, int count, int value) { // TODO: we could avoid using the NumberFormat in most cases for a significant speedup. // The only problem is that we expose the NumberFormat to third-party code, so we'd have // some work to do to work out when the optimization is valid. int minimumIntegerDigits = numberFormat.getMinimumIntegerDigits(); numberFormat.setMinimumIntegerDigits(count); numberFormat.format(Integer.valueOf(value), buffer, new FieldPosition(0)); numberFormat.setMinimumIntegerDigits(minimumIntegerDigits); } private Date error(ParsePosition position, int offset, TimeZone zone) { position.setErrorIndex(offset); calendar.setTimeZone(zone); return null; } /** * Formats the specified date as a string using the pattern of this date * format and appends the string to the specified string buffer. *

* 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) { // Harmony delegates to ICU's SimpleDateFormat, we implement it directly return formatImpl(date, buffer, fieldPos, null); } /** * Returns the date which is the start of the one hundred year period for two-digit year values. * See {@link #set2DigitYearStart} for details. */ public Date get2DigitYearStart() { return (Date) defaultCenturyStart.clone(); } /** * Returns the {@code DateFormatSymbols} used by this simple date format. * * @return the {@code DateFormatSymbols} object. */ public DateFormatSymbols getDateFormatSymbols() { return (DateFormatSymbols) formatData.clone(); } @Override public int hashCode() { return super.hashCode() + pattern.hashCode() + formatData.hashCode() + creationYear; } private int parse(String string, int offset, char format, int count) { int index = PATTERN_CHARS.indexOf(format); if (index == -1) { throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); } int field = -1; // TODO: what's 'absolute' for? when is 'count' negative, and why? 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 STAND_ALONE_MONTH_FIELD: // 'L' return parseMonth(string, offset, count, absolute, true); case MONTH_FIELD: // 'M' return parseMonth(string, offset, count, absolute, false); case DATE_FIELD: field = Calendar.DATE; break; case HOUR_OF_DAY1_FIELD: // 'k' 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: // 'H' 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 STAND_ALONE_DAY_OF_WEEK_FIELD: return parseDayOfWeek(string, offset, true); case DAY_OF_WEEK_FIELD: return parseDayOfWeek(string, offset, false); 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: // 'h' 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: // 'K' field = Calendar.HOUR; break; case TIMEZONE_FIELD: // 'z' return parseTimeZone(string, offset); case RFC_822_TIMEZONE_FIELD: // 'Z' return parseTimeZone(string, offset); } if (field != -1) { return parseNumber(absolute, string, offset, field, 0); } return offset; } private int parseDayOfWeek(String string, int offset, boolean standAlone) { LocaleData ld = formatData.localeData; int index = parseText(string, offset, standAlone ? ld.longStandAloneWeekdayNames : formatData.weekdays, Calendar.DAY_OF_WEEK); if (index < 0) { index = parseText(string, offset, standAlone ? ld.shortStandAloneWeekdayNames : formatData.shortWeekdays, Calendar.DAY_OF_WEEK); } return index; } private int parseMonth(String string, int offset, int count, int absolute, boolean standAlone) { if (count <= 2) { return parseNumber(absolute, string, offset, Calendar.MONTH, -1); } LocaleData ld = formatData.localeData; int index = parseText(string, offset, standAlone ? ld.longStandAloneMonthNames : formatData.months, Calendar.MONTH); if (index < 0) { index = parseText(string, offset, standAlone ? ld.shortStandAloneMonthNames : formatData.shortMonths, Calendar.MONTH); } return index; } /** * 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) { // 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; } private Number parseNumber(int max, String string, ParsePosition position) { int length = string.length(); 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); Number n = numberFormat.parse(string, position); // In RTL locales, NumberFormat might have parsed "2012-" in an ISO date as the // negative number -2012. // Ideally, we wouldn't have this broken API that exposes a NumberFormat and expects // us to use it. The next best thing would be a way to ask the NumberFormat to parse // positive numbers only, but icu4c supports negative (BCE) years. The best we can do // is try to recognize when icu4c has done this, and undo it. if (n != null && n.longValue() < 0) { if (numberFormat instanceof DecimalFormat) { DecimalFormat df = (DecimalFormat) numberFormat; char lastChar = string.charAt(position.getIndex() - 1); char minusSign = df.getDecimalFormatSymbols().getMinusSign(); if (lastChar == minusSign) { n = Long.valueOf(-n.longValue()); // Make the value positive. position.setIndex(position.getIndex() - 1); // Spit out the negative sign. } } } return n; } int result = 0; int digit; while (index < length && (digit = Character.digit(string.charAt(index), 10)) != -1) { result = result * 10 + digit; ++index; } 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].isEmpty()) { 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) { boolean foundGMT = string.regionMatches(offset, "GMT", 0, 3); 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, "")); return position.getIndex(); } if (foundGMT) { calendar.setTimeZone(TimeZone.getTimeZone("GMT")); return offset; } for (String[] row : formatData.internalZoneStrings()) { for (int i = TimeZoneNames.LONG_NAME; i < TimeZoneNames.NAME_COUNT; ++i) { if (row[i] == null) { // If icu4c doesn't have a name, our array contains a null. Normally we'd // work out the correct GMT offset, but we already handled parsing GMT offsets // above, so we can just ignore these cases. http://b/8128460. continue; } if (string.regionMatches(true, offset, row[i], 0, row[i].length())) { TimeZone zone = TimeZone.getTimeZone(row[TimeZoneNames.OLSON_NAME]); if (zone == null) { return -offset - 1; } int raw = zone.getRawOffset(); if (i == TimeZoneNames.LONG_NAME_DST || i == TimeZoneNames.SHORT_NAME_DST) { // Not all time zones use a one-hour difference, so we need to query // the TimeZone. (Australia/Lord_Howe is the usual example of this.) int dstSavings = zone.getDSTSavings(); // One problem with TimeZone.getDSTSavings is that it will return 0 if the // time zone has stopped using DST, even if we're parsing a date from // the past. In that case, assume the default. if (dstSavings == 0) { // TODO: we should change this to use TimeZone.getOffset(long), // but that requires the complete date to be parsed first. dstSavings = 3600000; } raw += dstSavings; } calendar.setTimeZone(new SimpleTimeZone(raw, "")); return offset + row[i].length(); } } } return -offset - 1; } /** * Sets the date which is the start of the one hundred year period for two-digit year values. * *

When parsing a date string using the abbreviated year pattern {@code 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 was created. For * example, using a pattern of {@code MM/dd/yy}, an * instance created on Jan 1, 1997 would interpret the string {@code "01/11/12"} * as Jan 11, 2012 but interpret the string {@code "05/04/64"} 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, {@code "-1"}), is interpreted literally. So using the same pattern, both * {@code "01/02/3"} and {@code "01/02/003"} are parsed as Jan 2, 3 AD. * Similarly, {@code "01/02/-3"} is parsed as Jan 2, 4 BC. * *

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 {@code MM/dd/yyyy}, {@code "01/11/12"} is parsed as Jan 11, 12 A.D. */ public void set2DigitYearStart(Date date) { defaultCenturyStart = (Date) date.clone(); Calendar cal = new GregorianCalendar(); cal.setTime(defaultCenturyStart); 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) { formatData = (DateFormatSymbols) value.clone(); } /** * Returns the pattern of this simple date format using localized pattern * characters. * * @return the localized pattern. */ public String toLocalizedPattern() { return convertPattern(pattern, PATTERN_CHARS, formatData.getLocalPatternChars(), false); } private static String convertPattern(String template, String fromChars, String toChars, boolean check) { if (!check && fromChars.equals(toChars)) { return template; } boolean quote = false; StringBuilder output = new StringBuilder(); int length = template.length(); for (int i = 0; i < length; i++) { int index; char next = template.charAt(i); if (next == '\'') { quote = !quote; } if (!quote && (index = fromChars.indexOf(next)) != -1) { output.append(toChars.charAt(index)); } else if (check && !quote && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) { throw new IllegalArgumentException("Invalid pattern character '" + next + "' in " + "'" + template + "'"); } else { output.append(next); } } if (quote) { throw new IllegalArgumentException("Unterminated quote"); } return output.toString(); } /** * 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), new ObjectStreamField("formatData", DateFormatSymbols.class), new ObjectStreamField("pattern", String.class), new ObjectStreamField("serialVersionOnStream", int.class), }; private void writeObject(ObjectOutputStream stream) throws IOException { ObjectOutputStream.PutField fields = stream.putFields(); fields.put("defaultCenturyStart", defaultCenturyStart); fields.put("formatData", formatData); fields.put("pattern", pattern); fields.put("serialVersionOnStream", 1); stream.writeFields(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = stream.readFields(); int version = fields.get("serialVersionOnStream", 0); Date date; if (version > 0) { date = (Date) fields.get("defaultCenturyStart", new Date()); } else { date = new Date(); } set2DigitYearStart(date); formatData = (DateFormatSymbols) fields.get("formatData", null); pattern = (String) fields.get("pattern", ""); } }