org.gwtproject.i18n.shared.DateTimeFormat Maven / Gradle / Ivy
Show all versions of gwt-datetimeformat Show documentation
/*
* Copyright © 2018 The GWT Authors
*
* Licensed 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 org.gwtproject.i18n.shared;
import java.util.*;
import org.gwtproject.i18n.shared.cldr.DateTimeFormatInfo;
import org.gwtproject.i18n.shared.cldr.impl.DateTimeFormatInfo_factory;
import org.gwtproject.i18n.shared.cldr.impl.DefaultDateTimeFormatInfo;
import org.gwtproject.i18n.shared.impl.DateRecord;
/**
* Formats and parses dates and times using locale-sensitive patterns.
*
* Patterns
*
*
*
* Symbol
* Meaning
* Presentation
* Example
*
*
*
* G
* era designator
* Text
* AD
*
*
*
* y
* year
* Number
* 1996
*
*
*
* L
* standalone month in year
* Text or Number
* July (or) 07
*
*
*
* M
* month in year
* Text or Number
* July (or) 07
*
*
*
* d
* day in month
* Number
* 10
*
*
*
* h
* hour in am/pm (1-12)
* Number
* 12
*
*
*
* H
* hour in day (0-23)
* Number
* 0
*
*
*
* m
* minute in hour
* Number
* 30
*
*
*
* s
* second in minute
* Number
* 55
*
*
*
* S
* fractional second
* Number
* 978
*
*
*
* E
* day of week
* Text
* Tuesday
*
*
*
* c
* standalone day of week
* Text
* Tuesday
*
*
*
* a
* am/pm marker
* Text
* PM
*
*
*
* k
* hour in day (1-24)
* Number
* 24
*
*
*
* K
* hour in am/pm (0-11)
* Number
* 0
*
*
*
* z
* time zone
* Text
* Pacific Standard Time(see comment)
*
*
*
* Z
* time zone (RFC 822)
* Text
* -0800(See comment)
*
*
*
* v
* time zone id
* Text
* America/Los_Angeles(See comment)
*
*
*
* '
* escape for text
* Delimiter
* 'Date='
*
*
*
* ''
* single quote
* Literal
* 'o''clock'
*
*
*
* The number of pattern letters influences the format, as follows:
*
*
* - Text
*
- if 4 or more, then use the full form; if less than 4, use short or abbreviated form if it
* exists (e.g.,
"EEEE"
produces "Monday"
, "EEE"
* produces "Mon"
)
* - Number
*
- the minimum number of digits. Shorter numbers are zero-padded to this amount (e.g. if
*
"m"
produces "6"
, "mm"
produces "06"
).
* Year is handled specially; that is, if the count of 'y' is 2, the Year will be truncated to
* 2 digits. (e.g., if "yyyy"
produces "1997"
, "yy"
* produces "97"
.) Unlike other fields, fractional seconds are padded on the
* right with zero.
* - Text or Number
*
- 3 or more, use text, otherwise use number. (e.g.
"M"
produces "1"
* , "MM"
produces "01"
, "MMM"
produces "Jan"
*
, and "MMMM"
produces "January"
. Some pattern letters also
* treat a count of 5 specially, meaning a single-letter abbreviation: L
, M
*
, E
, and c
.
*
*
* 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 ':
', ' .
', '
' (space), '#
'
* and ' @
' will appear in the resulting time text even they are not embraced within
* single quotes.
*
*
[Time Zone Handling] Web browsers don't provide all the information we need for proper time
* zone formating -- so GWT has a copy of the required data, for your convenience. For simpler
* cases, one can also use a fallback implementation that only keeps track of the current timezone
* offset. These two approaches are called, respectively, Common TimeZones and Simple TimeZones,
* although both are implemented with the same TimeZone class.
*
*
"TimeZone createTimeZone(String timezoneData)" returns a Common TimeZone object, and "TimeZone
* createTimeZone(int timeZoneOffsetInMinutes)" returns a Simple TimeZone object. The one provided
* by OS fall into to Simple TimeZone category. For formatting purpose, following table shows the
* behavior of GWT DateTimeFormat.
*
*
*
* Pattern
* Common TimeZone
* Simple TimeZone
*
*
* z, zz, zzz
* PDT
* UTC-7
*
*
* zzzz
* Pacific Daylight Time
* UTC-7
*
*
* Z, ZZ
* -0700
* -0700
*
*
* ZZZ
* -07:00
* -07:00
*
*
* ZZZZ
* GMT-07:00
* GMT-07:00
*
*
* v, vv, vvv, vvvv
* America/Los_Angeles
* Etc/GMT+7
*
*
*
* Parsing Dates and Times
*
* The pattern does not need to specify every field. If the year, month, or day is missing from
* the pattern, the corresponding value will be taken from the current date. If the month is
* specified but the day is not, the day will be constrained to the last day within the specified
* month. If the hour, minute, or second is missing, the value defaults to zero.
*
*
As with formatting (described above), the count of pattern letters determines the parsing
* behavior.
*
*
* - Text
*
- 4 or more pattern letters--use full form, less than 4--use short or abbreviated form if one
* exists. In parsing, we will always try long format, then short.
*
- Number
*
- the minimum number of digits.
*
- Text or Number
*
- 3 or more characters means use text, otherwise use number
*
*
* Although the current pattern specification doesn't not specify behavior for all letters, it
* may in the future. It is strongly discouraged to use unspecified letters as literal text without
* quoting them.
*
*
[Note on TimeZone] The time zone support for parsing is limited. Only standard GMT and RFC
* format are supported. Time zone specification using time zone id (like America/Los_Angeles), time
* zone names (like PST, Pacific Standard Time) are not supported. Normally, it is too much a burden
* for a client application to load all the time zone symbols. And in almost all those cases, it is
* a better choice to do such parsing on server side through certain RPC mechanism. This decision is
* based on particular use cases we have studied; in principle, it could be changed in future
* versions.
*
*
Examples
*
*
*
* Pattern
* Formatted Text
*
*
*
* "yyyy.MM.dd G 'at' HH:mm:ss vvvv"
* 1996.07.10 AD at 15:08:56 America/Los_Angeles
*
*
*
* "EEE, MMM d, ''yy"
* Wed, July 10, '96
*
*
*
* "h:mm a"
* 12:08 PM
*
*
*
* "hh 'o''clock' a, zzzz"
* 12 o'clock PM, Pacific Daylight Time
*
*
*
* "K:mm a, vvvv"
* 0:00 PM, America/Los_Angeles
*
*
*
* "yyyyy.MMMMM.dd GGG hh:mm aaa"
* 01996.July.10 AD 12:08 PM
*
*
*
* Additional Parsing Considerations
*
* When parsing a date string using the abbreviated year pattern ( "yy"
), the parser
* 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 parser instance is created. For
* example, using a pattern of "MM/dd/yy"
and a DateTimeFormat
object
* 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 Character#isDigit(char)},
* will be parsed into the default century. If the year pattern does not have exactly two 'y'
* characters, the year is interpreted literally, regardless of the number of digits. For example,
* using the pattern "MM/dd/yyyy"
, "01/11/12" parses to Jan 11, 12 A.D.
*
*
When numeric fields abut one another directly, with no intervening delimiter characters, they
* constitute a run of abutting 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.
*
*
In the current implementation, timezone parsing only supports GMT:hhmm
,
* GMT:+hhmm
, and GMT:-hhmm
.
*
*
Example
*
* {@example com.google.gwt.examples.DateTimeFormatExample}
*/
public class DateTimeFormat {
/**
* Predefined date/time formats -- see {@link CustomDateTimeFormat} if you need some format that
* isn't supplied here.
*/
public enum PredefinedFormat {
// TODO(jat): Javadoc to explain these formats
/**
* ISO 8601 date format, fixed across all locales.
*
* Example: {@code 2008-10-03T10:29:40.046-04:00}
*
*
http://code.google.com/p/google-web-toolkit/issues/detail?id=3068
*
*
http://www.iso.org/iso/support/faqs/faqs_widely_used_standards/widely_used_standards_other/date_and_time_format.htm
*/
ISO_8601,
/**
* RFC 2822 date format, fixed across all locales.
*
*
Example: {@code Thu, 20 May 2010 17:54:50 -0700}
*
*
http://tools.ietf.org/html/rfc2822#section-3.3
*/
RFC_2822,
DATE_FULL,
DATE_LONG,
DATE_MEDIUM,
DATE_SHORT,
TIME_FULL,
TIME_LONG,
TIME_MEDIUM,
TIME_SHORT,
DATE_TIME_FULL,
DATE_TIME_LONG,
DATE_TIME_MEDIUM,
DATE_TIME_SHORT,
DAY,
HOUR_MINUTE,
HOUR_MINUTE_SECOND,
HOUR24_MINUTE,
HOUR24_MINUTE_SECOND,
MINUTE_SECOND,
MONTH,
MONTH_ABBR,
MONTH_ABBR_DAY,
MONTH_DAY,
MONTH_NUM_DAY,
MONTH_WEEKDAY_DAY,
YEAR,
YEAR_MONTH,
YEAR_MONTH_ABBR,
YEAR_MONTH_ABBR_DAY,
YEAR_MONTH_DAY,
YEAR_MONTH_NUM,
YEAR_MONTH_NUM_DAY,
YEAR_MONTH_WEEKDAY_DAY,
YEAR_QUARTER,
YEAR_QUARTER_ABBR,
}
/** Class PatternPart holds a "compiled" pattern part. */
private static class PatternPart {
public String text;
public int count; // 0 has a special meaning, it stands for literal
public boolean abutStart;
public PatternPart(String txt, int cnt) {
text = txt;
count = cnt;
abutStart = false;
}
}
protected static final String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z";
protected static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZ";
private static final int NUMBER_BASE = 10;
private static final int JS_START_YEAR = 1900;
private static final Map cache;
private static final int NUM_MILLISECONDS_IN_DAY = 24 * 60 * 60000;
private static final String PATTERN_CHARS = "GyMLdkHmsSEcDahKzZv";
// Note: M & L must be the first two characters
private static final String NUMERIC_FORMAT_CHARS = "MLydhHmsSDkK";
private static final String WHITE_SPACE = " \t\r\n";
private static final String GMT = "GMT";
private static final String UTC = "UTC";
private static final int MINUTES_PER_HOUR = 60;
static {
cache = new HashMap<>();
}
/**
* Get a DateTimeFormat instance for a predefined format.
*
* See {@link CustomDateTimeFormat} if you need a localized format that is not supported here.
*
* @param predef {@link PredefinedFormat} describing desired format
* @return a DateTimeFormat instance for the specified format
*/
public static DateTimeFormat getFormat(PredefinedFormat predef) {
if (usesFixedEnglishStrings(predef)) {
String pattern;
switch (predef) {
case RFC_2822:
pattern = RFC2822_PATTERN;
break;
case ISO_8601:
pattern = ISO8601_PATTERN;
break;
default:
throw new IllegalStateException("Unexpected predef type " + predef);
}
return getFormat(pattern, new DefaultDateTimeFormatInfo());
}
DateTimeFormatInfo dtfi = getDefaultDateTimeFormatInfo();
String pattern;
switch (predef) {
case DATE_FULL:
pattern = dtfi.dateFormatFull();
break;
case DATE_LONG:
pattern = dtfi.dateFormatLong();
break;
case DATE_MEDIUM:
pattern = dtfi.dateFormatMedium();
break;
case DATE_SHORT:
pattern = dtfi.dateFormatShort();
break;
case DATE_TIME_FULL:
pattern = dtfi.dateTimeFull(dtfi.timeFormatFull(), dtfi.dateFormatFull());
break;
case DATE_TIME_LONG:
pattern = dtfi.dateTimeLong(dtfi.timeFormatLong(), dtfi.dateFormatLong());
break;
case DATE_TIME_MEDIUM:
pattern = dtfi.dateTimeMedium(dtfi.timeFormatMedium(), dtfi.dateFormatMedium());
break;
case DATE_TIME_SHORT:
pattern = dtfi.dateTimeShort(dtfi.timeFormatShort(), dtfi.dateFormatShort());
break;
case DAY:
pattern = dtfi.formatDay();
break;
case HOUR24_MINUTE:
pattern = dtfi.formatHour24Minute();
break;
case HOUR24_MINUTE_SECOND:
pattern = dtfi.formatHour24MinuteSecond();
break;
case HOUR_MINUTE:
pattern = dtfi.formatHour12Minute();
break;
case HOUR_MINUTE_SECOND:
pattern = dtfi.formatHour12MinuteSecond();
break;
case MINUTE_SECOND:
pattern = dtfi.formatMinuteSecond();
break;
case MONTH:
pattern = dtfi.formatMonthFull();
break;
case MONTH_ABBR:
pattern = dtfi.formatMonthAbbrev();
break;
case MONTH_ABBR_DAY:
pattern = dtfi.formatMonthAbbrevDay();
break;
case MONTH_DAY:
pattern = dtfi.formatMonthFullDay();
break;
case MONTH_NUM_DAY:
pattern = dtfi.formatMonthNumDay();
break;
case MONTH_WEEKDAY_DAY:
pattern = dtfi.formatMonthFullWeekdayDay();
break;
case TIME_FULL:
pattern = dtfi.timeFormatFull();
break;
case TIME_LONG:
pattern = dtfi.timeFormatLong();
break;
case TIME_MEDIUM:
pattern = dtfi.timeFormatMedium();
break;
case TIME_SHORT:
pattern = dtfi.timeFormatShort();
break;
case YEAR:
pattern = dtfi.formatYear();
break;
case YEAR_MONTH:
pattern = dtfi.formatYearMonthFull();
break;
case YEAR_MONTH_ABBR:
pattern = dtfi.formatYearMonthAbbrev();
break;
case YEAR_MONTH_ABBR_DAY:
pattern = dtfi.formatYearMonthAbbrevDay();
break;
case YEAR_MONTH_DAY:
pattern = dtfi.formatYearMonthFullDay();
break;
case YEAR_MONTH_NUM:
pattern = dtfi.formatYearMonthNum();
break;
case YEAR_MONTH_NUM_DAY:
pattern = dtfi.formatYearMonthNumDay();
break;
case YEAR_MONTH_WEEKDAY_DAY:
pattern = dtfi.formatYearMonthWeekdayDay();
break;
case YEAR_QUARTER:
pattern = dtfi.formatYearQuarterFull();
break;
case YEAR_QUARTER_ABBR:
pattern = dtfi.formatYearQuarterShort();
break;
default:
throw new IllegalArgumentException("Unexpected predefined format " + predef);
}
return getFormat(pattern, dtfi);
}
/**
* Returns a DateTimeFormat object using the specified pattern. If you need to format or parse
* repeatedly using the same pattern, it is highly recommended that you cache the returned
* DateTimeFormat
object and reuse it rather than calling this method repeatedly.
*
*
Note that the pattern supplied is used as-is -- for example, if you supply "MM/dd/yyyy" as
* the pattern, that is the order you will get the fields, even in locales where the order is
* different. It is recommended to use {@link #getFormat(PredefinedFormat)} instead -- if you use
* this method, you are taking responsibility for localizing the patterns yourself.
*
* @param pattern string to specify how the date should be formatted
* @return a DateTimeFormat
object that can be used for format or parse date/time
* values matching the specified pattern
* @throws IllegalArgumentException if the specified pattern could not be parsed
*/
public static DateTimeFormat getFormat(String pattern) {
return getFormat(pattern, getDefaultDateTimeFormatInfo());
}
/**
* Internal factory method that provides caching.
*
* @param pattern
* @param dtfi
* @return DateTimeFormat instance
*/
protected static DateTimeFormat getFormat(String pattern, DateTimeFormatInfo dtfi) {
DateTimeFormatInfo defaultDtfi = getDefaultDateTimeFormatInfo();
DateTimeFormat dtf = null;
if (dtfi == defaultDtfi) {
dtf = cache.get(pattern);
}
if (dtf == null) {
dtf = new DateTimeFormat(pattern, dtfi);
if (dtfi == defaultDtfi) {
cache.put(pattern, dtf);
}
}
return dtf;
}
private static DateTimeFormatInfo getDefaultDateTimeFormatInfo() {
return DateTimeFormatInfo_factory.create();
}
/**
* Returns true if the predefined format is one that specifies always using English
* names/separators.
*
*
This should be a method on PredefinedFormat, but that would defeat the enum optimizations
* GWT is currently capable of.
*
* @param predef
* @return true if the specified format requires English names/separators
*/
private static boolean usesFixedEnglishStrings(PredefinedFormat predef) {
switch (predef) {
case RFC_2822:
return true;
case ISO_8601:
return true;
default:
return false;
}
}
private final ArrayList patternParts = new ArrayList();
private final DateTimeFormatInfo dateTimeFormatInfo;
private final String pattern;
/**
* Constructs a format object using the specified pattern and the date time constants for the
* default locale.
*
* @param pattern string pattern specification
*/
protected DateTimeFormat(String pattern) {
this(pattern, getDefaultDateTimeFormatInfo());
}
/**
* Constructs a format object using the specified pattern and user-supplied date time constants.
*
* @param pattern string pattern specification
* @param dtfi DateTimeFormatInfo instance to use
*/
protected DateTimeFormat(String pattern, DateTimeFormatInfo dtfi) {
this.pattern = pattern;
this.dateTimeFormatInfo = dtfi;
/*
* Even though the pattern is only compiled for use in parsing and parsing
* is far less common than formatting, the pattern is still parsed eagerly
* here to fail fast in case the pattern itself is malformed.
*/
parsePattern(pattern);
}
/**
* Format a date object.
*
* @param date the date object being formatted
* @return string representation for this date in desired format
*/
public String format(Date date) {
return format(date, null);
}
/**
* Format a date object using specified time zone.
*
* @param date the date object being formatted
* @param timeZone a TimeZone object that holds time zone information, or {@code null} to use the
* default
* @return string representation for this date in the format defined by this object
*/
@SuppressWarnings("deprecation")
public String format(Date date, TimeZone timeZone) {
// We use the Date class to calculate each date/time field in order
// to maximize performance and minimize code size.
// JavaScript only provides an API for rendering local time (in the os time
// zone). Here we want to render time in any timezone. So suppose we try to
// render the date (20:00 GMT0000, or 16:00 GMT-0400, or 12:00 GMT-0800) for
// time zone GMT-0400, and OS has time zone GMT-0800. By adding the
// difference between OS time zone (GMT-0800) and target time zone
// (GMT-0400) to "date", we end up with 16:00 GMT-0800. This date object
// has the same date/time fields (year, month, date, hour, minutes, etc)
// in GMT-0800 as original date in our target time zone (GMT-0400). We
// just need to take care of time zone display, but that's needed anyway.
// Things get a little bit more tricky when a daylight time transition
// happens. For example, if the OS timezone is America/Los_Angeles,
// it is just impossible to have a Date represent 2006/4/2 02:30, because
// 2:00 to 3:00 on that day does not exist in US Pacific time zone because
// of the daylight time switch.
// But we can use 2 separate date objects, one to represent 2006/4/2, one
// to represent 02:30. Of course, for the 2nd date object its date can be
// any other day in that year, except 2006/4/2. So we end up have 3 Date
// objects: one for resolving "Year, month, day", one for time within that
// day, and the original date object, which is needed for figuring out
// actual time zone offset.
if (timeZone == null) {
timeZone = createTimeZone(date.getTimezoneOffset());
}
int diff = (date.getTimezoneOffset() - timeZone.getOffset(date)) * 60000;
Date keepDate = new Date(date.getTime() + diff);
Date keepTime = keepDate;
if (keepDate.getTimezoneOffset() != date.getTimezoneOffset()) {
if (diff > 0) {
diff -= NUM_MILLISECONDS_IN_DAY;
} else {
diff += NUM_MILLISECONDS_IN_DAY;
}
keepTime = new Date(date.getTime() + diff);
}
StringBuilder toAppendTo = new StringBuilder(64);
int j, n = pattern.length();
for (int i = 0; i < n; ) {
char ch = pattern.charAt(i);
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
// ch is a date-time pattern character to be interpreted by subFormat().
// Count the number of times it is repeated.
for (j = i + 1; j < n && pattern.charAt(j) == ch; ++j) {}
subFormat(toAppendTo, ch, j - i, date, keepDate, keepTime, timeZone);
i = j;
} else if (ch == '\'') {
// Handle an entire quoted string, included embedded
// doubled apostrophes (as in 'o''clock').
// i points after '.
++i;
// If start with '', just add ' and continue.
if (i < n && pattern.charAt(i) == '\'') {
toAppendTo.append('\'');
++i;
continue;
}
// Otherwise add the quoted string.
boolean trailQuote = false;
while (!trailQuote) {
// j points to next ' or EOS.
j = i;
while (j < n && pattern.charAt(j) != '\'') {
++j;
}
if (j >= n) {
// Trailing ' (pathological).
throw new IllegalArgumentException("Missing trailing \'");
}
// Look ahead to detect '' within quotes.
if (j + 1 < n && pattern.charAt(j + 1) == '\'') {
++j;
} else {
trailQuote = true;
}
toAppendTo.append(pattern.substring(i, j));
i = j + 1;
}
} else {
// Append unquoted literal characters.
toAppendTo.append(ch);
++i;
}
}
return toAppendTo.toString();
}
/**
* Retrieve the pattern used in this DateTimeFormat object.
*
* @return pattern string
*/
public String getPattern() {
return pattern;
}
/**
* Parses text to produce a {@link Date} value. An {@link IllegalArgumentException} is thrown if
* either the text is empty or if the parse does not consume all characters of the text.
*
* Dates are parsed leniently, so invalid dates will be wrapped around as needed. For example,
* February 30 will wrap to March 2.
*
* @param text the string being parsed
* @return a parsed date/time value
* @throws IllegalArgumentException if the entire text could not be converted into a number
*/
public Date parse(String text) throws IllegalArgumentException {
return parse(text, false);
}
/**
* This method modifies a {@link Date} object to reflect the date that is parsed from an input
* string.
*
*
Dates are parsed leniently, so invalid dates will be wrapped around as needed. For example,
* February 30 will wrap to March 2.
*
* @param text the string that need to be parsed
* @param start the character position in "text" where parsing should start
* @param date the date object that will hold parsed value
* @return 0 if parsing failed, otherwise the number of characters advanced
*/
public int parse(String text, int start, Date date) {
return parse(text, start, date, false);
}
/**
* Parses text to produce a {@link Date} value. An {@link IllegalArgumentException} is thrown if
* either the text is empty or if the parse does not consume all characters of the text.
*
*
Dates are parsed strictly, so invalid dates will result in an {@link
* IllegalArgumentException}.
*
* @param text the string being parsed
* @return a parsed date/time value
* @throws IllegalArgumentException if the entire text could not be converted into a number
*/
public Date parseStrict(String text) throws IllegalArgumentException {
return parse(text, true);
}
/**
* This method modifies a {@link Date} object to reflect the date that is parsed from an input
* string.
*
*
Dates are parsed strictly, so invalid dates will return 0. For example, February 30 will
* return 0 because February only has 28 days.
*
* @param text the string that need to be parsed
* @param start the character position in "text" where parsing should start
* @param date the date object that will hold parsed value
* @return 0 if parsing failed, otherwise the number of characters advanced
*/
public int parseStrict(String text, int start, Date date) {
return parse(text, start, date, true);
}
/**
* @param timezoneOffset
* @return {@link TimeZone} instance
*/
protected TimeZone createTimeZone(int timezoneOffset) {
// MUSTFIX(jat): implement
return org.gwtproject.i18n.client.TimeZone.createTimeZone(timezoneOffset);
}
/**
* Method append current content in buf as pattern part if there is any, and clear buf for next
* part.
*
* @param buf pattern part text specification
* @param count pattern part repeat count
*/
private void addPart(StringBuilder buf, int count) {
if (buf.length() > 0) {
patternParts.add((new PatternPart(buf.toString(), count)));
buf.setLength(0);
}
}
/**
* Formats (0..11) Hours field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
@SuppressWarnings("deprecation")
private void format0To11Hours(StringBuilder buf, int count, Date date) {
int value = date.getHours() % 12;
zeroPaddingNumber(buf, value, count);
}
/**
* Formats (0..23) Hours field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
@SuppressWarnings("deprecation")
private void format0To23Hours(StringBuilder buf, int count, Date date) {
int value = date.getHours();
zeroPaddingNumber(buf, value, count);
}
/**
* Formats (1..12) Hours field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
@SuppressWarnings("deprecation")
private void format1To12Hours(StringBuilder buf, int count, Date date) {
int value = date.getHours() % 12;
if (value == 0) {
zeroPaddingNumber(buf, 12, count);
} else {
zeroPaddingNumber(buf, value, count);
}
}
/**
* Formats (1..24) Hours field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
@SuppressWarnings("deprecation")
private void format24Hours(StringBuilder buf, int count, Date date) {
int value = date.getHours();
if (value == 0) {
zeroPaddingNumber(buf, 24, count);
} else {
zeroPaddingNumber(buf, value, count);
}
}
/**
* Formats AM/PM field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param date hold the date object to be formatted
*/
@SuppressWarnings("deprecation")
private void formatAmPm(StringBuilder buf, Date date) {
if (date.getHours() >= 12 && date.getHours() < 24) {
buf.append(dateTimeFormatInfo.ampms()[1]);
} else {
buf.append(dateTimeFormatInfo.ampms()[0]);
}
}
/**
* Formats Date field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatDate(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getDate();
zeroPaddingNumber(buf, value, count);
}
/**
* Formats Day of week field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatDayOfWeek(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getDay();
if (count == 5) {
buf.append(dateTimeFormatInfo.weekdaysNarrow()[value]);
} else if (count == 4) {
buf.append(dateTimeFormatInfo.weekdaysFull()[value]);
} else {
buf.append(dateTimeFormatInfo.weekdaysShort()[value]);
}
}
/**
* Formats Era field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatEra(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getYear() >= -JS_START_YEAR ? 1 : 0;
if (count >= 4) {
buf.append(dateTimeFormatInfo.erasFull()[value]);
} else {
buf.append(dateTimeFormatInfo.erasShort()[value]);
}
}
/**
* Formats Fractional seconds field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatFractionalSeconds(StringBuilder buf, int count, Date date) {
/*
* Fractional seconds should be left-justified, ie. zero must be padded from
* left. For example, if the value in milliseconds is 5, and the count is 3,
* the output will be "005".
*
* Values with less than three digits are rounded to the desired number of
* places, but the rounded values are truncated at 9 or 99 in order to avoid
* changing the values of seconds.
*/
long time = date.getTime();
int value;
if (time < 0) {
value = 1000 - (int) (-time % 1000);
if (value == 1000) {
value = 0;
}
} else {
value = (int) (time % 1000);
}
if (count == 1) {
value = Math.min((value + 50) / 100, 9); // Round to 100ms, clamp to 9
buf.append((char) ('0' + value));
} else if (count == 2) {
value = Math.min((value + 5) / 10, 99); // Round to 10ms, clamp to 99
zeroPaddingNumber(buf, value, 2);
} else {
zeroPaddingNumber(buf, value, 3);
if (count > 3) {
zeroPaddingNumber(buf, 0, count - 3);
}
}
}
/**
* Formats Minutes field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatMinutes(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getMinutes();
zeroPaddingNumber(buf, value, count);
}
/**
* Formats Month field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatMonth(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getMonth();
switch (count) {
case 5:
buf.append(dateTimeFormatInfo.monthsNarrow()[value]);
break;
case 4:
buf.append(dateTimeFormatInfo.monthsFull()[value]);
break;
case 3:
buf.append(dateTimeFormatInfo.monthsShort()[value]);
break;
default:
zeroPaddingNumber(buf, value + 1, count);
}
}
/**
* Formats Quarter field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatQuarter(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getMonth() / 3;
if (count < 4) {
buf.append(dateTimeFormatInfo.quartersShort()[value]);
} else {
buf.append(dateTimeFormatInfo.quartersFull()[value]);
}
}
/**
* Formats Seconds field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatSeconds(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getSeconds();
zeroPaddingNumber(buf, value, count);
}
/**
* Formats Standalone weekday field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatStandaloneDay(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getDay();
if (count == 5) {
buf.append(dateTimeFormatInfo.weekdaysNarrowStandalone()[value]);
} else if (count == 4) {
buf.append(dateTimeFormatInfo.weekdaysFullStandalone()[value]);
} else if (count == 3) {
buf.append(dateTimeFormatInfo.weekdaysShortStandalone()[value]);
} else {
zeroPaddingNumber(buf, value, 1);
}
}
/**
* Formats Standalone Month field according to pattern specified.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatStandaloneMonth(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getMonth();
if (count == 5) {
buf.append(dateTimeFormatInfo.monthsNarrowStandalone()[value]);
} else if (count == 4) {
buf.append(dateTimeFormatInfo.monthsFullStandalone()[value]);
} else if (count == 3) {
buf.append(dateTimeFormatInfo.monthsShortStandalone()[value]);
} else {
zeroPaddingNumber(buf, value + 1, count);
}
}
/**
* Formats Timezone field.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatTimeZone(StringBuilder buf, int count, Date date, TimeZone timeZone) {
if (count < 4) {
buf.append(timeZone.getShortName(date));
} else {
buf.append(timeZone.getLongName(date));
}
}
/**
* Formats Timezone field following RFC.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date hold the date object to be formatted
*/
private void formatTimeZoneRFC(StringBuilder buf, int count, Date date, TimeZone timeZone) {
if (count < 3) {
buf.append(timeZone.getRFCTimeZoneString(date));
} else if (count == 3) {
buf.append(timeZone.getISOTimeZoneString(date));
} else {
buf.append(timeZone.getGMTString(date));
}
}
/**
* Formats Year field according to pattern specified. Javascript Date object seems incapable
* handling 1BC and year before. It can show you year 0 which does not exists. following we just
* keep consistent with javascript's toString method. But keep in mind those things should be
* unsupported.
*
* @param buf where formatted string will be appended to
* @param count number of time pattern char repeats; this controls how a field should be
* formatted; 2 is treated specially with the last two digits of the year, while more than 2
* digits are zero-padded
* @param date hold the date object to be formatted
*/
private void formatYear(StringBuilder buf, int count, Date date) {
@SuppressWarnings("deprecation")
int value = date.getYear() + JS_START_YEAR;
if (value < 0) {
value = -value;
}
switch (count) {
case 1: // no padding
buf.append(value);
break;
case 2: // last 2 digits of year, zero-padded
zeroPaddingNumber(buf, value % 100, 2);
break;
default: // anything else is zero-padded
zeroPaddingNumber(buf, value, count);
break;
}
}
/**
* Method getNextCharCountInPattern calculate character repeat count in pattern.
*
* @param pattern describe the format of date string that need to be parsed
* @param start the position of pattern character
* @return repeat count
*/
private int getNextCharCountInPattern(String pattern, int start) {
char ch = pattern.charAt(start);
int next = start + 1;
while (next < pattern.length() && pattern.charAt(next) == ch) {
++next;
}
return next - start;
}
/**
* Method identifies the start of a run of abutting numeric fields. Take the pattern "HHmmss" as
* an example. We will try to parse 2/2/2 characters of the input text, then if that fails, 1/2/2.
* We only adjust the width of the leftmost field; the others remain fixed. This allows "123456"
* => 12:34:56, but "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2, 3/2/2,
* 2/2/2, and finally 1/2/2. The first field of connected numeric fields will be marked as
* abutStart, its width can be reduced to accommodate others.
*/
private void identifyAbutStart() {
// 'abut' parts are continuous numeric parts. abutStart is the switch
// point from non-abut to abut.
boolean abut = false;
int len = patternParts.size();
for (int i = 0; i < len; i++) {
if (isNumeric(patternParts.get(i))) {
// If next part is not following abut sequence, and isNumeric.
if (!abut && i + 1 < len && isNumeric(patternParts.get(i + 1))) {
abut = true;
patternParts.get(i).abutStart = true;
}
} else {
abut = false;
}
}
}
/**
* Method checks if the pattern part is a numeric field.
*
* @param part pattern part to be examined
* @return true
if the pattern part is numberic field
*/
private boolean isNumeric(PatternPart part) {
if (part.count <= 0) {
return false;
}
int i = NUMERIC_FORMAT_CHARS.indexOf(part.text.charAt(0));
// M & L (index 0 and 1) are only numeric if there are less than 3 chars
return (i > 1 || (i >= 0 && part.count < 3));
}
/**
* Method attempts to match the text at a given position against an array of strings. Since
* multiple strings in the array may match (for example, if the array contains "a", "ab", and
* "abc", all will match the input string "abcd") the longest match is returned.
*
* @param text the time text being parsed
* @param start where to start parsing
* @param data the string array to parsed
* @param pos to receive where the match stopped
* @return the new start position if matching succeeded; a negative number indicating matching
* failure
*/
private int matchString(String text, int start, String[] data, int[] pos) {
int count = data.length;
// There may be multiple strings in the data[] array which begin with
// the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
// We keep track of the longest match, and return that. Note that this
// unfortunately requires us to test all array elements.
int bestMatchLength = 0, bestMatch = -1;
String textInLowerCase = text.substring(start).toLowerCase(Locale.ROOT);
for (int i = 0; i < count; ++i) {
int length = data[i].length();
// Always compare if we have no match yet; otherwise only compare
// against potentially better matches (longer strings).
if (length > bestMatchLength
&& textInLowerCase.startsWith(data[i].toLowerCase(Locale.ROOT))) {
bestMatch = i;
bestMatchLength = length;
}
}
if (bestMatch >= 0) {
pos[0] = start + bestMatchLength;
}
return bestMatch;
}
/**
* Parses text to produce a {@link Date} value. An {@link IllegalArgumentException} is thrown if
* either the text is empty or if the parse does not consume all characters of the text.
*
*
If using lenient parsing, certain invalid dates and times will be parsed. For example,
* February 32nd would be parsed as March 4th in lenient mode, but would throw an exception in
* non-lenient mode.
*
* @param text the string being parsed
* @param strict true to be strict when parsing, false to be lenient
* @return a parsed date/time value
* @throws IllegalArgumentException if the entire text could not be converted into a number
*/
private Date parse(String text, boolean strict) {
Date curDate = new Date();
@SuppressWarnings("deprecation")
Date date = new Date(curDate.getYear(), curDate.getMonth(), curDate.getDate());
int charsConsumed = parse(text, 0, date, strict);
if (charsConsumed == 0 || charsConsumed < text.length()) {
throw new IllegalArgumentException(text);
}
return date;
}
/**
* This method parses the input string and fills its value into a {@link Date} .
*
*
If using lenient parsing, certain invalid dates and times will be parsed. For example,
* February 32nd would be parsed as March 4th in lenient mode, but would return 0 in non-lenient
* mode.
*
* @param text the string that need to be parsed
* @param start the character position in "text" where parsing should start
* @param date the date object that will hold parsed value
* @param strict true to be strict when parsing, false to be lenient
* @return 0 if parsing failed, otherwise the number of characters advanced
*/
private int parse(String text, int start, Date date, boolean strict) {
DateRecord cal = new DateRecord();
int[] parsePos = {start};
// For parsing abutting numeric fields. 'abutPat' is the
// offset into 'pattern' of the first of 2 or more abutting
// numeric fields. 'abutStart' is the offset into 'text'
// where parsing the fields begins. 'abutPass' starts off as 0
// and increments each time we try to parse the fields.
int abutPat = -1; // If >=0, we are in a run of abutting numeric fields.
int abutStart = 0;
int abutPass = 0;
for (int i = 0; i < patternParts.size(); ++i) {
PatternPart part = patternParts.get(i);
if (part.count > 0) {
if (abutPat < 0 && part.abutStart) {
abutPat = i;
abutStart = parsePos[0];
abutPass = 0;
}
// Handle fields within a run of abutting numeric fields. Take
// the pattern "HHmmss" as an example. We will try to parse
// 2/2/2 characters of the input text, then if that fails,
// 1/2/2. We only adjust the width of the leftmost field; the
// others remain fixed. This allows "123456" => 12:34:56, but
// "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
// try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
if (abutPat >= 0) {
// If we are at the start of a run of abutting fields, then
// shorten this field in each pass. If we can't shorten
// this field any more, then the parse of this set of
// abutting numeric fields has failed.
int count = part.count;
if (i == abutPat) {
count -= abutPass++;
if (count == 0) {
return 0;
}
}
if (!subParse(text, parsePos, part, count, cal)) {
// If the parse fails anywhere in the run, back up to the
// start of the run and retry.
i = abutPat - 1;
parsePos[0] = abutStart;
continue;
}
} else {
// Handle non-numeric fields and non-abutting numeric fields.
abutPat = -1;
if (!subParse(text, parsePos, part, 0, cal)) {
return 0;
}
}
} else {
// Handle literal pattern characters. These are any
// quoted characters and non-alphabetic unquoted characters.
abutPat = -1;
// A run of white space in the pattern matches a run
// of white space in the input text.
if (part.text.charAt(0) == ' ') {
// Advance over run in input text.
int s = parsePos[0];
skipSpace(text, parsePos);
// Must see at least one white space char in input.
if (parsePos[0] > s) {
continue;
}
} else if (text.startsWith(part.text, parsePos[0])) {
parsePos[0] += part.text.length();
continue;
}
// We fall through to this point if the match fails.
return 0;
}
}
// Calculate the date from the parts
if (!cal.calcDate(date, strict)) {
return 0;
}
// Return progress.
return parsePos[0] - start;
}
/**
* Method parses a integer string and return integer value.
*
* @param text string being parsed
* @param pos parse position
* @return integer value
*/
private int parseInt(String text, int[] pos) {
int ret = 0;
int ind = pos[0];
if (ind >= text.length()) {
return -1;
}
char ch = text.charAt(ind);
while (ch >= '0' && ch <= '9') {
ret = ret * 10 + (ch - '0');
ind++;
if (ind >= text.length()) {
break;
}
ch = text.charAt(ind);
}
if (ind > pos[0]) {
pos[0] = ind;
} else {
ret = -1;
}
return ret;
}
/**
* Method parses the input pattern string a generate a vector of pattern parts.
*
* @param pattern describe the format of date string that need to be parsed
*/
private void parsePattern(String pattern) {
StringBuilder buf = new StringBuilder(32);
boolean inQuote = false;
for (int i = 0; i < pattern.length(); i++) {
char ch = pattern.charAt(i);
// Handle space, add literal part (if exist), and add space part.
if (ch == ' ') {
addPart(buf, 0);
buf.append(' ');
addPart(buf, 0);
while (i + 1 < pattern.length() && pattern.charAt(i + 1) == ' ') {
i++;
}
continue;
}
// If inside quote, except two quote connected, just copy or exit.
if (inQuote) {
if (ch == '\'') {
if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
// Quote appeared twice continuously, interpret as one quote.
buf.append(ch);
++i;
} else {
inQuote = false;
}
} else {
// Literal.
buf.append(ch);
}
continue;
}
// Outside quote now.
if (PATTERN_CHARS.indexOf(ch) > 0) {
addPart(buf, 0);
buf.append(ch);
int count = getNextCharCountInPattern(pattern, i);
addPart(buf, count);
i += count - 1;
continue;
}
// Two consecutive quotes is a quote literal, inside or outside of quotes.
if (ch == '\'') {
if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
buf.append('\'');
i++;
} else {
inQuote = true;
}
} else {
buf.append(ch);
}
}
addPart(buf, 0);
identifyAbutStart();
}
/**
* Method parses time zone offset.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param cal DateRecord object that holds parsed value
* @return true
if parsing successful, otherwise false
*/
private boolean parseTimeZoneOffset(String text, int[] pos, DateRecord cal) {
if (pos[0] >= text.length()) {
cal.setTzOffset(0);
return true;
}
int sign;
switch (text.charAt(pos[0])) {
case '+':
sign = 1;
break;
case '-':
sign = -1;
break;
default:
cal.setTzOffset(0);
return true;
}
++(pos[0]);
// Look for hours:minutes or hhmm.
int st = pos[0];
int value = parseInt(text, pos);
if (value == 0 && pos[0] == st) {
return false;
}
int offset;
if (pos[0] < text.length() && text.charAt(pos[0]) == ':') {
// This is the hours:minutes case.
offset = value * MINUTES_PER_HOUR;
++(pos[0]);
st = pos[0];
value = parseInt(text, pos);
if (value == 0 && pos[0] == st) {
return false;
}
offset += value;
} else {
// This is the hhmm case.
offset = value;
// Assume "-23".."+23" refers to hours.
if (offset < 24 && (pos[0] - st) <= 2) {
offset *= MINUTES_PER_HOUR;
} else {
offset = offset % 100 + offset / 100 * MINUTES_PER_HOUR;
}
}
offset *= sign;
cal.setTzOffset(-offset);
return true;
}
/**
* Method skips space in the string as pointed by pos.
*
* @param text input string
* @param pos where skip start, and return back where skip stop
*/
private void skipSpace(String text, int[] pos) {
while (pos[0] < text.length() && WHITE_SPACE.indexOf(text.charAt(pos[0])) >= 0) {
++(pos[0]);
}
}
/**
* Formats a single field according to pattern specified.
*
* @param ch pattern character for this field
* @param count number of time pattern char repeats; this controls how a field should be formatted
* @param date the date object to be formatted
* @param adjustedDate holds the time zone adjusted date fields
* @param adjustedTime holds the time zone adjusted time fields
* @return true
if pattern valid, otherwise false
*/
private boolean subFormat(
StringBuilder buf,
char ch,
int count,
Date date,
Date adjustedDate,
Date adjustedTime,
TimeZone timezone) {
switch (ch) {
case 'G':
formatEra(buf, count, adjustedDate);
break;
case 'y':
formatYear(buf, count, adjustedDate);
break;
case 'M':
formatMonth(buf, count, adjustedDate);
break;
case 'k':
format24Hours(buf, count, adjustedTime);
break;
case 'S':
formatFractionalSeconds(buf, count, adjustedTime);
break;
case 'E':
formatDayOfWeek(buf, count, adjustedDate);
break;
case 'a':
formatAmPm(buf, adjustedTime);
break;
case 'h':
format1To12Hours(buf, count, adjustedTime);
break;
case 'K':
format0To11Hours(buf, count, adjustedTime);
break;
case 'H':
format0To23Hours(buf, count, adjustedTime);
break;
case 'c':
formatStandaloneDay(buf, count, adjustedDate);
break;
case 'L':
formatStandaloneMonth(buf, count, adjustedDate);
break;
case 'Q':
formatQuarter(buf, count, adjustedDate);
break;
case 'd':
formatDate(buf, count, adjustedDate);
break;
case 'm':
formatMinutes(buf, count, adjustedTime);
break;
case 's':
formatSeconds(buf, count, adjustedTime);
break;
case 'z':
formatTimeZone(buf, count, date, timezone);
break;
case 'v':
buf.append(timezone.getID());
break;
case 'Z':
formatTimeZoneRFC(buf, count, date, timezone);
break;
default:
return false;
}
return true;
}
/**
* Converts one field of the input string into a numeric field value. Returns false
* if failed.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param part the pattern part for this field
* @param digitCount when greater than 0, numeric parsing must obey the count
* @param cal DateRecord object that will hold parsed value
* @return true
if parsing successful
*/
@SuppressWarnings("fallthrough")
private boolean subParse(
String text, int[] pos, PatternPart part, int digitCount, DateRecord cal) {
skipSpace(text, pos);
int start = pos[0];
char ch = part.text.charAt(0);
// Parse integer value if it is a numeric field.
int value = -1; // initialize value to be -1,
if (isNumeric(part)) {
if (digitCount > 0) {
if ((start + digitCount) > text.length()) {
return false;
}
value = parseInt(text.substring(0, start + digitCount), pos);
} else {
value = parseInt(text, pos);
}
}
switch (ch) {
case 'G': // era
value = matchString(text, start, dateTimeFormatInfo.erasFull(), pos);
cal.setEra(value);
return true;
case 'M': // month
return subParseMonth(text, pos, cal, value, start);
case 'L': // standalone month
return subParseStandaloneMonth(text, pos, cal, value, start);
case 'E': // day of week
return subParseDayOfWeek(text, pos, start, cal);
case 'c': // standalone day of week
return subParseStandaloneDay(text, pos, start, cal);
case 'a': // AM/PM
value = matchString(text, start, dateTimeFormatInfo.ampms(), pos);
cal.setAmpm(value);
return true;
case 'y': // year
return subParseYear(text, pos, start, value, part, cal);
case 'd': // day of month
if (value <= 0) {
return false;
}
cal.setDayOfMonth(value);
return true;
case 'S': // fractional seconds
if (value < 0) {
return false;
}
return subParseFractionalSeconds(value, start, pos[0], cal);
case 'h': // hour (1..12)
if (value == 12) {
value = 0;
}
// fall through
case 'K': // hour (0..11)
case 'H': // hour (0..23)
if (value < 0) {
return false;
}
cal.setHours(value);
cal.setMidnightIs24(false);
return true;
case 'k': // hour (1..24)
if (value < 0) {
return false;
}
cal.setHours(value);
cal.setMidnightIs24(true);
return true;
case 'm': // minute
if (value < 0) {
return false;
}
cal.setMinutes(value);
return true;
case 's': // second
if (value < 0) {
return false;
}
cal.setSeconds(value);
return true;
case 'Z': // time zone RFC
// ISO-8601 times can have a literal Z to indicate GMT+0
if (start < text.length() && text.charAt(start) == 'Z') {
pos[0]++;
cal.setTzOffset(0);
return true;
}
// $FALL-THROUGH$
case 'z': // time zone offset
case 'v': // time zone generic
return subParseTimeZoneInGMT(text, start, pos, cal);
default:
return false;
}
}
/**
* Method subParseDayOfWeek parses day of the week field.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param start from where parse start
* @param cal DateRecord object that holds parsed value
* @return true
if parsing successful, otherwise false
*/
private boolean subParseDayOfWeek(String text, int[] pos, int start, DateRecord cal) {
int value;
// 'E' - DAY_OF_WEEK
// Want to be able to parse both short and long forms.
// Try count == 4 (DDDD) first:
value = matchString(text, start, dateTimeFormatInfo.weekdaysFull(), pos);
if (value < 0) {
value = matchString(text, start, dateTimeFormatInfo.weekdaysShort(), pos);
}
if (value < 0) {
return false;
}
cal.setDayOfWeek(value);
return true;
}
/**
* Method subParseFractionalSeconds parses fractional seconds field.
*
* @param value parsed numberic value
* @param start
* @param end parse position
* @param cal DateRecord object that holds parsed value
* @return true
if parsing successful, otherwise false
*/
private boolean subParseFractionalSeconds(int value, int start, int end, DateRecord cal) {
// Fractional seconds left-justify.
int i = end - start;
if (i < 3) {
while (i < 3) {
value *= 10;
i++;
}
} else {
int a = 1;
while (i > 3) {
a *= 10;
i--;
}
value = (value + (a >> 1)) / a;
}
cal.setMilliseconds(value);
return true;
}
/**
* Parses Month field.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param cal DateRecord object that will hold parsed value
* @param value numeric value if this field is expressed using numberic pattern
* @param start from where parse start
* @return true
if parsing successful
*/
private boolean subParseMonth(String text, int[] pos, DateRecord cal, int value, int start) {
// When month is symbols, i.e., MMM or MMMM, value will be -1.
if (value < 0) {
// Want to be able to parse both short and long forms.
// Try count == 4 first:
value = matchString(text, start, dateTimeFormatInfo.monthsFull(), pos);
if (value < 0) { // count == 4 failed, now try count == 3.
value = matchString(text, start, dateTimeFormatInfo.monthsShort(), pos);
}
if (value < 0) {
return false;
}
cal.setMonth(value);
return true;
} else if (value > 0) {
cal.setMonth(value - 1);
return true;
}
return false;
}
/**
* Parses standalone day of the week field.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param start from where parse start
* @param cal DateRecord object that holds parsed value
* @return true
if parsing successful, otherwise false
*/
private boolean subParseStandaloneDay(String text, int[] pos, int start, DateRecord cal) {
int value;
// 'c' - DAY_OF_WEEK
// Want to be able to parse both short and long forms.
// Try count == 4 (cccc) first:
value = matchString(text, start, dateTimeFormatInfo.weekdaysFullStandalone(), pos);
if (value < 0) {
value = matchString(text, start, dateTimeFormatInfo.weekdaysShortStandalone(), pos);
}
if (value < 0) {
return false;
}
cal.setDayOfWeek(value);
return true;
}
/**
* Parses a standalone month field.
*
* @param text the time text to be parsed
* @param pos Parse position
* @param cal DateRecord object that will hold parsed value
* @param value numeric value if this field is expressed using numberic pattern
* @param start from where parse start
* @return true
if parsing successful
*/
private boolean subParseStandaloneMonth(
String text, int[] pos, DateRecord cal, int value, int start) {
// When month is symbols, i.e., LLL or LLLL, value will be -1.
if (value < 0) {
// Want to be able to parse both short and long forms.
// Try count == 4 first:
value = matchString(text, start, dateTimeFormatInfo.monthsFullStandalone(), pos);
if (value < 0) { // count == 4 failed, now try count == 3.
value = matchString(text, start, dateTimeFormatInfo.monthsShortStandalone(), pos);
}
if (value < 0) {
return false;
}
cal.setMonth(value);
return true;
} else if (value > 0) {
cal.setMonth(value - 1);
return true;
}
return false;
}
/**
* Method parses GMT type timezone.
*
* @param text the time text to be parsed
* @param start from where parse start
* @param pos Parse position
* @param cal DateRecord object that holds parsed value
* @return true
if parsing successful, otherwise false
*/
private boolean subParseTimeZoneInGMT(String text, int start, int[] pos, DateRecord cal) {
// First try to parse generic forms such as GMT-07:00. Do this first
// in case localized DateFormatZoneData contains the string "GMT"
// for a zone; in that case, we don't want to match the first three
// characters of GMT+/-HH:MM etc.
// For time zones that have no known names, look for strings
// of the form:
// GMT[+-]hours:minutes or
// GMT[+-]hhmm or
// GMT.
if (text.startsWith(GMT, start)) {
pos[0] = start + GMT.length();
return parseTimeZoneOffset(text, pos, cal);
}
// Likewise for UTC.
if (text.startsWith(UTC, start)) {
pos[0] = start + UTC.length();
return parseTimeZoneOffset(text, pos, cal);
}
// At this point, check for named time zones by looking through
// the locale data from the DateFormatZoneData strings.
// Want to be able to parse both short and long forms.
/*
* i = subParseZoneString(text, start, cal); if (i != 0) return i;
*/
// As a last resort, look for numeric timezones of the form
// [+-]hhmm as specified by RFC 822. This code is actually
// a little more permissive than RFC 822. It will try to do
// its best with numbers that aren't strictly 4 digits long.
return parseTimeZoneOffset(text, pos, cal);
}
/**
* Method subParseYear parse year field. Year field is special because 1, two digit year need to
* be resolved. 2, we allow year to take a sign. 3, year field participate in abut processing. In
* my testing, negative year does not seem working due to JDK (or GWT implementation) limitation.
* It is not a big deal so we don't worry about it. But keep the logic here so that we might want
* to replace DateRecord with our a calendar class.
*
* @param text the time text to be parsed
* @param pos parse position
* @param start where this field starts
* @param value integer value of year
* @param part the pattern part for this field
* @param cal DateRecord object that will hold parsed value
* @return true
if successful
*/
private boolean subParseYear(
String text, int[] pos, int start, int value, PatternPart part, DateRecord cal) {
char ch = ' ';
if (value < 0) {
if (pos[0] >= text.length()) {
return false;
}
ch = text.charAt(pos[0]);
// Check if it is a sign.
if (ch != '+' && ch != '-') {
return false;
}
++(pos[0]);
value = parseInt(text, pos);
if (value < 0) {
return false;
}
if (ch == '-') {
value = -value;
}
}
// no sign, only 2 digit was actually parsed, pattern say it has 2 digit.
if (ch == ' ' && (pos[0] - start) == 2 && part.count == 2) {
// Assume for example that the defaultCenturyStart is 6/18/1903.
// This means that two-digit years will be forced into the range
// 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02
// correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
// to 1904, 1905, etc. If the year is 03, then it is 2003 if the
// other fields specify a date before 6/18, or 1903 if they specify a
// date afterwards. As a result, 03 is an ambiguous year. All other
// two-digit years are unambiguous.
Date date = new Date();
@SuppressWarnings("deprecation")
int defaultCenturyStartYear = date.getYear() + 1900 - 80;
int ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
cal.setAmbiguousYear(value == ambiguousTwoDigitYear);
value += (defaultCenturyStartYear / 100) * 100 + (value < ambiguousTwoDigitYear ? 100 : 0);
}
cal.setYear(value);
return true;
}
/**
* Formats a number with the specified minimum number of digits, using zero to fill the gap.
*
* @param buf where zero padded string will be written to
* @param value the number value being formatted
* @param minWidth minimum width of the formatted string; zero will be padded to reach this width
*/
private void zeroPaddingNumber(StringBuilder buf, int value, int minWidth) {
int b = NUMBER_BASE;
for (int i = 0; i < minWidth - 1; i++) {
if (value < b) {
buf.append('0');
}
b *= NUMBER_BASE;
}
buf.append(value);
}
}