/* * 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.Map; /** * Formats and parses dates and times using locale-sensitive patterns. * * <h3>Patterns</h3> * * <table> * <tr> * <th>Symbol</th> * <th>Meaning</th> * <th>Presentation</th> * <th>Example</th> * </tr> * * <tr> * <td><code>G</code></td> * <td>era designator</td> * <td>Text</td> * <td><code>AD</code></td> * </tr> * * <tr> * <td><code>y</code></td> * <td>year</td> * <td>Number</td> * <td><code>1996</code></td> * </tr> * * <tr> * <td><code>L</code></td> * <td>standalone month in year</td> * <td>Text or Number</td> * <td><code>July (or) 07</code></td> * </tr> * * <tr> * <td><code>M</code></td> * <td>month in year</td> * <td>Text or Number</td> * <td><code>July (or) 07</code></td> * </tr> * * <tr> * <td><code>d</code></td> * <td>day in month</td> * <td>Number</td> * <td><code>10</code></td> * </tr> * * <tr> * <td><code>h</code></td> * <td>hour in am/pm (1-12)</td> * <td>Number</td> * <td><code>12</code></td> * </tr> * * <tr> * <td><code>H</code></td> * <td>hour in day (0-23)</td> * <td>Number</td> * <td><code>0</code></td> * </tr> * * <tr> * <td><code>m</code></td> * <td>minute in hour</td> * <td>Number</td> * <td><code>30</code></td> * </tr> * * <tr> * <td><code>s</code></td> * <td>second in minute</td> * <td>Number</td> * <td><code>55</code></td> * </tr> * * <tr> * <td><code>S</code></td> * <td>fractional second</td> * <td>Number</td> * <td><code>978</code></td> * </tr> * * <tr> * <td><code>E</code></td> * <td>day of week</td> * <td>Text</td> * <td><code>Tuesday</code></td> * </tr> * * <tr> * <td><code>c</code></td> * <td>standalone day of week</td> * <td>Text</td> * <td><code>Tuesday</code></td> * </tr> * * <tr> * <td><code>a</code></td> * <td>am/pm marker</td> * <td>Text</td> * <td><code>PM</code></td> * </tr> * * <tr> * <td><code>k</code></td> * <td>hour in day (1-24)</td> * <td>Number</td> * <td><code>24</code></td> * </tr> * * <tr> * <td><code>K</code></td> * <td>hour in am/pm (0-11)</td> * <td>Number</td> * <td><code>0</code></td> * </tr> * * <tr> * <td><code>z</code></td> * <td>time zone</td> * <td>Text</td> * <td><code>Pacific Standard Time(see comment)</code></td> * </tr> * * <tr> * <td><code>Z</code></td> * <td>time zone (RFC 822)</td> * <td>Text</td> * <td><code>-0800(See comment)</code></td> * </tr> * * <tr> * <td><code>v</code></td> * <td>time zone id</td> * <td>Text</td> * <td><code>America/Los_Angeles(See comment)</code></td> * </tr> * * <tr> * <td><code>'</code></td> * <td>escape for text</td> * <td>Delimiter</td> * <td><code>'Date='</code></td> * </tr> * * <tr> * <td><code>''</code></td> * <td>single quote</td> * <td>Literal</td> * <td><code>'o''clock'</code></td> * </tr> * </table> * * <p> * The number of pattern letters influences the format, as follows: * </p> * * <dl> * <dt>Text</dt> * <dd>if 4 or more, then use the full form; if less than 4, use short or * abbreviated form if it exists (e.g., <code>"EEEE"</code> produces * <code>"Monday"</code>, <code>"EEE"</code> produces <code>"Mon"</code>)</dd> * * <dt>Number</dt> * <dd>the minimum number of digits. Shorter numbers are zero-padded to this * amount (e.g. if <code>"m"</code> produces <code>"6"</code>, <code>"mm"</code> * produces <code>"06"</code>). Year is handled specially; that is, if the count * of 'y' is 2, the Year will be truncated to 2 digits. (e.g., if * <code>"yyyy"</code> produces <code>"1997"</code>, <code>"yy"</code> produces * <code>"97"</code>.) Unlike other fields, fractional seconds are padded on the * right with zero.</dd> * * <dt>Text or Number</dt> * <dd>3 or more, use text, otherwise use number. (e.g. <code>"M"</code> * produces <code>"1"</code>, <code>"MM"</code> produces <code>"01"</code>, * <code>"MMM"</code> produces <code>"Jan"</code>, and <code>"MMMM"</code> * produces <code>"January"</code>. Some pattern letters also treat a count * of 5 specially, meaning a single-letter abbreviation: <code>L</code>, * <code>M</code>, <code>E</code>, and <code>c</code>.</dd> * </dl> * * <p> * Any characters in the pattern that are not in the ranges of ['<code>a</code> * '..'<code>z</code>'] and ['<code>A</code>'..'<code>Z</code>'] will be treated * as quoted text. For instance, characters like '<code>:</code>', ' * <code>.</code>', '<code> </code>' (space), '<code>#</code>' and ' * <code>@</code>' will appear in the resulting time text even they are not * embraced within single quotes. * </p> * * <p> * [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. * </p> * <table> * <tr> * <th>Pattern</th> * <th>Common TimeZone</th> * <th>Simple TimeZone</th> * </tr> * <tr> * <td>z, zz, zzz</td> * <td>PDT</td> * <td>UTC-7</td> * </tr> * <tr> * <td>zzzz</td> * <td>Pacific Daylight Time</td> * <td>UTC-7</td> * </tr> * <tr> * <td>Z, ZZ</td> * <td>-0700</td> * <td>-0700</td> * </tr> * <tr> * <td>ZZZ</td> * <td>-07:00</td> * <td>-07:00</td> * </tr> * <tr> * <td>ZZZZ</td> * <td>GMT-07:00</td> * <td>GMT-07:00</td> * </tr> * <tr> * <td>v, vv, vvv, vvvv</td> * <td>America/Los_Angeles</td> * <td>Etc/GMT+7</td> * </tr> * </table> * * <h3>Parsing Dates and Times</h3> * <p> * 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. * </p> * * <p> * As with formatting (described above), the count of pattern letters determines * the parsing behavior. * </p> * * <dl> * <dt>Text</dt> * <dd>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.</dd> * * <dt>Number</dt> * <dd>the minimum number of digits.</dd> * * <dt>Text or Number</dt> * <dd>3 or more characters means use text, otherwise use number</dd> * </dl> * * <p> * 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. * </p> * <p> * [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. * </p> * * <h3>Examples</h3> * <table> * <tr> * <th>Pattern</th> * <th>Formatted Text</th> * </tr> * * <tr> * <td><code>"yyyy.MM.dd G 'at' HH:mm:ss vvvv"</code></td> * <td><code>1996.07.10 AD at 15:08:56 America/Los_Angeles</code></td> * </tr> * * <tr> * <td><code>"EEE, MMM d, ''yy"</code></td> * <td><code>Wed, July 10, '96</code></td> * </tr> * * <tr> * <td><code>"h:mm a"</code></td> * <td><code>12:08 PM</code></td> * </tr> * * <tr> * <td><code>"hh 'o''clock' a, zzzz"</code></td> * <td><code> 12 o'clock PM, Pacific Daylight Time</code></td> * </tr> * * <tr> * <td><code>"K:mm a, vvvv"</code></td> * <td><code> 0:00 PM, America/Los_Angeles</code></td> * </tr> * * <tr> * <td><code>"yyyyy.MMMMM.dd GGG hh:mm aaa"</code></td> * <td><code>01996.July.10 AD 12:08 PM</code></td> * </tr> * </table> * * <h3>Additional Parsing Considerations</h3> * <p> * When parsing a date string using the abbreviated year pattern ( * <code>"yy"</code>), 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 <code>"MM/dd/yy"</code> and a <code>DateTimeFormat</code> * object created on Jan 1, 1997, the string <code>"01/11/12"</code> would be * interpreted as Jan 11, 2012 while the string <code>"05/04/64"</code> 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 * <code>"MM/dd/yyyy"</code>, "01/11/12" parses to Jan 11, 12 A.D. * </p> * * <p> * 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. * </p> * * <p> * In the current implementation, timezone parsing only supports * <code>GMT:hhmm</code>, <code>GMT:+hhmm</code>, and <code>GMT:-hhmm</code>. * </p> * * <h3>Example</h3> {@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. * <p>Example: {@code 2008-10-03T10:29:40.046-04:00} * <p>http://code.google.com/p/google-web-toolkit/issues/detail?id=3068 * <p>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. * <p>Example: {@code Thu, 20 May 2010 17:54:50 -0700} * <p>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<String, DateTimeFormat> 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<String, DateTimeFormat>(); } /** * Get a DateTimeFormat instance for a predefined format. * * <p>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 <code>DateTimeFormat</code> object and reuse it * rather than calling this method repeatedly. * * <p>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 <code>DateTimeFormat</code> 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. * <p>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<PatternPart> patternParts = new ArrayList<PatternPart>(); 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); } StringBuffer toAppendTo = new StringBuffer(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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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(StringBuffer 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 accomodate * 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 <code>true</code> 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(); 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())) { 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 = start; 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) { StringBuffer buf = new StringBuffer(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 <code>true</code> if parsing successful, otherwise * <code>false</code> */ 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 <code>true</code> if pattern valid, otherwise <code>false</code> * */ private boolean subFormat(StringBuffer 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 * <code>false</code> 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 <code>true</code> 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); return true; case 'k': // hour (1..24) if (value < 0) { return false; } cal.setHours(value); 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 <code>true</code> if parsing successful, otherwise * <code>false</code> */ 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 <code>true</code> if parsing successful, otherwise * <code>false</code> */ 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 <code>true</code> 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 <code>true</code> if parsing successful, otherwise * <code>false</code> */ 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 <code>true</code> 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 <code>true</code> if parsing successful, otherwise * <code>false</code> */ 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 <code>true</code> 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(StringBuffer 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); } }