0001: /*
0002: * Copyright 2007 Google Inc.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
0005: * use this file except in compliance with the License. You may obtain a copy of
0006: * the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
0012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
0013: * License for the specific language governing permissions and limitations under
0014: * the License.
0015: */
0016:
0017: package com.google.gwt.i18n.client;
0018:
0019: import com.google.gwt.core.client.GWT;
0020: import com.google.gwt.i18n.client.constants.DateTimeConstants;
0021:
0022: import java.util.ArrayList;
0023: import java.util.Date;
0024:
0025: /**
0026: * Formats and parses dates and times using locale-sensitive patterns.
0027: *
0028: * <h3>Patterns</h3>
0029: *
0030: * <table>
0031: * <tr>
0032: * <th>Symbol</th>
0033: * <th>Meaning</th>
0034: * <th>Presentation</th>
0035: * <th>Example</th>
0036: * </tr>
0037: *
0038: * <tr>
0039: * <td><code>G</code></td>
0040: * <td>era designator</td>
0041: * <td>Text</td>
0042: * <td><code>AD</code></td>
0043: * </tr>
0044: *
0045: * <tr>
0046: * <td><code>y</code></td>
0047: * <td>year</td>
0048: * <td>Number</td>
0049: * <td><code>1996</code></td>
0050: * </tr>
0051: *
0052: * <tr>
0053: * <td><code>M</code></td>
0054: * <td>month in year</td>
0055: * <td>Text or Number</td>
0056: * <td><code>July (or) 07</code></td>
0057: * </tr>
0058: *
0059: * <tr>
0060: * <td><code>d</code></td>
0061: * <td>day in month</td>
0062: * <td>Number</td>
0063: * <td><code>10</code></td>
0064: * </tr>
0065: *
0066: * <tr>
0067: * <td><code>h</code></td>
0068: * <td>hour in am/pm (1-12)</td>
0069: * <td>Number</td>
0070: * <td><code>12</code></td>
0071: * </tr>
0072: *
0073: * <tr>
0074: * <td><code>H</code></td>
0075: * <td>hour in day (0-23)</td>
0076: * <td>Number</td>
0077: * <td><code>0</code></td>
0078: * </tr>
0079: *
0080: * <tr>
0081: * <td><code>m</code></td>
0082: * <td>minute in hour</td>
0083: * <td>Number</td>
0084: * <td><code>30</code></td>
0085: * </tr>
0086: *
0087: * <tr>
0088: * <td><code>s</code></td>
0089: * <td>second in minute</td>
0090: * <td>Number</td>
0091: * <td><code>55</code></td>
0092: * </tr>
0093: *
0094: * <tr>
0095: * <td><code>S</code></td>
0096: * <td>fractional second</td>
0097: * <td>Number</td>
0098: * <td><code>978</code></td>
0099: * </tr>
0100: *
0101: * <tr>
0102: * <td><code>E</code></td>
0103: * <td>day of week</td>
0104: * <td>Text</td>
0105: * <td><code>Tuesday</code></td>
0106: * </tr>
0107: *
0108: * <tr>
0109: * <td><code>a</code></td>
0110: * <td>am/pm marker</td>
0111: * <td>Text</td>
0112: * <td><code>PM</code></td>
0113: * </tr>
0114: *
0115: * <tr>
0116: * <td><code>k</code></td>
0117: * <td>hour in day (1-24)</td>
0118: * <td>Number</td>
0119: * <td><code>24</code></td>
0120: * </tr>
0121: *
0122: * <tr>
0123: * <td><code>K</code></td>
0124: * <td>hour in am/pm (0-11)</td>
0125: * <td>Number</td>
0126: * <td><code>0</code></td>
0127: * </tr>
0128: *
0129: * <tr>
0130: * <td><code>z</code></td>
0131: * <td>time zone</td>
0132: * <td>Text</td>
0133: * <td><code>Pacific Standard Time</code></td>
0134: * </tr>
0135: *
0136: * <tr>
0137: * <td><code>Z</code></td>
0138: * <td>time zone (RFC 822)</td>
0139: * <td>Number</td>
0140: * <td><code>-0800</code></td>
0141: * </tr>
0142: *
0143: * <tr>
0144: * <td><code>v</code></td>
0145: * <td>time zone (generic)</td>
0146: * <td>Text</td>
0147: * <td><code>Pacific Time</code></td>
0148: * </tr>
0149: *
0150: * <tr>
0151: * <td><code>'</code></td>
0152: * <td>escape for text</td>
0153: * <td>Delimiter</td>
0154: * <td><code>'Date='</code></td>
0155: * </tr>
0156: *
0157: * <tr>
0158: * <td><code>''</code></td>
0159: * <td>single quote</td>
0160: * <td>Literal</td>
0161: * <td><code>'o''clock'</code></td>
0162: * </tr>
0163: * </table>
0164: *
0165: * <p>
0166: * The number of pattern letters influences the format, as follows:
0167: * </p>
0168: *
0169: * <dl>
0170: * <dt>Text</dt>
0171: * <dd>if 4 or more, then use the full form; if less than 4, use short or
0172: * abbreviated form if it exists (e.g., <code>"EEEE"</code> produces
0173: * <code>"Monday"</code>, <code>"EEE"</code> produces <code>"Mon"</code>)</dd>
0174: *
0175: * <dt>Number</dt>
0176: * <dd>the minimum number of digits. Shorter numbers are zero-padded to this
0177: * amount (e.g. if <code>"m"</code> produces <code>"6"</code>,
0178: * <code>"mm"</code> produces <code>"06"</code>). Year is handled
0179: * specially; that is, if the count of 'y' is 2, the Year will be truncated to 2
0180: * digits. (e.g., if <code>"yyyy"</code> produces <code>"1997"</code>,
0181: * <code>"yy"</code> produces <code>"97"</code>.) Unlike other fields,
0182: * fractional seconds are padded on the right with zero.</dd>
0183: *
0184: * <dt>Text or Number</dt>
0185: * <dd>3 or more, use text, otherwise use number. (e.g. <code>"M"</code>
0186: * produces <code>"1"</code>, <code>"MM"</code> produces <code>"01"</code>,
0187: * <code>"MMM"</code> produces <code>"Jan"</code>, and <code>"MMMM"</code>
0188: * produces <code>"January"</code>.</dd>
0189: * </dl>
0190: *
0191: * <p>
0192: * Any characters in the pattern that are not in the ranges of ['<code>a</code>'..'<code>z</code>']
0193: * and ['<code>A</code>'..'<code>Z</code>'] will be treated as quoted
0194: * text. For instance, characters like '<code>:</code>', '<code>.</code>', '<code> </code>'
0195: * (space), '<code>#</code>' and '<code>@</code>' will appear in the
0196: * resulting time text even they are not embraced within single quotes.
0197: * </p>
0198: *
0199: * <h3>Parsing Dates and Times</h3>
0200: * <p>
0201: * This implementation could parse partial date/time. Current date will be
0202: * used to fill in the unavailable date part. 00:00:00 will be used to
0203: * fill in the time part.
0204: * </p>
0205: *
0206: * <p>
0207: * As with formatting (described above), the count of pattern letters determine
0208: * the parsing behavior.
0209: * </p>
0210: *
0211: * <dl>
0212: * <dt>Text</dt>
0213: * <dd>4 or more pattern letters--use full form, less than 4--use short or
0214: * abbreviated form if one exists. In parsing, we will always try long format,
0215: * then short.</dd>
0216: *
0217: * <dt>Number</dt>
0218: * <dd>the minimum number of digits.</dd>
0219: *
0220: * <dt>Text or Number</dt>
0221: * <dd>3 or more characters means use text, otherwise use number</dd>
0222: * </dl>
0223: *
0224: * <p>
0225: * Although the current pattern specification doesn't not specify behavior for
0226: * all letters, it may in the future. It is strongly discouraged to used
0227: * unspecified letters as literal text without being surrounded by quotes.
0228: * </p>
0229: *
0230: * <h3>Examples</h3>
0231: * <table>
0232: * <tr>
0233: * <th>Pattern</th>
0234: * <th>Formatted Text</th>
0235: * </tr>
0236: *
0237: * <tr>
0238: * <td><code>"yyyy.MM.dd G 'at' HH:mm:ss vvvv"</code></td>
0239: * <td><code>1996.07.10 AD at 15:08:56 Pacific Time</code></td>
0240: * </tr>
0241: *
0242: * <tr>
0243: * <td><code>"EEE, MMM d, ''yy"</code></td>
0244: * <td><code>Wed, July 10, '96</code></td>
0245: * </tr>
0246: *
0247: * <tr>
0248: * <td><code>"h:mm a"</code></td>
0249: * <td><code>12:08 PM</code></td>
0250: * </tr>
0251: *
0252: * <tr>
0253: * <td><code>"hh 'o''clock' a, zzzz"</code></td>
0254: * <td><code> 12 o'clock PM, Pacific Daylight Time</code></td>
0255: * </tr>
0256: *
0257: * <tr>
0258: * <td><code>"K:mm a, vvv"</code></td>
0259: * <td><code> 0:00 PM, PT</code></td>
0260: * </tr>
0261: *
0262: * <tr>
0263: * <td><code>"yyyyy.MMMMM.dd GGG hh:mm aaa"</code></td>
0264: * <td><code>01996.July.10 AD 12:08 PM</code></td>
0265: * </tr>
0266: * </table>
0267: *
0268: * <h3>Additional Parsing Considerations</h3>
0269: * <p>
0270: * When parsing a date string using the abbreviated year pattern (<code>"yy"</code>),
0271: * the parser must interpret the abbreviated year relative to some century. It
0272: * does this by adjusting dates to be within 80 years before and 20 years after
0273: * the time the parser instance is created. For example, using a pattern of
0274: * <code>"MM/dd/yy"</code> and a <code>DateTimeFormat</code> object created
0275: * on Jan 1, 1997, the string <code>"01/11/12"</code> would be interpreted as
0276: * Jan 11, 2012 while the string <code>"05/04/64"</code> would be interpreted
0277: * as May 4, 1964. During parsing, only strings consisting of exactly two
0278: * digits, as defined by {@link java.lang.Character#isDigit(char)}, will be
0279: * parsed into the default century. If the year pattern does not have exactly
0280: * two 'y' characters, the year is interpreted literally, regardless of the
0281: * number of digits. For example, using the pattern <code>"MM/dd/yyyy"</code>,
0282: * "01/11/12" parses to Jan 11, 12 A.D.
0283: * </p>
0284: *
0285: * <p>
0286: * When numeric fields abut one another directly, with no intervening delimiter
0287: * characters, they constitute a run of abutting numeric fields. Such runs are
0288: * parsed specially. For example, the format "HHmmss" parses the input text
0289: * "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and fails to
0290: * parse "1234". In other words, the leftmost field of the run is flexible,
0291: * while the others keep a fixed width. If the parse fails anywhere in the run,
0292: * then the leftmost field is shortened by one character, and the entire run is
0293: * parsed again. This is repeated until either the parse succeeds or the
0294: * leftmost field is one character in length. If the parse still fails at that
0295: * point, the parse of the run fails.
0296: * </p>
0297: *
0298: * <p>
0299: * In the current implementation, timezone parsing only supports
0300: * <code>GMT:hhmm</code>, <code>GMT:+hhmm</code>, and
0301: * <code>GMT:-hhmm</code>.
0302: * </p>
0303: */
0304: public class DateTimeFormat {
0305: /**
0306: * Class PatternPart holds a "compiled" pattern part.
0307: */
0308: private class PatternPart {
0309: public String text;
0310: public int count; // 0 has a special meaning, it stands for literal
0311: public boolean abutStart;
0312:
0313: public PatternPart(String txt, int cnt) {
0314: text = txt;
0315: count = cnt;
0316: abutStart = false;
0317: }
0318: }
0319:
0320: private static final int FULL_DATE_FORMAT = 0;
0321: private static final int LONG_DATE_FORMAT = 1;
0322: private static final int MEDIUM_DATE_FORMAT = 2;
0323: private static final int SHORT_DATE_FORMAT = 3;
0324: private static final int FULL_TIME_FORMAT = 0;
0325: private static final int LONG_TIME_FORMAT = 1;
0326: private static final int MEDIUM_TIME_FORMAT = 2;
0327:
0328: private static final int SHORT_TIME_FORMAT = 3;
0329: private static final int NUMBER_BASE = 10;
0330: private static final int JS_START_YEAR = 1900;
0331:
0332: private static DateTimeFormat cachedFullDateFormat;
0333: private static DateTimeFormat cachedLongDateFormat;
0334: private static DateTimeFormat cachedMediumDateFormat;
0335:
0336: private static DateTimeFormat cachedShortDateFormat;
0337: private static DateTimeFormat cachedFullTimeFormat;
0338: private static DateTimeFormat cachedLongTimeFormat;
0339: private static DateTimeFormat cachedMediumTimeFormat;
0340:
0341: private static DateTimeFormat cachedShortTimeFormat;
0342: private static DateTimeFormat cachedFullDateTimeFormat;
0343: private static DateTimeFormat cachedLongDateTimeFormat;
0344: private static DateTimeFormat cachedMediumDateTimeFormat;
0345: private static DateTimeFormat cachedShortDateTimeFormat;
0346:
0347: private static final DateTimeConstants defaultDateTimeConstants = (DateTimeConstants) GWT
0348: .create(DateTimeConstants.class);
0349:
0350: private static final String PATTERN_CHARS = "GyMdkHmsSEDahKzZv";
0351:
0352: private static final String NUMERIC_FORMAT_CHARS = "MydhHmsSDkK";
0353:
0354: private static final String WHITE_SPACE = " \t\r\n";
0355:
0356: private static final String GMT = "GMT";
0357:
0358: private static final int MINUTES_PER_HOUR = 60;
0359:
0360: /**
0361: * Returns a format object using the specified pattern and the date time
0362: * constants for the default locale. If you need to format or parse repeatedly
0363: * using the same pattern, it is highly recommended that you cache the
0364: * returned <code>DateTimeFormat</code> object and reuse it rather than
0365: * calling this method repeatedly.
0366: *
0367: * @param pattern string to specify how the date should be formatted
0368: *
0369: * @return a <code>DateTimeFormat</code> object that can be used for format
0370: * or parse date/time values matching the specified pattern
0371: *
0372: * @throws IllegalArgumentException if the specified pattern could not be
0373: * parsed
0374: */
0375: public static DateTimeFormat getFormat(String pattern) {
0376: return new DateTimeFormat(pattern, defaultDateTimeConstants);
0377: }
0378:
0379: public static DateTimeFormat getFullDateFormat() {
0380: if (cachedFullDateFormat == null) {
0381: String pattern = defaultDateTimeConstants.dateFormats()[FULL_DATE_FORMAT];
0382: cachedFullDateFormat = new DateTimeFormat(pattern);
0383: }
0384: return cachedFullDateFormat;
0385: }
0386:
0387: public static DateTimeFormat getFullDateTimeFormat() {
0388: if (cachedFullDateTimeFormat == null) {
0389: String pattern = defaultDateTimeConstants.dateFormats()[FULL_DATE_FORMAT]
0390: + " "
0391: + defaultDateTimeConstants.timeFormats()[FULL_TIME_FORMAT];
0392: cachedFullDateTimeFormat = new DateTimeFormat(pattern);
0393: }
0394: return cachedFullDateTimeFormat;
0395: }
0396:
0397: public static DateTimeFormat getFullTimeFormat() {
0398: if (cachedFullTimeFormat == null) {
0399: String pattern = defaultDateTimeConstants.timeFormats()[FULL_TIME_FORMAT];
0400: cachedFullTimeFormat = new DateTimeFormat(pattern);
0401: }
0402: return cachedFullTimeFormat;
0403: }
0404:
0405: public static DateTimeFormat getLongDateFormat() {
0406: if (cachedLongDateFormat == null) {
0407: String pattern = defaultDateTimeConstants.dateFormats()[LONG_DATE_FORMAT];
0408: cachedLongDateFormat = new DateTimeFormat(pattern);
0409: }
0410: return cachedLongDateFormat;
0411: }
0412:
0413: public static DateTimeFormat getLongDateTimeFormat() {
0414: if (cachedLongDateTimeFormat == null) {
0415: String pattern = defaultDateTimeConstants.dateFormats()[LONG_DATE_FORMAT]
0416: + " "
0417: + defaultDateTimeConstants.timeFormats()[LONG_TIME_FORMAT];
0418: cachedLongDateTimeFormat = new DateTimeFormat(pattern);
0419: }
0420: return cachedLongDateTimeFormat;
0421: }
0422:
0423: public static DateTimeFormat getLongTimeFormat() {
0424: if (cachedLongTimeFormat == null) {
0425: String pattern = defaultDateTimeConstants.timeFormats()[LONG_TIME_FORMAT];
0426: cachedLongTimeFormat = new DateTimeFormat(pattern);
0427: }
0428: return cachedLongTimeFormat;
0429: }
0430:
0431: public static DateTimeFormat getMediumDateFormat() {
0432: if (cachedMediumDateFormat == null) {
0433: String pattern = defaultDateTimeConstants.dateFormats()[MEDIUM_DATE_FORMAT];
0434: cachedMediumDateFormat = new DateTimeFormat(pattern);
0435: }
0436: return cachedMediumDateFormat;
0437: }
0438:
0439: public static DateTimeFormat getMediumDateTimeFormat() {
0440: if (cachedMediumDateTimeFormat == null) {
0441: String pattern = defaultDateTimeConstants.dateFormats()[MEDIUM_DATE_FORMAT]
0442: + " "
0443: + defaultDateTimeConstants.timeFormats()[MEDIUM_TIME_FORMAT];
0444: cachedMediumDateTimeFormat = new DateTimeFormat(pattern);
0445: }
0446: return cachedMediumDateTimeFormat;
0447: }
0448:
0449: public static DateTimeFormat getMediumTimeFormat() {
0450: if (cachedMediumTimeFormat == null) {
0451: String pattern = defaultDateTimeConstants.timeFormats()[MEDIUM_TIME_FORMAT];
0452: cachedMediumTimeFormat = new DateTimeFormat(pattern);
0453: }
0454: return cachedMediumTimeFormat;
0455: }
0456:
0457: public static DateTimeFormat getShortDateFormat() {
0458: if (cachedShortDateFormat == null) {
0459: String pattern = defaultDateTimeConstants.dateFormats()[SHORT_DATE_FORMAT];
0460: cachedShortDateFormat = new DateTimeFormat(pattern);
0461: }
0462: return cachedShortDateFormat;
0463: }
0464:
0465: public static DateTimeFormat getShortDateTimeFormat() {
0466: if (cachedShortDateTimeFormat == null) {
0467: String pattern = defaultDateTimeConstants.dateFormats()[SHORT_DATE_FORMAT]
0468: + " "
0469: + defaultDateTimeConstants.timeFormats()[SHORT_TIME_FORMAT];
0470: cachedShortDateTimeFormat = new DateTimeFormat(pattern);
0471: }
0472: return cachedShortDateTimeFormat;
0473: }
0474:
0475: public static DateTimeFormat getShortTimeFormat() {
0476: if (cachedShortTimeFormat == null) {
0477: String pattern = defaultDateTimeConstants.timeFormats()[SHORT_TIME_FORMAT];
0478: cachedShortTimeFormat = new DateTimeFormat(pattern);
0479: }
0480: return cachedShortTimeFormat;
0481: }
0482:
0483: private final ArrayList<PatternPart> patternParts = new ArrayList<PatternPart>();
0484:
0485: private final DateTimeConstants dateTimeConstants;
0486:
0487: private final String pattern;
0488:
0489: /**
0490: * Constructs a format object using the specified pattern and the date time
0491: * constants for the default locale.
0492: *
0493: * @param pattern string pattern specification
0494: */
0495: protected DateTimeFormat(String pattern) {
0496: this (pattern, defaultDateTimeConstants);
0497: }
0498:
0499: /**
0500: * Constructs a format object using the specified pattern and user-supplied
0501: * date time constants.
0502: *
0503: * @param pattern string pattern specification
0504: * @param dateTimeConstants locale specific symbol collection
0505: */
0506: protected DateTimeFormat(String pattern,
0507: DateTimeConstants dateTimeConstants) {
0508: this .pattern = pattern;
0509: this .dateTimeConstants = dateTimeConstants;
0510:
0511: /*
0512: * Even though the pattern is only compiled for use in parsing and parsing
0513: * is far less common than formatting, the pattern is still parsed eagerly
0514: * here to fail fast in case the pattern itself is malformed.
0515: */
0516: parsePattern(pattern);
0517: }
0518:
0519: /**
0520: * Format a date object.
0521: *
0522: * @param date the date object being formatted
0523: *
0524: * @return formatted date representation
0525: */
0526: public String format(Date date) {
0527: StringBuffer toAppendTo = new StringBuffer(64);
0528: int j, n = pattern.length();
0529: for (int i = 0; i < n;) {
0530: char ch = pattern.charAt(i);
0531: if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
0532: // ch is a date-time pattern character to be interpreted by subFormat().
0533: // Count the number of times it is repeated.
0534: for (j = i + 1; j < n && pattern.charAt(j) == ch; ++j) {
0535: }
0536: subFormat(toAppendTo, ch, j - i, date);
0537: i = j;
0538: } else if (ch == '\'') {
0539: // Handle an entire quoted string, included embedded
0540: // doubled apostrophes (as in 'o''clock').
0541:
0542: // i points after '.
0543: ++i;
0544:
0545: // If start with '', just add ' and continue.
0546: if (i < n && pattern.charAt(i) == '\'') {
0547: toAppendTo.append('\'');
0548: ++i;
0549: continue;
0550: }
0551:
0552: // Otherwise add the quoted string.
0553: boolean trailQuote = false;
0554: while (!trailQuote) {
0555: // j points to next ' or EOS.
0556: j = i;
0557: while (j < n && pattern.charAt(j) != '\'') {
0558: ++j;
0559: }
0560:
0561: if (j >= n) {
0562: // Trailing ' (pathological).
0563: throw new IllegalArgumentException(
0564: "Missing trailing \'");
0565: }
0566:
0567: // Look ahead to detect '' within quotes.
0568: if (j + 1 < n && pattern.charAt(j + 1) == '\'') {
0569: ++j;
0570: } else {
0571: trailQuote = true;
0572: }
0573: toAppendTo.append(pattern.substring(i, j));
0574: i = j + 1;
0575: }
0576: } else {
0577: // Append unquoted literal characters.
0578: toAppendTo.append(ch);
0579: ++i;
0580: }
0581: }
0582:
0583: return toAppendTo.toString();
0584: }
0585:
0586: public String getPattern() {
0587: return pattern;
0588: }
0589:
0590: /**
0591: * Parses text to produce a {@link Date} value. An
0592: * {@link IllegalArgumentException} is thrown if either the text is empty or
0593: * if the parse does not consume all characters of the text.
0594: *
0595: * @param text the string being parsed
0596: * @return a parsed date/time value
0597: * @throws IllegalArgumentException if the entire text could not be converted
0598: * into a number
0599: */
0600: public Date parse(String text) {
0601: Date curDate = new Date();
0602: Date date = new Date(curDate.getYear(), curDate.getMonth(),
0603: curDate.getDate());
0604: int charsConsumed = parse(text, 0, date);
0605: if (charsConsumed == 0 || charsConsumed < text.length()) {
0606: throw new IllegalArgumentException(text);
0607: }
0608: return date;
0609: }
0610:
0611: /**
0612: * This method parses the input string, fill its value into a {@link Date}.
0613: *
0614: * @param text the string that need to be parsed
0615: * @param start the character position in "text" where parsing should start
0616: * @param date the date object that will hold parsed value
0617: *
0618: * @return 0 if parsing failed, otherwise the number of characters advanced
0619: */
0620: public int parse(String text, int start, Date date) {
0621: DateRecord cal = new DateRecord();
0622: int[] parsePos = { start };
0623:
0624: // For parsing abutting numeric fields. 'abutPat' is the
0625: // offset into 'pattern' of the first of 2 or more abutting
0626: // numeric fields. 'abutStart' is the offset into 'text'
0627: // where parsing the fields begins. 'abutPass' starts off as 0
0628: // and increments each time we try to parse the fields.
0629: int abutPat = -1; // If >=0, we are in a run of abutting numeric fields.
0630: int abutStart = 0;
0631: int abutPass = 0;
0632:
0633: for (int i = 0; i < patternParts.size(); ++i) {
0634: PatternPart part = patternParts.get(i);
0635:
0636: if (part.count > 0) {
0637: if (abutPat < 0 && part.abutStart) {
0638: abutPat = i;
0639: abutStart = start;
0640: abutPass = 0;
0641: }
0642:
0643: // Handle fields within a run of abutting numeric fields. Take
0644: // the pattern "HHmmss" as an example. We will try to parse
0645: // 2/2/2 characters of the input text, then if that fails,
0646: // 1/2/2. We only adjust the width of the leftmost field; the
0647: // others remain fixed. This allows "123456" => 12:34:56, but
0648: // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
0649: // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
0650: if (abutPat >= 0) {
0651: // If we are at the start of a run of abutting fields, then
0652: // shorten this field in each pass. If we can't shorten
0653: // this field any more, then the parse of this set of
0654: // abutting numeric fields has failed.
0655: int count = part.count;
0656: if (i == abutPat) {
0657: count -= abutPass++;
0658: if (count == 0) {
0659: return 0;
0660: }
0661: }
0662:
0663: if (!subParse(text, parsePos, part, count, cal)) {
0664: // If the parse fails anywhere in the run, back up to the
0665: // start of the run and retry.
0666: i = abutPat - 1;
0667: parsePos[0] = abutStart;
0668: continue;
0669: }
0670: } else {
0671: // Handle non-numeric fields and non-abutting numeric fields.
0672: abutPat = -1;
0673: if (!subParse(text, parsePos, part, 0, cal)) {
0674: return 0;
0675: }
0676: }
0677: } else {
0678: // Handle literal pattern characters. These are any
0679: // quoted characters and non-alphabetic unquoted characters.
0680: abutPat = -1;
0681: // A run of white space in the pattern matches a run
0682: // of white space in the input text.
0683: if (part.text.charAt(0) == ' ') {
0684: // Advance over run in input text.
0685: int s = parsePos[0];
0686: skipSpace(text, parsePos);
0687:
0688: // Must see at least one white space char in input.
0689: if (parsePos[0] > s) {
0690: continue;
0691: }
0692: } else if (text.startsWith(part.text, parsePos[0])) {
0693: parsePos[0] += part.text.length();
0694: continue;
0695: }
0696:
0697: // We fall through to this point if the match fails.
0698: return 0;
0699: }
0700: }
0701:
0702: if (!cal.calcDate(date)) {
0703: return 0;
0704: }
0705:
0706: // Return progress.
0707: return parsePos[0] - start;
0708: }
0709:
0710: /**
0711: * Method append current content in buf as pattern part if there is any, and
0712: * clear buf for next part.
0713: *
0714: * @param buf pattern part text specification
0715: * @param count pattern part repeat count
0716: */
0717: private void addPart(StringBuffer buf, int count) {
0718: if (buf.length() > 0) {
0719: patternParts.add((new PatternPart(buf.toString(), count)));
0720: buf.setLength(0);
0721: }
0722: }
0723:
0724: /**
0725: * Generate GMT timezone string for given date.
0726: *
0727: * @param buf where timezone string will be appended to
0728: * @param date whose value being evaluated
0729: */
0730: private void appendGMT(StringBuffer buf, Date date) {
0731: int value = -date.getTimezoneOffset();
0732:
0733: if (value < 0) {
0734: buf.append("GMT-");
0735: value = -value; // suppress the '-' sign for text display.
0736: } else {
0737: buf.append("GMT+");
0738: }
0739:
0740: zeroPaddingNumber(buf, value / MINUTES_PER_HOUR, 2);
0741: buf.append(':');
0742: zeroPaddingNumber(buf, value % MINUTES_PER_HOUR, 2);
0743: }
0744:
0745: /**
0746: * Formats (0..11) Hours field according to pattern specified.
0747: *
0748: * @param buf where formatted string will be appended to
0749: * @param count number of time pattern char repeats; this controls how a field
0750: * should be formatted
0751: * @param date hold the date object to be formatted
0752: */
0753: private void format0To11Hours(StringBuffer buf, int count, Date date) {
0754: int value = date.getHours() % 12;
0755: zeroPaddingNumber(buf, value, count);
0756: }
0757:
0758: /**
0759: * Formats (0..23) Hours field according to pattern specified.
0760: *
0761: * @param buf where formatted string will be appended to
0762: * @param count number of time pattern char repeats; this controls how a field
0763: * should be formatted
0764: * @param date hold the date object to be formatted
0765: */
0766: private void format0To23Hours(StringBuffer buf, int count, Date date) {
0767: int value = date.getHours();
0768: zeroPaddingNumber(buf, value, count);
0769: }
0770:
0771: /**
0772: * Formats (1..12) Hours field according to pattern specified.
0773: *
0774: * @param buf where formatted string will be appended to
0775: * @param count number of time pattern char repeats; this controls how a field
0776: * should be formatted
0777: * @param date hold the date object to be formatted
0778: */
0779: private void format1To12Hours(StringBuffer buf, int count, Date date) {
0780: int value = date.getHours() % 12;
0781: if (value == 0) {
0782: zeroPaddingNumber(buf, 12, count);
0783: } else {
0784: zeroPaddingNumber(buf, value, count);
0785: }
0786: }
0787:
0788: /**
0789: * Formats (1..24) Hours field according to pattern specified.
0790: *
0791: * @param buf where formatted string will be appended to
0792: * @param count number of time pattern char repeats; this controls how a field
0793: * should be formatted
0794: * @param date hold the date object to be formatted
0795: */
0796: private void format24Hours(StringBuffer buf, int count, Date date) {
0797: int value = date.getHours();
0798: if (value == 0) {
0799: zeroPaddingNumber(buf, 24, count);
0800: } else {
0801: zeroPaddingNumber(buf, value, count);
0802: }
0803: }
0804:
0805: /**
0806: * Formats AM/PM field according to pattern specified.
0807: *
0808: * @param buf where formatted string will be appended to
0809: * @param count number of time pattern char repeats; this controls how a field
0810: * should be formatted
0811: * @param date hold the date object to be formatted
0812: */
0813: private void formatAmPm(StringBuffer buf, int count, Date date) {
0814: if (date.getHours() >= 12 && date.getHours() < 24) {
0815: buf.append(dateTimeConstants.ampms()[1]);
0816: } else {
0817: buf.append(dateTimeConstants.ampms()[0]);
0818: }
0819: }
0820:
0821: /**
0822: * Formats Date field according to pattern specified.
0823: *
0824: * @param buf where formatted string will be appended to
0825: * @param count number of time pattern char repeats; this controls how a field
0826: * should be formatted
0827: * @param date hold the date object to be formatted
0828: */
0829: private void formatDate(StringBuffer buf, int count, Date date) {
0830: int value = date.getDate();
0831: zeroPaddingNumber(buf, value, count);
0832: }
0833:
0834: /**
0835: * Formats Day of week field according to pattern specified.
0836: *
0837: * @param buf where formatted string will be appended to
0838: * @param count number of time pattern char repeats; this controls how a field
0839: * should be formatted
0840: * @param date hold the date object to be formatted
0841: */
0842: private void formatDayOfWeek(StringBuffer buf, int count, Date date) {
0843: int value = date.getDay();
0844: if (count >= 4) {
0845: buf.append(dateTimeConstants.weekdays()[value]);
0846: } else {
0847: buf.append(dateTimeConstants.shortWeekdays()[value]);
0848: }
0849: }
0850:
0851: /**
0852: * Formats Era field according to pattern specified.
0853: *
0854: * @param buf where formatted string will be appended to
0855: * @param count number of time pattern char repeats; this controls how a field
0856: * should be formatted
0857: * @param date hold the date object to be formatted
0858: */
0859: private void formatEra(StringBuffer buf, int count, Date date) {
0860: int value = date.getYear() >= -JS_START_YEAR ? 1 : 0;
0861: if (count >= 4) {
0862: buf.append(dateTimeConstants.eraNames()[value]);
0863: } else {
0864: buf.append(dateTimeConstants.eras()[value]);
0865: }
0866: }
0867:
0868: /**
0869: * Formats Fractional seconds field according to pattern specified.
0870: *
0871: * @param buf where formatted string will be appended to
0872: * @param count number of time pattern char repeats; this controls how a field
0873: * should be formatted
0874: * @param date hold the date object to be formatted
0875: */
0876: private void formatFractionalSeconds(StringBuffer buf, int count,
0877: Date date) {
0878: // Fractional seconds should be left-justified, ie. zero must be padded
0879: // from left. For example, if value in milliseconds is 5, and count is 3,
0880: // the output need to be "005".
0881: int value = (int) (date.getTime() % 1000);
0882: if (count == 1) {
0883: value = (value + 50) / 100; // Round to 100ms.
0884: buf.append(Integer.toString(value));
0885: } else if (count == 2) {
0886: value = (value + 5) / 10; // Round to 10ms.
0887: zeroPaddingNumber(buf, value, 2);
0888: } else {
0889: zeroPaddingNumber(buf, value, 3);
0890:
0891: if (count > 3) {
0892: zeroPaddingNumber(buf, 0, count - 3);
0893: }
0894: }
0895: }
0896:
0897: /**
0898: * Formats Minutes field according to pattern specified.
0899: *
0900: * @param buf where formatted string will be appended to
0901: * @param count number of time pattern char repeats; this controls how a field
0902: * should be formatted
0903: * @param date hold the date object to be formatted
0904: */
0905: private void formatMinutes(StringBuffer buf, int count, Date date) {
0906: int value = date.getMinutes();
0907: zeroPaddingNumber(buf, value, count);
0908: }
0909:
0910: /**
0911: * Formats Month field according to pattern specified.
0912: *
0913: * @param buf where formatted string will be appended to
0914: * @param count number of time pattern char repeats; this controls how a field
0915: * should be formatted
0916: * @param date hold the date object to be formatted
0917: */
0918: private void formatMonth(StringBuffer buf, int count, Date date) {
0919: int value = date.getMonth();
0920: switch (count) {
0921: case 5:
0922: buf.append(dateTimeConstants.narrowMonths()[value]);
0923: break;
0924: case 4:
0925: buf.append(dateTimeConstants.standaloneMonths()[value]);
0926: break;
0927: case 3:
0928: buf.append(dateTimeConstants.shortMonths()[value]);
0929: break;
0930: default:
0931: zeroPaddingNumber(buf, value + 1, count);
0932: }
0933: }
0934:
0935: /**
0936: * Formats Quarter field according to pattern specified.
0937: *
0938: * @param buf where formatted string will be appended to
0939: * @param count number of time pattern char repeats; this controls how a field
0940: * should be formatted
0941: * @param date hold the date object to be formatted
0942: */
0943: private void formatQuarter(StringBuffer buf, int count, Date date) {
0944: int value = date.getMonth() / 3;
0945: if (count < 4) {
0946: buf.append(dateTimeConstants.shortQuarters()[value]);
0947: } else {
0948: buf.append(dateTimeConstants.quarters()[value]);
0949: }
0950: }
0951:
0952: /**
0953: * Formats Seconds field according to pattern specified.
0954: *
0955: * @param buf where formatted string will be appended to
0956: * @param count number of time pattern char repeats; this controls how a field
0957: * should be formatted
0958: * @param date hold the date object to be formatted
0959: */
0960: private void formatSeconds(StringBuffer buf, int count, Date date) {
0961: int value = date.getSeconds();
0962: zeroPaddingNumber(buf, value, count);
0963: }
0964:
0965: /**
0966: * Formats Standalone weekday field according to pattern specified.
0967: *
0968: * @param buf where formatted string will be appended to
0969: * @param count number of time pattern char repeats; this controls how a field
0970: * should be formatted
0971: * @param date hold the date object to be formatted
0972: */
0973: private void formatStandaloneDay(StringBuffer buf, int count,
0974: Date date) {
0975: int value = date.getDay();
0976: if (count == 5) {
0977: buf
0978: .append(dateTimeConstants
0979: .standaloneNarrowWeekdays()[value]);
0980: } else if (count == 4) {
0981: buf.append(dateTimeConstants.standaloneWeekdays()[value]);
0982: } else if (count == 3) {
0983: buf
0984: .append(dateTimeConstants.standaloneShortWeekdays()[value]);
0985: } else {
0986: zeroPaddingNumber(buf, value, 1);
0987: }
0988: }
0989:
0990: /**
0991: * Formats Standalone Month field according to pattern specified.
0992: *
0993: * @param buf where formatted string will be appended to
0994: * @param count number of time pattern char repeats; this controls how a field
0995: * should be formatted
0996: * @param date hold the date object to be formatted
0997: */
0998: private void formatStandaloneMonth(StringBuffer buf, int count,
0999: Date date) {
1000: int value = date.getMonth();
1001: if (count == 5) {
1002: buf
1003: .append(dateTimeConstants.standaloneNarrowMonths()[value]);
1004: } else if (count == 4) {
1005: buf.append(dateTimeConstants.standaloneMonths()[value]);
1006: } else if (count == 3) {
1007: buf
1008: .append(dateTimeConstants.standaloneShortMonths()[value]);
1009: } else {
1010: zeroPaddingNumber(buf, value + 1, count);
1011: }
1012: }
1013:
1014: /**
1015: * Formats Timezone field following RFC.
1016: *
1017: * @param buf where formatted string will be appended to
1018: * @param count number of time pattern char repeats; this controls how a field
1019: * should be formatted
1020: * @param date hold the date object to be formatted
1021: */
1022: private void formatTimeZoneRFC(StringBuffer buf, int count,
1023: Date date) {
1024: if (count < 4) {
1025: // 'short' (standard Java) form, must use ASCII digits
1026: int val = date.getTimezoneOffset();
1027: char sign = '-';
1028: if (val < 0) {
1029: val = -val;
1030: sign = '+';
1031: }
1032:
1033: val = (val / 3) * 5 + (val % MINUTES_PER_HOUR); // minutes => KKmm
1034: buf.append(sign);
1035: zeroPaddingNumber(buf, val, 4);
1036: } else {
1037: appendGMT(buf, date);
1038: }
1039: }
1040:
1041: /**
1042: * Formats Year field according to pattern specified. Javascript Date object
1043: * seems incapable handling 1BC and year before. It can show you year 0 which
1044: * does not exists. following we just keep consistent with javascript's
1045: * toString method. But keep in mind those things should be unsupported.
1046: *
1047: * @param buf where formatted string will be appended to
1048: * @param count number of time pattern char repeats; this controls how a field
1049: * should be formatted
1050: * @param date hold the date object to be formatted
1051: */
1052: private void formatYear(StringBuffer buf, int count, Date date) {
1053: int value = date.getYear() + JS_START_YEAR;
1054: if (value < 0) {
1055: value = -value;
1056: }
1057: if (count == 2) {
1058: zeroPaddingNumber(buf, value % 100, 2);
1059: } else {
1060: // count != 2
1061: buf.append(Integer.toString(value));
1062: }
1063: }
1064:
1065: /**
1066: * Method getNextCharCountInPattern calculate character repeat count in
1067: * pattern.
1068: *
1069: * @param pattern describe the format of date string that need to be parsed
1070: * @param start the position of pattern character
1071: * @return repeat count
1072: */
1073: private int getNextCharCountInPattern(String pattern, int start) {
1074: char ch = pattern.charAt(start);
1075: int next = start + 1;
1076: while (next < pattern.length() && pattern.charAt(next) == ch) {
1077: ++next;
1078: }
1079: return next - start;
1080: }
1081:
1082: /**
1083: * Method identifies the start of a run of abutting numeric fields. Take the
1084: * pattern "HHmmss" as an example. We will try to parse 2/2/2 characters of
1085: * the input text, then if that fails, 1/2/2. We only adjust the width of the
1086: * leftmost field; the others remain fixed. This allows "123456" => 12:34:56,
1087: * but "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,
1088: * 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric
1089: * fields will be marked as abutStart, its width can be reduced to accomodate
1090: * others.
1091: */
1092: private void identifyAbutStart() {
1093: // 'abut' parts are continuous numeric parts. abutStart is the switch
1094: // point from non-abut to abut.
1095: boolean abut = false;
1096:
1097: int len = patternParts.size();
1098: for (int i = 0; i < len; i++) {
1099: if (isNumeric(patternParts.get(i))) {
1100: // If next part is not following abut sequence, and isNumeric.
1101: if (!abut && i + 1 < len
1102: && isNumeric(patternParts.get(i + 1))) {
1103: abut = true;
1104: patternParts.get(i).abutStart = true;
1105: }
1106: } else {
1107: abut = false;
1108: }
1109: }
1110: }
1111:
1112: /**
1113: * Method checks if the pattern part is a numeric field.
1114: *
1115: * @param part pattern part to be examined
1116: * @return <code>true</code> if the pattern part is numberic field
1117: */
1118: private final boolean isNumeric(PatternPart part) {
1119: if (part.count <= 0) {
1120: return false;
1121: }
1122: int i = NUMERIC_FORMAT_CHARS.indexOf(part.text.charAt(0));
1123: return (i > 0 || (i == 0 && part.count < 3));
1124: }
1125:
1126: /**
1127: * Method attempts to match the text at a given position against an array of
1128: * strings. Since multiple strings in the array may match (for example, if the
1129: * array contains "a", "ab", and "abc", all will match the input string
1130: * "abcd") the longest match is returned.
1131: *
1132: * @param text the time text being parsed
1133: * @param start where to start parsing
1134: * @param data the string array to parsed
1135: * @param pos to receive where the match stopped
1136: * @return the new start position if matching succeeded; a negative number
1137: * indicating matching failure
1138: */
1139: private int matchString(String text, int start, String[] data,
1140: int[] pos) {
1141: int count = data.length;
1142:
1143: // There may be multiple strings in the data[] array which begin with
1144: // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
1145: // We keep track of the longest match, and return that. Note that this
1146: // unfortunately requires us to test all array elements.
1147: int bestMatchLength = 0, bestMatch = -1;
1148: String textInLowerCase = text.substring(start).toLowerCase();
1149: for (int i = 0; i < count; ++i) {
1150: int length = data[i].length();
1151: // Always compare if we have no match yet; otherwise only compare
1152: // against potentially better matches (longer strings).
1153: if (length > bestMatchLength
1154: && textInLowerCase
1155: .startsWith(data[i].toLowerCase())) {
1156: bestMatch = i;
1157: bestMatchLength = length;
1158: }
1159: }
1160: if (bestMatch >= 0) {
1161: pos[0] = start + bestMatchLength;
1162: }
1163: return bestMatch;
1164: }
1165:
1166: /**
1167: * Method parses a integer string and return integer value.
1168: *
1169: * @param text string being parsed
1170: * @param pos parse position
1171: *
1172: * @return integer value
1173: */
1174: private int parseInt(String text, int[] pos) {
1175: int ret = 0;
1176: int ind = pos[0];
1177: char ch = text.charAt(ind);
1178: while (ch >= '0' && ch <= '9') {
1179: ret = ret * 10 + (ch - '0');
1180: ind++;
1181: if (ind >= text.length()) {
1182: break;
1183: }
1184: ch = text.charAt(ind);
1185: }
1186: if (ind > pos[0]) {
1187: pos[0] = ind;
1188: } else {
1189: ret = -1;
1190: }
1191: return ret;
1192: }
1193:
1194: /**
1195: * Method parses the input pattern string a generate a vector of pattern
1196: * parts.
1197: *
1198: * @param pattern describe the format of date string that need to be parsed
1199: */
1200: private void parsePattern(String pattern) {
1201: StringBuffer buf = new StringBuffer(32);
1202: boolean inQuote = false;
1203:
1204: for (int i = 0; i < pattern.length(); i++) {
1205: char ch = pattern.charAt(i);
1206:
1207: // Handle space, add literal part (if exist), and add space part.
1208: if (ch == ' ') {
1209: addPart(buf, 0);
1210: buf.append(' ');
1211: addPart(buf, 0);
1212: while (i + 1 < pattern.length()
1213: && pattern.charAt(i + 1) == ' ') {
1214: i++;
1215: }
1216: continue;
1217: }
1218:
1219: // If inside quote, except two quote connected, just copy or exit.
1220: if (inQuote) {
1221: if (ch == '\'') {
1222: if (i + 1 < pattern.length()
1223: && pattern.charAt(i + 1) == '\'') {
1224: // Quote appeared twice continuously, interpret as one quote.
1225: buf.append(ch);
1226: ++i;
1227: } else {
1228: inQuote = false;
1229: }
1230: } else {
1231: // Literal.
1232: buf.append(ch);
1233: }
1234: continue;
1235: }
1236:
1237: // Outside quote now.
1238: if (PATTERN_CHARS.indexOf(ch) > 0) {
1239: addPart(buf, 0);
1240: buf.append(ch);
1241: int count = getNextCharCountInPattern(pattern, i);
1242: addPart(buf, count);
1243: i += count - 1;
1244: continue;
1245: }
1246:
1247: // Two consecutive quotes is a quote literal, inside or outside of quotes.
1248: if (ch == '\'') {
1249: if (i + 1 < pattern.length()
1250: && pattern.charAt(i + 1) == '\'') {
1251: buf.append('\'');
1252: i++;
1253: } else {
1254: inQuote = true;
1255: }
1256: } else {
1257: buf.append(ch);
1258: }
1259: }
1260:
1261: addPart(buf, 0);
1262:
1263: identifyAbutStart();
1264: }
1265:
1266: /**
1267: * Method parses time zone offset.
1268: *
1269: * @param text the time text to be parsed
1270: * @param pos Parse position
1271: * @param cal DateRecord object that holds parsed value
1272: *
1273: * @return <code>true</code> if parsing successful, otherwise
1274: * <code>false</code>
1275: */
1276: private boolean parseTimeZoneOffset(String text, int[] pos,
1277: DateRecord cal) {
1278: if (pos[0] >= text.length()) {
1279: cal.setTzOffset(0);
1280: return true;
1281: }
1282:
1283: int sign;
1284: switch (text.charAt(pos[0])) {
1285: case '+':
1286: sign = 1;
1287: break;
1288: case '-':
1289: sign = -1;
1290: break;
1291: default:
1292: cal.setTzOffset(0);
1293: return true;
1294: }
1295: ++(pos[0]);
1296:
1297: // Look for hours:minutes or hhmm.
1298: int st = pos[0];
1299: int value = parseInt(text, pos);
1300: if (value == 0 && pos[0] == st) {
1301: return false;
1302: }
1303:
1304: int offset;
1305: if (pos[0] < text.length() && text.charAt(pos[0]) == ':') {
1306: // This is the hours:minutes case.
1307: offset = value * MINUTES_PER_HOUR;
1308: ++(pos[0]);
1309: st = pos[0];
1310: value = parseInt(text, pos);
1311: if (value == 0 && pos[0] == st) {
1312: return false;
1313: }
1314: offset += value;
1315: } else {
1316: // This is the hhmm case.
1317: offset = value;
1318: // Assume "-23".."+23" refers to hours.
1319: if (offset < 24 && (pos[0] - st) <= 2) {
1320: offset *= MINUTES_PER_HOUR;
1321: } else {
1322: offset = offset % 100 + offset / 100 * MINUTES_PER_HOUR;
1323: }
1324: }
1325:
1326: offset *= sign;
1327: cal.setTzOffset(-offset);
1328: return true;
1329: }
1330:
1331: /**
1332: * Method skips space in the string as pointed by pos.
1333: *
1334: * @param text input string
1335: * @param pos where skip start, and return back where skip stop
1336: */
1337: private void skipSpace(String text, int[] pos) {
1338: while (pos[0] < text.length()
1339: && WHITE_SPACE.indexOf(text.charAt(pos[0])) >= 0) {
1340: ++(pos[0]);
1341: }
1342: }
1343:
1344: /**
1345: * Formats a single field according to pattern specified.
1346: *
1347: * @param buf where formatted string will be appended to
1348: * @param ch pattern for this field
1349: * @param count number of time pattern char repeats; this controls how a field
1350: * should be formatted
1351: * @param date hold the date object to be formatted
1352: *
1353: * @return <code>true</code> if pattern valid, otherwise <code>false</code>
1354: */
1355: private boolean subFormat(StringBuffer buf, char ch, int count,
1356: Date date) {
1357: switch (ch) {
1358: case 'G':
1359: formatEra(buf, count, date);
1360: break;
1361: case 'y':
1362: formatYear(buf, count, date);
1363: break;
1364: case 'M':
1365: formatMonth(buf, count, date);
1366: break;
1367: case 'k':
1368: format24Hours(buf, count, date);
1369: break;
1370: case 'S':
1371: formatFractionalSeconds(buf, count, date);
1372: break;
1373: case 'E':
1374: formatDayOfWeek(buf, count, date);
1375: break;
1376: case 'a':
1377: formatAmPm(buf, count, date);
1378: break;
1379: case 'h':
1380: format1To12Hours(buf, count, date);
1381: break;
1382: case 'K':
1383: format0To11Hours(buf, count, date);
1384: break;
1385: case 'H':
1386: format0To23Hours(buf, count, date);
1387: break;
1388: case 'c':
1389: formatStandaloneDay(buf, count, date);
1390: break;
1391: case 'L':
1392: formatStandaloneMonth(buf, count, date);
1393: break;
1394: case 'Q':
1395: formatQuarter(buf, count, date);
1396: break;
1397: case 'd':
1398: formatDate(buf, count, date);
1399: break;
1400: case 'm':
1401: formatMinutes(buf, count, date);
1402: break;
1403: case 's':
1404: formatSeconds(buf, count, date);
1405: break;
1406: case 'z':
1407: case 'v':
1408: appendGMT(buf, date);
1409: break;
1410: case 'Z':
1411: formatTimeZoneRFC(buf, count, date);
1412: break;
1413: default:
1414: return false;
1415: }
1416: return true;
1417: }
1418:
1419: /**
1420: * Converts one field of the input string into a numeric field value. Returns
1421: * <code>false</code> if failed.
1422: *
1423: * @param text the time text to be parsed
1424: * @param pos Parse position
1425: * @param part the pattern part for this field
1426: * @param digitCount when greater than 0, numeric parsing must obey the count
1427: * @param cal DateRecord object that will hold parsed value
1428: *
1429: * @return <code>true</code> if parsing successful
1430: */
1431: @SuppressWarnings("fallthrough")
1432: private boolean subParse(String text, int[] pos, PatternPart part,
1433: int digitCount, DateRecord cal) {
1434:
1435: skipSpace(text, pos);
1436:
1437: int start = pos[0];
1438: char ch = part.text.charAt(0);
1439:
1440: // Parse integer value if it is a numeric field.
1441: int value = -1; // initialize value to be -1,
1442: if (isNumeric(part)) {
1443: if (digitCount > 0) {
1444: if ((start + digitCount) > text.length()) {
1445: return false;
1446: }
1447: value = parseInt(text.substring(0, start + digitCount),
1448: pos);
1449: } else {
1450: value = parseInt(text, pos);
1451: }
1452: }
1453:
1454: switch (ch) {
1455: case 'G': // 'G' - ERA
1456: value = matchString(text, start, dateTimeConstants.eras(),
1457: pos);
1458: cal.setEra(value);
1459: return true;
1460: case 'M': // 'M' - MONTH
1461: return subParseMonth(text, pos, cal, value, start);
1462: case 'E':
1463: return subParseDayOfWeek(text, pos, start, cal);
1464: case 'a': // 'a' - AM_PM
1465: value = matchString(text, start, dateTimeConstants.ampms(),
1466: pos);
1467: cal.setAmpm(value);
1468: return true;
1469: case 'y': // 'y' - YEAR
1470: return subParseYear(text, pos, start, value, part, cal);
1471: case 'd': // 'd' - DATE
1472: cal.setDayOfMonth(value);
1473: return true;
1474: case 'S': // 'S' - FRACTIONAL_SECOND
1475: return subParseFractionalSeconds(value, start, pos[0], cal);
1476: case 'h': // 'h' - HOUR (1..12)
1477: if (value == 12) {
1478: value = 0;
1479: }
1480: // fall through
1481: case 'K': // 'K' - HOUR (0..11)
1482: case 'H': // 'H' - HOUR_OF_DAY (0..23)
1483: cal.setHours(value);
1484: return true;
1485: case 'k': // 'k' - HOUR_OF_DAY (1..24)
1486: cal.setHours(value);
1487: return true;
1488: case 'm': // 'm' - MINUTE
1489: cal.setMinutes(value);
1490: return true;
1491: case 's': // 's' - SECOND
1492: cal.setSeconds(value);
1493: return true;
1494:
1495: case 'z': // 'z' - ZONE_OFFSET
1496: case 'Z': // 'Z' - TIMEZONE_RFC
1497: case 'v': // 'v' - TIMEZONE_GENERIC
1498: return subParseTimeZoneInGMT(text, start, pos, cal);
1499: default:
1500: return false;
1501: }
1502: }
1503:
1504: /**
1505: * Method subParseDayOfWeek parses day of the week field.
1506: *
1507: * @param text the time text to be parsed
1508: * @param pos Parse position
1509: * @param start from where parse start
1510: * @param cal DateRecord object that holds parsed value
1511: *
1512: * @return <code>true</code> if parsing successful, otherwise
1513: * <code>false</code>
1514: */
1515: private boolean subParseDayOfWeek(String text, int[] pos,
1516: int start, DateRecord cal) {
1517: int value;
1518: // 'E' - DAY_OF_WEEK
1519: // Want to be able to parse both short and long forms.
1520: // Try count == 4 (DDDD) first:
1521: value = matchString(text, start, dateTimeConstants.weekdays(),
1522: pos);
1523: if (value < 0) {
1524: value = matchString(text, start, dateTimeConstants
1525: .shortWeekdays(), pos);
1526: }
1527: if (value < 0) {
1528: return false;
1529: }
1530: cal.setDayOfWeek(value);
1531: return true;
1532: }
1533:
1534: /**
1535: * Method subParseFractionalSeconds parses fractional seconds field.
1536: *
1537: * @param value parsed numberic value
1538: * @param start
1539: * @param end parse position
1540: * @param cal DateRecord object that holds parsed value
1541: * @return <code>true</code> if parsing successful, otherwise
1542: * <code>false</code>
1543: */
1544: private boolean subParseFractionalSeconds(int value, int start,
1545: int end, DateRecord cal) {
1546: // Fractional seconds left-justify.
1547: int i = end - start;
1548: if (i < 3) {
1549: while (i < 3) {
1550: value *= 10;
1551: i++;
1552: }
1553: } else {
1554: int a = 1;
1555: while (i > 3) {
1556: a *= 10;
1557: i--;
1558: }
1559: value = (value + (a >> 1)) / a;
1560: }
1561: cal.setMilliseconds(value);
1562: return true;
1563: }
1564:
1565: /**
1566: * Method subParseMonth parses Month field.
1567: *
1568: * @param text the time text to be parsed
1569: * @param pos Parse position
1570: * @param cal DateRecord object that will hold parsed value
1571: * @param value numeric value if this field is expressed using numberic
1572: * pattern
1573: * @param start from where parse start
1574: *
1575: * @return <code>true</code> if parsing successful
1576: */
1577: private boolean subParseMonth(String text, int[] pos,
1578: DateRecord cal, int value, int start) {
1579: // When month is symbols, i.e., MMM or MMMM, value will be -1.
1580: if (value < 0) {
1581: // Want to be able to parse both short and long forms.
1582: // Try count == 4 first:
1583: value = matchString(text, start,
1584: dateTimeConstants.months(), pos);
1585: if (value < 0) { // count == 4 failed, now try count == 3.
1586: value = matchString(text, start, dateTimeConstants
1587: .shortMonths(), pos);
1588: }
1589: if (value < 0) {
1590: return false;
1591: }
1592: cal.setMonth(value);
1593: return true;
1594: } else {
1595: cal.setMonth(value - 1);
1596: return true;
1597: }
1598: }
1599:
1600: /**
1601: * Method parses GMT type timezone.
1602: *
1603: * @param text the time text to be parsed
1604: * @param start from where parse start
1605: * @param pos Parse position
1606: * @param cal DateRecord object that holds parsed value
1607: *
1608: * @return <code>true</code> if parsing successful, otherwise
1609: * <code>false</code>
1610: */
1611: private boolean subParseTimeZoneInGMT(String text, int start,
1612: int[] pos, DateRecord cal) {
1613: // First try to parse generic forms such as GMT-07:00. Do this first
1614: // in case localized DateFormatZoneData contains the string "GMT"
1615: // for a zone; in that case, we don't want to match the first three
1616: // characters of GMT+/-HH:MM etc.
1617:
1618: // For time zones that have no known names, look for strings
1619: // of the form:
1620: // GMT[+-]hours:minutes or
1621: // GMT[+-]hhmm or
1622: // GMT.
1623: if (text.startsWith(GMT, start)) {
1624: pos[0] = start + GMT.length();
1625: return parseTimeZoneOffset(text, pos, cal);
1626: }
1627:
1628: // At this point, check for named time zones by looking through
1629: // the locale data from the DateFormatZoneData strings.
1630: // Want to be able to parse both short and long forms.
1631: /*
1632: * i = subParseZoneString(text, start, cal); if (i != 0) return i;
1633: */
1634:
1635: // As a last resort, look for numeric timezones of the form
1636: // [+-]hhmm as specified by RFC 822. This code is actually
1637: // a little more permissive than RFC 822. It will try to do
1638: // its best with numbers that aren't strictly 4 digits long.
1639: return parseTimeZoneOffset(text, pos, cal);
1640: }
1641:
1642: /**
1643: * Method subParseYear parse year field. Year field is special because 1, two
1644: * digit year need to be resolved. 2, we allow year to take a sign. 3, year
1645: * field participate in abut processing. In my testing, negative year does not
1646: * seem working due to JDK (or redpill implementation) limitation. It is not a
1647: * big deal so we don't worry about it. But keep the logic here so that we
1648: * might want to replace DateRecord with our a calendar class.
1649: *
1650: * @param text the time text to be parsed
1651: * @param pos parse position
1652: * @param start where this field star
1653: * @param value integer value of yea
1654: * @param part the pattern part for this field
1655: * @param cal DateRecord object that will hold parsed value
1656: *
1657: * @return <code>true</code> if successful
1658: */
1659: private boolean subParseYear(String text, int[] pos, int start,
1660: int value, PatternPart part, DateRecord cal) {
1661: char ch = ' ';
1662: if (value < 0) {
1663: ch = text.charAt(pos[0]);
1664: // Check if it is a sign.
1665: if (ch != '+' && ch != '-') {
1666: return false;
1667: }
1668: ++(pos[0]);
1669: value = parseInt(text, pos);
1670: if (value < 0) {
1671: return false;
1672: }
1673: if (ch == '-') {
1674: value = -value;
1675: }
1676: }
1677:
1678: // no sign, only 2 digit was actually parsed, pattern say it has 2 digit.
1679: if (ch == ' ' && (pos[0] - start) == 2 && part.count == 2) {
1680: // Assume for example that the defaultCenturyStart is 6/18/1903.
1681: // This means that two-digit years will be forced into the range
1682: // 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02
1683: // correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
1684: // to 1904, 1905, etc. If the year is 03, then it is 2003 if the
1685: // other fields specify a date before 6/18, or 1903 if they specify a
1686: // date afterwards. As a result, 03 is an ambiguous year. All other
1687: // two-digit years are unambiguous.
1688: Date date = new Date();
1689: int defaultCenturyStartYear = date.getYear() + 1900 - 80;
1690: int ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
1691: cal.setAmbiguousYear(value == ambiguousTwoDigitYear);
1692: value += (defaultCenturyStartYear / 100) * 100
1693: + (value < ambiguousTwoDigitYear ? 100 : 0);
1694: }
1695: cal.setYear(value);
1696: return true;
1697: }
1698:
1699: /**
1700: * Formats a number with the specified minimum number of digits, using zero to
1701: * fill the gap.
1702: *
1703: * @param buf where zero padded string will be written to
1704: * @param value the number value being formatted
1705: * @param minWidth minimum width of the formatted string; zero will be padded
1706: * to reach this width
1707: */
1708: private void zeroPaddingNumber(StringBuffer buf, int value,
1709: int minWidth) {
1710: int b = NUMBER_BASE;
1711: for (int i = 0; i < minWidth - 1; i++) {
1712: if (value < b) {
1713: buf.append('0');
1714: }
1715: b *= NUMBER_BASE;
1716: }
1717: buf.append(Integer.toString(value));
1718: }
1719: }
|