All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.i18n.shared.DateTimeFormat Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * Copyright 2008 Google Inc.
 * 
 * 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 com.google.gwt.i18n.shared;

import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.i18n.shared.impl.DateRecord;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * Formats and parses dates and times using locale-sensitive patterns.
 * 
 * 

Patterns

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
SymbolMeaningPresentationExample
Gera designatorTextAD
yyearNumber1996
Lstandalone month in yearText or NumberJuly (or) 07
Mmonth in yearText or NumberJuly (or) 07
dday in monthNumber10
hhour in am/pm (1-12)Number12
Hhour in day (0-23)Number0
mminute in hourNumber30
ssecond in minuteNumber55
Sfractional secondNumber978
Eday of weekTextTuesday
cstandalone day of weekTextTuesday
aam/pm markerTextPM
khour in day (1-24)Number24
Khour in am/pm (0-11)Number0
ztime zoneTextPacific Standard Time(see comment)
Ztime zone (RFC 822)Text-0800(See comment)
vtime zone idTextAmerica/Los_Angeles(See comment)
'escape for textDelimiter'Date='
''single quoteLiteral'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. *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
PatternCommon TimeZoneSimple TimeZone
z, zz, zzzPDTUTC-7
zzzzPacific Daylight TimeUTC-7
Z, ZZ-0700-0700
ZZZ-07:00-07:00
ZZZZGMT-07:00GMT-07:00
v, vv, vvv, vvvvAmerica/Los_AngelesEtc/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

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
PatternFormatted 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 java.lang.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() { // MUSTFIX(jat): implement return LocaleInfo.getCurrentLocale().getDateTimeFormatInfo(); } /** * 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 com.google.gwt.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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy