com.google.gwt.i18n.shared.DateTimeFormat.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.i18n.shared.DateTimeFormat.java

Source

/*
 * 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 offset
        case 'Z': // time zone RFC
        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);
    }
}