/* * 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.TimeZones; /** * A concrete class for formatting and parsing 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}. * *
You can supply a 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. * *
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. * *
Symbol | Meaning | Presentation | Example |
{@code D} | day in year | (Number) | 189 |
{@code E} | day of week | (Text) | Tuesday |
{@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/Number) | July / 07 |
{@code M} | month in year | (Text/Number) | July / 07 |
{@code S} | fractional seconds | (Number) | 978 |
{@code W} | week in month | (Number) | 2 |
{@code Z} | time zone (RFC 822) | (Timezone) | -0800 |
{@code a} | am/pm marker | (Text) | PM |
{@code c} | stand-alone day of week | (Text/Number) | Tuesday / 2 |
{@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) | 2010 |
{@code z} | time zone | (Timezone) | Pacific Standard Time |
{@code '} | escape for text | (Delimiter) | 'Date=' |
{@code ''} | single quote | (Literal) | 'o''clock' |
The number of consecutive copies (the "count") of a pattern character further influences * the format. *
Years are handled specially: {@code yy} truncates to the last 2 digits, but any * other number of consecutive {@code y}s does not truncate. So where {@code yyyy} or * {@code y} might give {@code 2010}, {@code yy} would give {@code 10}. * *
Fractional seconds are also 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. These are necessary for correct localization in languages such as Russian * that distinguish between, say, "June" and "June 2010". * *
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. * *
See {@link #set2DigitYearStart} for more about handling two-digit years. * *
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.err.println(df.format(new Date(0))); * } ** *
Produces this output when run on an {@code en_US} device in the PDT 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.err.format("%30s %s\n", format, sdf.format(new Date(0))); * sdf.setTimeZone(TimeZone.getTimeZone("UTC")); * System.err.format("%30s %s\n", format, sdf.format(new Date(0))); * } ** *
Which produces this output when run in the PDT 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). * *
* 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
* 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,
formatData.longStandAloneMonths, formatData.shortStandAloneMonths);
case MONTH_FIELD: // M
return parseMonth(string, offset, count, absolute,
formatData.months, formatData.shortMonths);
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, formatData.longStandAloneWeekdays, formatData.shortStandAloneWeekdays);
case DAY_OF_WEEK_FIELD:
return parseDayOfWeek(string, offset, formatData.weekdays, formatData.shortWeekdays);
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, String[] longs, String[] shorts) {
int index = parseText(string, offset, longs, Calendar.DAY_OF_WEEK);
if (index < 0) {
index = parseText(string, offset, shorts, Calendar.DAY_OF_WEEK);
}
return index;
}
private int parseMonth(String string, int offset, int count, int absolute, String[] longs, String[] shorts) {
if (count <= 2) {
return parseNumber(absolute, string, offset, Calendar.MONTH, -1);
}
int index = parseText(string, offset, longs, Calendar.MONTH);
if (index < 0) {
index = parseText(string, offset, shorts, 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 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].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;
}
String[][] zones = formatData.internalZoneStrings();
for (String[] element : zones) {
for (int j = TimeZones.LONG_NAME; j < TimeZones.NAME_COUNT; j++) {
if (string.regionMatches(true, offset, element[j], 0, element[j].length())) {
TimeZone zone = TimeZone.getTimeZone(element[TimeZones.OLSON_NAME]);
if (zone == null) {
return -offset - 1;
}
int raw = zone.getRawOffset();
if (j == TimeZones.LONG_NAME_DST || j == TimeZones.SHORT_NAME_DST) {
/*
* TODO, http://b/4723412
* We can't use TimeZone#getDSTSavings here because that
* will return 0 if the zone no longer uses DST. We
* should change this to use TimeZone.getOffset(long),
* which requires the complete date to be parsed first.
*/
raw += 3600000;
}
calendar.setTimeZone(new SimpleTimeZone(raw, ""));
return offset + element[j].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", "");
}
}