0001: /*
0002: *******************************************************************************
0003: * Copyright (C) 1996-2006, International Business Machines Corporation and *
0004: * others. All Rights Reserved. *
0005: *******************************************************************************
0006: */
0007:
0008: package com.ibm.icu.text;
0009:
0010: import com.ibm.icu.util.Calendar;
0011: import com.ibm.icu.lang.UCharacter;
0012: import com.ibm.icu.impl.CalendarData;
0013: import com.ibm.icu.impl.UCharacterProperty;
0014: import com.ibm.icu.impl.ZoneMeta;
0015: import com.ibm.icu.util.TimeZone;
0016: import com.ibm.icu.util.ULocale;
0017:
0018: import java.io.IOException;
0019: import java.io.ObjectInputStream;
0020: import java.lang.ref.SoftReference;
0021: import java.text.FieldPosition;
0022: import java.text.MessageFormat;
0023: import java.text.ParsePosition;
0024: import java.util.Date;
0025: import java.util.HashMap;
0026: import java.util.Hashtable;
0027: import java.util.Locale;
0028: import java.util.Map;
0029:
0030: import com.ibm.icu.impl.LocaleUtility;
0031:
0032: /**
0033: * <code>SimpleDateFormat</code> is a concrete class for formatting and
0034: * parsing dates in a locale-sensitive manner. It allows for formatting
0035: * (date -> text), parsing (text -> date), and normalization.
0036: *
0037: * <p>
0038: * <code>SimpleDateFormat</code> allows you to start by choosing
0039: * any user-defined patterns for date-time formatting. However, you
0040: * are encouraged to create a date-time formatter with either
0041: * <code>getTimeInstance</code>, <code>getDateInstance</code>, or
0042: * <code>getDateTimeInstance</code> in <code>DateFormat</code>. Each
0043: * of these class methods can return a date/time formatter initialized
0044: * with a default format pattern. You may modify the format pattern
0045: * using the <code>applyPattern</code> methods as desired.
0046: * For more information on using these methods, see
0047: * {@link DateFormat}.
0048: *
0049: * <p>
0050: * <strong>Time Format Syntax:</strong>
0051: * <p>
0052: * To specify the time format use a <em>time pattern</em> string.
0053: * In this pattern, all ASCII letters are reserved as pattern letters,
0054: * which are defined as the following:
0055: * <blockquote>
0056: * <pre>
0057: * Symbol Meaning Presentation Example
0058: * ------ ------- ------------ -------
0059: * G era designator (Text) AD
0060: * y† year (Number) 1996
0061: * Y* year (week of year) (Number) 1997
0062: * u* extended year (Number) 4601
0063: * M month in year (Text & Number) July & 07
0064: * d day in month (Number) 10
0065: * h hour in am/pm (1~12) (Number) 12
0066: * H hour in day (0~23) (Number) 0
0067: * m minute in hour (Number) 30
0068: * s second in minute (Number) 55
0069: * S fractional second (Number) 978
0070: * E day of week (Text) Tuesday
0071: * e* day of week (local 1~7) (Number) 2
0072: * D day in year (Number) 189
0073: * F day of week in month (Number) 2 (2nd Wed in July)
0074: * w week in year (Number) 27
0075: * W week in month (Number) 2
0076: * a am/pm marker (Text) PM
0077: * k hour in day (1~24) (Number) 24
0078: * K hour in am/pm (0~11) (Number) 0
0079: * z time zone (Text) Pacific Standard Time
0080: * Z time zone (RFC 822) (Number) -0800
0081: * v time zone (generic) (Text) Pacific Time
0082: * g* Julian day (Number) 2451334
0083: * A* milliseconds in day (Number) 69540000
0084: * ' escape for text (Delimiter) 'Date='
0085: * '' single quote (Literal) 'o''clock'
0086: * </pre>
0087: * </blockquote>
0088: * <tt><b>*</b></tt> These items are not supported by Java's SimpleDateFormat.<br>
0089: * <tt><b>†</b></tt> ICU interprets a single 'y' differently than Java.</p>
0090: * <p>
0091: * The count of pattern letters determine the format.
0092: * <p>
0093: * <strong>(Text)</strong>: 4 or more pattern letters--use full form,
0094: * < 4--use short or abbreviated form if one exists.
0095: * <p>
0096: * <strong>(Number)</strong>: the minimum number of digits. Shorter
0097: * numbers are zero-padded to this amount. Year is handled specially;
0098: * that is, if the count of 'y' is 2, the Year will be truncated to 2 digits.
0099: * (e.g., if "yyyy" produces "1997", "yy" produces "97".)
0100: * Unlike other fields, fractional seconds are padded on the right with zero.
0101: * <p>
0102: * <strong>(Text & Number)</strong>: 3 or over, use text, otherwise use number.
0103: * <p>
0104: * Any characters in the pattern that are not in the ranges of ['a'..'z']
0105: * and ['A'..'Z'] will be treated as quoted text. For instance, characters
0106: * like ':', '.', ' ', '#' and '@' will appear in the resulting time text
0107: * even they are not embraced within single quotes.
0108: * <p>
0109: * A pattern containing any invalid pattern letter will result in a thrown
0110: * exception during formatting or parsing.
0111: *
0112: * <p>
0113: * <strong>Examples Using the US Locale:</strong>
0114: * <blockquote>
0115: * <pre>
0116: * Format Pattern Result
0117: * -------------- -------
0118: * "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->> 1996.07.10 AD at 15:08:56 Pacific Time
0119: * "EEE, MMM d, ''yy" ->> Wed, July 10, '96
0120: * "h:mm a" ->> 12:08 PM
0121: * "hh 'o''clock' a, zzzz" ->> 12 o'clock PM, Pacific Daylight Time
0122: * "K:mm a, vvv" ->> 0:00 PM, PT
0123: * "yyyyy.MMMMM.dd GGG hh:mm aaa" ->> 01996.July.10 AD 12:08 PM
0124: * </pre>
0125: * </blockquote>
0126: * <strong>Code Sample:</strong>
0127: * <blockquote>
0128: * <pre>
0129: * SimpleTimeZone pdt = new SimpleTimeZone(-8 * 60 * 60 * 1000, "PST");
0130: * pdt.setStartRule(Calendar.APRIL, 1, Calendar.SUNDAY, 2*60*60*1000);
0131: * pdt.setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2*60*60*1000);
0132: * <br>
0133: * // Format the current time.
0134: * SimpleDateFormat formatter
0135: * = new SimpleDateFormat ("yyyy.MM.dd G 'at' hh:mm:ss a zzz");
0136: * Date currentTime_1 = new Date();
0137: * String dateString = formatter.format(currentTime_1);
0138: * <br>
0139: * // Parse the previous string back into a Date.
0140: * ParsePosition pos = new ParsePosition(0);
0141: * Date currentTime_2 = formatter.parse(dateString, pos);
0142: * </pre>
0143: * </blockquote>
0144: * In the example, the time value <code>currentTime_2</code> obtained from
0145: * parsing will be equal to <code>currentTime_1</code>. However, they may not be
0146: * equal if the am/pm marker 'a' is left out from the format pattern while
0147: * the "hour in am/pm" pattern symbol is used. This information loss can
0148: * happen when formatting the time in PM.
0149: *
0150: * <p>
0151: * When parsing a date string using the abbreviated year pattern ("yy"),
0152: * SimpleDateFormat must interpret the abbreviated year
0153: * relative to some century. It does this by adjusting dates to be
0154: * within 80 years before and 20 years after the time the SimpleDateFormat
0155: * instance is created. For example, using a pattern of "MM/dd/yy" and a
0156: * SimpleDateFormat instance created on Jan 1, 1997, the string
0157: * "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64"
0158: * would be interpreted as May 4, 1964.
0159: * During parsing, only strings consisting of exactly two digits, as defined by
0160: * {@link java.lang.Character#isDigit(char)}, will be parsed into the default
0161: * century.
0162: * Any other numeric string, such as a one digit string, a three or more digit
0163: * string, or a two digit string that isn't all digits (for example, "-1"), is
0164: * interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the
0165: * same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC.
0166: *
0167: * <p>
0168: * If the year pattern does not have exactly two 'y' characters, the year is
0169: * interpreted literally, regardless of the number of digits. So using the
0170: * pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
0171: *
0172: * <p>
0173: * When numeric fields abut one another directly, with no intervening delimiter
0174: * characters, they constitute a run of abutting numeric fields. Such runs are
0175: * parsed specially. For example, the format "HHmmss" parses the input text
0176: * "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and fails to
0177: * parse "1234". In other words, the leftmost field of the run is flexible,
0178: * while the others keep a fixed width. If the parse fails anywhere in the run,
0179: * then the leftmost field is shortened by one character, and the entire run is
0180: * parsed again. This is repeated until either the parse succeeds or the
0181: * leftmost field is one character in length. If the parse still fails at that
0182: * point, the parse of the run fails.
0183: *
0184: * <p>
0185: * For time zones that have no names, use strings GMT+hours:minutes or
0186: * GMT-hours:minutes.
0187: *
0188: * <p>
0189: * The calendar defines what is the first day of the week, the first week
0190: * of the year, whether hours are zero based or not (0 vs 12 or 24), and the
0191: * time zone. There is one common decimal format to handle all the numbers;
0192: * the digit count is handled programmatically according to the pattern.
0193: *
0194: * <h4>Synchronization</h4>
0195: *
0196: * Date formats are not synchronized. It is recommended to create separate
0197: * format instances for each thread. If multiple threads access a format
0198: * concurrently, it must be synchronized externally.
0199: *
0200: * @see com.ibm.icu.util.Calendar
0201: * @see com.ibm.icu.util.GregorianCalendar
0202: * @see com.ibm.icu.util.TimeZone
0203: * @see DateFormat
0204: * @see DateFormatSymbols
0205: * @see DecimalFormat
0206: * @author Mark Davis, Chen-Lieh Huang, Alan Liu
0207: * @stable ICU 2.0
0208: */
0209: public class SimpleDateFormat extends DateFormat {
0210:
0211: // the official serial version ID which says cryptically
0212: // which version we're compatible with
0213: private static final long serialVersionUID = 4774881970558875024L;
0214:
0215: // the internal serial version which says which version was written
0216: // - 0 (default) for version up to JDK 1.1.3
0217: // - 1 for version from JDK 1.1.4, which includes a new field
0218: static final int currentSerialVersion = 1;
0219:
0220: /**
0221: * The version of the serialized data on the stream. Possible values:
0222: * <ul>
0223: * <li><b>0</b> or not present on stream: JDK 1.1.3. This version
0224: * has no <code>defaultCenturyStart</code> on stream.
0225: * <li><b>1</b> JDK 1.1.4 or later. This version adds
0226: * <code>defaultCenturyStart</code>.
0227: * </ul>
0228: * When streaming out this class, the most recent format
0229: * and the highest allowable <code>serialVersionOnStream</code>
0230: * is written.
0231: * @serial
0232: */
0233: private int serialVersionOnStream = currentSerialVersion;
0234:
0235: /**
0236: * The pattern string of this formatter. This is always a non-localized
0237: * pattern. May not be null. See class documentation for details.
0238: * @serial
0239: */
0240: private String pattern;
0241:
0242: /**
0243: * The symbols used by this formatter for week names, month names,
0244: * etc. May not be null.
0245: * @serial
0246: * @see DateFormatSymbols
0247: */
0248: private DateFormatSymbols formatData;
0249:
0250: private transient ULocale locale;
0251:
0252: /**
0253: * We map dates with two-digit years into the century starting at
0254: * <code>defaultCenturyStart</code>, which may be any date. May
0255: * not be null.
0256: * @serial
0257: * @since JDK1.1.4
0258: */
0259: private Date defaultCenturyStart;
0260:
0261: transient private int defaultCenturyStartYear;
0262:
0263: private transient TimeZone parsedTimeZone;
0264:
0265: private static final int millisPerHour = 60 * 60 * 1000;
0266: private static final int millisPerMinute = 60 * 1000;
0267:
0268: // For time zones that have no names, use strings GMT+minutes and
0269: // GMT-minutes. For instance, in France the time zone is GMT+60.
0270: private static final String GMT_PLUS = "GMT+";
0271: private static final String GMT_MINUS = "GMT-";
0272: private static final String GMT = "GMT";
0273:
0274: // This prefix is designed to NEVER MATCH real text, in order to
0275: // suppress the parsing of negative numbers. Adjust as needed (if
0276: // this becomes valid Unicode).
0277: private static final String SUPPRESS_NEGATIVE_PREFIX = "\uAB00";
0278:
0279: /**
0280: * Cache to hold the DateTimePatterns of a Locale.
0281: */
0282: private static Hashtable cachedLocaleData = new Hashtable(3);
0283:
0284: /**
0285: * If true, this object supports fast formatting using the
0286: * subFormat variant that takes a StringBuffer.
0287: */
0288: private transient boolean useFastFormat;
0289:
0290: /**
0291: * If true, this object supports fast number format
0292: */
0293: private transient boolean useFastZeroPaddingNumber;
0294: private transient char zeroDigit;
0295: private char[] decimalBuf = new char[10]; // 10 digit is good enough to store Interger.MAX_VALUE
0296:
0297: /**
0298: * Construct a SimpleDateFormat using the default pattern for the default
0299: * locale. <b>Note:</b> Not all locales support SimpleDateFormat; for full
0300: * generality, use the factory methods in the DateFormat class.
0301: *
0302: * @see DateFormat
0303: * @stable ICU 2.0
0304: */
0305: public SimpleDateFormat() {
0306: this (SHORT, SHORT, ULocale.getDefault());
0307: }
0308:
0309: /**
0310: * Construct a SimpleDateFormat using the given pattern in the default
0311: * locale. <b>Note:</b> Not all locales support SimpleDateFormat; for full
0312: * generality, use the factory methods in the DateFormat class.
0313: * @stable ICU 2.0
0314: */
0315: public SimpleDateFormat(String pattern) {
0316: this (pattern, ULocale.getDefault());
0317: }
0318:
0319: /**
0320: * Construct a SimpleDateFormat using the given pattern and locale.
0321: * <b>Note:</b> Not all locales support SimpleDateFormat; for full
0322: * generality, use the factory methods in the DateFormat class.
0323: * @stable ICU 2.0
0324: */
0325: public SimpleDateFormat(String pattern, Locale loc) {
0326: this (pattern, ULocale.forLocale(loc));
0327: }
0328:
0329: /**
0330: * Construct a SimpleDateFormat using the given pattern and locale.
0331: * <b>Note:</b> Not all locales support SimpleDateFormat; for full
0332: * generality, use the factory methods in the DateFormat class.
0333: * @draft ICU 3.2
0334: * @provisional This API might change or be removed in a future release.
0335: */
0336: public SimpleDateFormat(String pattern, ULocale loc) {
0337: this .pattern = pattern;
0338: this .formatData = new DateFormatSymbols(loc);
0339: initialize(loc);
0340: }
0341:
0342: /**
0343: * Construct a SimpleDateFormat using the given pattern and
0344: * locale-specific symbol data.
0345: * Warning: uses default locale for digits!
0346: * @stable ICU 2.0
0347: */
0348: public SimpleDateFormat(String pattern, DateFormatSymbols formatData) {
0349: this (pattern, formatData, ULocale.getDefault());
0350: }
0351:
0352: /**
0353: * @internal ICU 3.2
0354: * @deprecated This API is ICU internal only.
0355: */
0356: public SimpleDateFormat(String pattern,
0357: DateFormatSymbols formatData, ULocale loc) {
0358: this .pattern = pattern;
0359: this .formatData = (DateFormatSymbols) formatData.clone();
0360:
0361: initialize(loc);
0362: }
0363:
0364: /**
0365: * Package-private constructor that allows a subclass to specify
0366: * whether it supports fast formatting.
0367: *
0368: * TODO make this API public.
0369: */
0370: SimpleDateFormat(String pattern, DateFormatSymbols formatData,
0371: boolean useFastFormat) {
0372: this .pattern = pattern;
0373: this .formatData = (DateFormatSymbols) formatData.clone();
0374: initialize(ULocale.getDefault());
0375: // this.useFastFormat is set by initialize(); fix it up afterwards
0376: this .useFastFormat = useFastFormat;
0377: }
0378:
0379: // try caching
0380: private static final boolean CACHE = true;
0381: private static long cacheAge;
0382: private static SoftReference highCacheRef;
0383:
0384: /* Package-private, called by DateFormat factory methods */
0385: SimpleDateFormat(int timeStyle, int dateStyle, ULocale loc) {
0386: // try a high level cache first!
0387:
0388: Map map = null;
0389: String key = null;
0390: if (CACHE) {
0391: // age test is so we don't have to compute the century start all the time... once a day is enough.
0392: long time = System.currentTimeMillis();
0393: if (((time - cacheAge) < 1000 * 60 * 60 * 24L)
0394: && highCacheRef != null) {
0395: map = (Map) highCacheRef.get();
0396: }
0397: if (map == null) {
0398: map = new HashMap(3);
0399: highCacheRef = new SoftReference(map);
0400: cacheAge = time;
0401: }
0402: key = loc.toString() + timeStyle + dateStyle;
0403: SimpleDateFormat target = (SimpleDateFormat) map.get(key);
0404: if (target != null) { // kindof skanky
0405: // if ("en_US22".equals(key))
0406: // System.out.println("\nfound key: " + key + " pat: " + target.pattern +
0407: // " cal: " + target.calendar + " fmt: " + target.numberFormat);
0408: this .pattern = target.pattern;
0409: this .formatData = target.formatData;
0410: this .defaultCenturyStart = target.defaultCenturyStart;
0411: this .defaultCenturyStartYear = target.defaultCenturyStartYear;
0412: this .calendar = (Calendar) target.calendar.clone();
0413: this .calendar.setTimeZone(TimeZone.getDefault()); // might have changed since cached
0414: this .numberFormat = (NumberFormat) target.numberFormat
0415: .clone();
0416: return;
0417: }
0418: }
0419:
0420: /* try the cache first */
0421: String[] dateTimePatterns = (String[]) cachedLocaleData
0422: .get(loc);
0423: if (dateTimePatterns == null) { /* cache miss */
0424: CalendarData calData = new CalendarData(loc, null); // TODO: type?
0425: // TODO: get correct actual/valid locale here
0426: ULocale uloc = calData.getULocale();
0427: setLocale(uloc, uloc);
0428:
0429: dateTimePatterns = calData
0430: .getStringArray("DateTimePatterns");
0431: /* update cache */
0432: cachedLocaleData.put(loc, dateTimePatterns);
0433: } else {
0434: // for now, just assume this is correct, so we have non-null locale info.
0435: // we may have to cache the result of calData.getULocale with the pattern strings
0436: // and set the locale with that.
0437: setLocale(loc, loc);
0438: }
0439: formatData = new DateFormatSymbols(loc);
0440: if ((timeStyle >= 0) && (dateStyle >= 0)) {
0441: Object[] dateTimeArgs = { dateTimePatterns[timeStyle],
0442: dateTimePatterns[dateStyle + 4] };
0443: pattern = MessageFormat.format(dateTimePatterns[8],
0444: dateTimeArgs);
0445: } else if (timeStyle >= 0) {
0446: pattern = dateTimePatterns[timeStyle];
0447: } else if (dateStyle >= 0) {
0448: pattern = dateTimePatterns[dateStyle + 4];
0449: } else {
0450: throw new IllegalArgumentException(
0451: "No date or time style specified");
0452: }
0453:
0454: initialize(loc);
0455:
0456: if (CACHE) {
0457: // if ("en_US22".equals(key))
0458: // System.out.println("\nregister key: " + key + " pat: " + this.pattern +
0459: // " cal: " + this.calendar + " fmt: " + this.numberFormat);
0460: map.put(key, this .clone()); // ok if we stomp existing target due to threading
0461: }
0462: }
0463:
0464: /* Initialize calendar and numberFormat fields */
0465: private void initialize(ULocale loc) {
0466: // time zone formatting
0467: locale = loc;
0468:
0469: // The format object must be constructed using the symbols for this zone.
0470: // However, the calendar should use the current default TimeZone.
0471: // If this is not contained in the locale zone strings, then the zone
0472: // will be formatted using generic GMT+/-H:MM nomenclature.
0473: calendar = Calendar.getInstance(TimeZone.getDefault(), loc);
0474: // TODO: convert to use ULocale APIs when we get to the text package
0475: numberFormat = NumberFormat.getInstance(loc);
0476: numberFormat.setGroupingUsed(false);
0477: useFastZeroPaddingNumber = false;
0478: ///CLOVER:OFF
0479: // difficult to test for case where NumberFormat.getInstance does not
0480: // return a DecimalFormat
0481: if (numberFormat instanceof DecimalFormat) {
0482: ((DecimalFormat) numberFormat)
0483: .setDecimalSeparatorAlwaysShown(false);
0484: zeroDigit = ((DecimalFormat) numberFormat)
0485: .getDecimalFormatSymbols().getZeroDigit();
0486: if (numberFormat.getClass().getName().equals(
0487: "com.ibm.icu.text.DecimalFormat")) {
0488: useFastZeroPaddingNumber = true;
0489: }
0490: }
0491: ///CLOVER:ON
0492: numberFormat.setParseIntegerOnly(true); /* So that dd.MM.yy can be parsed */
0493: numberFormat.setMinimumFractionDigits(0); // To prevent "Jan 1.00, 1997.00"
0494:
0495: initializeDefaultCentury();
0496:
0497: // Currently, we only support fast formatting in SimpleDateFormat
0498: // itself. TODO add constructor parameters to allow subclasses
0499: // to say that they implement fast formatting.
0500: useFastFormat = (getClass() == SimpleDateFormat.class);
0501: }
0502:
0503: /* Initialize the fields we use to disambiguate ambiguous years. Separate
0504: * so we can call it from readObject().
0505: */
0506: private void initializeDefaultCentury() {
0507: calendar.setTime(new Date());
0508: calendar.add(Calendar.YEAR, -80);
0509: parseAmbiguousDatesAsAfter(calendar.getTime());
0510: }
0511:
0512: /* Define one-century window into which to disambiguate dates using
0513: * two-digit years.
0514: */
0515: private void parseAmbiguousDatesAsAfter(Date startDate) {
0516: defaultCenturyStart = startDate;
0517: calendar.setTime(startDate);
0518: defaultCenturyStartYear = calendar.get(Calendar.YEAR);
0519: }
0520:
0521: /**
0522: * Sets the 100-year period 2-digit years will be interpreted as being in
0523: * to begin on the date the user specifies.
0524: * @param startDate During parsing, two digit years will be placed in the range
0525: * <code>startDate</code> to <code>startDate + 100 years</code>.
0526: * @stable ICU 2.0
0527: */
0528: public void set2DigitYearStart(Date startDate) {
0529: parseAmbiguousDatesAsAfter(startDate);
0530: }
0531:
0532: /**
0533: * Returns the beginning date of the 100-year period 2-digit years are interpreted
0534: * as being within.
0535: * @return the start of the 100-year period into which two digit years are
0536: * parsed
0537: * @stable ICU 2.0
0538: */
0539: public Date get2DigitYearStart() {
0540: return defaultCenturyStart;
0541: }
0542:
0543: /**
0544: * Overrides DateFormat.
0545: * <p>Formats a date or time, which is the standard millis
0546: * since January 1, 1970, 00:00:00 GMT.
0547: * <p>Example: using the US locale:
0548: * "yyyy.MM.dd G 'at' HH:mm:ss zzz" ->> 1996.07.10 AD at 15:08:56 PDT
0549: * @param cal the calendar whose date-time value is to be formatted into a date-time string
0550: * @param toAppendTo where the new date-time text is to be appended
0551: * @param pos the formatting position. On input: an alignment field,
0552: * if desired. On output: the offsets of the alignment field.
0553: * @return the formatted date-time string.
0554: * @see DateFormat
0555: * @stable ICU 2.0
0556: */
0557: public StringBuffer format(Calendar cal, StringBuffer toAppendTo,
0558: FieldPosition pos) {
0559: if (!useFastFormat) {
0560: return slowFormat(cal, toAppendTo, pos);
0561: }
0562:
0563: // Initialize
0564: pos.setBeginIndex(0);
0565: pos.setEndIndex(0);
0566:
0567: // Careful: For best performance, minimize the number of calls
0568: // to StringBuffer.append() by consolidating appends when
0569: // possible.
0570:
0571: int j, n = pattern.length();
0572: for (int i = 0; i < n;) {
0573: char ch = pattern.charAt(i);
0574: if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
0575: // ch is a date-time pattern character to be interpreted
0576: // by subFormat(); count the number of times it is repeated
0577: for (j = i + 1; j < n && pattern.charAt(j) == ch; ++j) {
0578: }
0579: subFormat(toAppendTo, ch, j - i, toAppendTo.length(),
0580: pos, cal);
0581: i = j;
0582: } else if (ch == '\'') {
0583: // Handle an entire quoted string, included embedded
0584: // doubled apostrophes (as in 'o''clock').
0585: int start = i + 1;
0586: for (;;) {
0587: ++i; // i points after '
0588: if (i == n) { // trailing ' (pathological)
0589: break;
0590: }
0591:
0592: for (j = i; j < n && pattern.charAt(j) != '\''; ++j) {
0593: }
0594: // j points to next ' or EOS
0595:
0596: if (j == start) { // '' outside of quotes
0597: toAppendTo.append('\'');
0598: ++i;
0599: break;
0600: }
0601:
0602: // look ahead to detect '' within quotes
0603: int k = j, jj = j + 1;
0604: if (jj < n && pattern.charAt(jj) == '\'') {
0605: ++k;
0606: }
0607:
0608: // append this run, and if there is '' within
0609: // quotes, append a trailing ' as well
0610: toAppendTo.append(pattern.substring(i, k));
0611:
0612: i = jj;
0613:
0614: if (k == j) {
0615: break;
0616: }
0617: }
0618: } else {
0619: // Append unquoted literal characters
0620: toAppendTo.append(ch);
0621: ++i;
0622: }
0623: }
0624:
0625: return toAppendTo;
0626: }
0627:
0628: private StringBuffer slowFormat(Calendar cal,
0629: StringBuffer toAppendTo, FieldPosition pos) {
0630: // Initialize
0631: pos.setBeginIndex(0);
0632: pos.setEndIndex(0);
0633:
0634: boolean inQuote = false; // true when between single quotes
0635: char prevCh = 0; // previous pattern character
0636: int count = 0; // number of time prevCh repeated
0637: for (int i = 0; i < pattern.length(); ++i) {
0638: char ch = pattern.charAt(i);
0639: // Use subFormat() to format a repeated pattern character
0640: // when a different pattern or non-pattern character is seen
0641: if (ch != prevCh && count > 0) {
0642: toAppendTo.append(subFormat(prevCh, count, toAppendTo
0643: .length(), pos, formatData, cal));
0644: count = 0;
0645: }
0646: if (ch == '\'') {
0647: // Consecutive single quotes are a single quote literal,
0648: // either outside of quotes or between quotes
0649: if ((i + 1) < pattern.length()
0650: && pattern.charAt(i + 1) == '\'') {
0651: toAppendTo.append('\'');
0652: ++i;
0653: } else {
0654: inQuote = !inQuote;
0655: }
0656: } else if (!inQuote
0657: && (ch >= 'a' && ch <= 'z' || ch >= 'A'
0658: && ch <= 'Z')) {
0659: // ch is a date-time pattern character to be interpreted
0660: // by subFormat(); count the number of times it is repeated
0661: prevCh = ch;
0662: ++count;
0663: } else {
0664: // Append quoted characters and unquoted non-pattern characters
0665: toAppendTo.append(ch);
0666: }
0667: }
0668: // Format the last item in the pattern, if any
0669: if (count > 0) {
0670: toAppendTo.append(subFormat(prevCh, count, toAppendTo
0671: .length(), pos, formatData, cal));
0672: }
0673: return toAppendTo;
0674: }
0675:
0676: // Map index into pattern character string to Calendar field number
0677: private static final int[] PATTERN_INDEX_TO_CALENDAR_FIELD = {
0678: /*GyM*/Calendar.ERA, Calendar.YEAR, Calendar.MONTH,
0679: /*dkH*/Calendar.DATE, Calendar.HOUR_OF_DAY, Calendar.HOUR_OF_DAY,
0680: /*msS*/Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND,
0681: /*EDF*/Calendar.DAY_OF_WEEK, Calendar.DAY_OF_YEAR,
0682: Calendar.DAY_OF_WEEK_IN_MONTH,
0683: /*wWa*/Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH,
0684: Calendar.AM_PM,
0685: /*hKz*/Calendar.HOUR, Calendar.HOUR, Calendar.ZONE_OFFSET,
0686: /*Yeu*/Calendar.YEAR_WOY, Calendar.DOW_LOCAL,
0687: Calendar.EXTENDED_YEAR,
0688: /*gAZ*/Calendar.JULIAN_DAY, Calendar.MILLISECONDS_IN_DAY,
0689: Calendar.ZONE_OFFSET,
0690: /*v*/Calendar.ZONE_OFFSET,
0691: /*c*/Calendar.DAY_OF_WEEK,
0692: /*L*/Calendar.MONTH,
0693: /*Qq*/Calendar.MONTH, Calendar.MONTH, };
0694:
0695: // Map index into pattern character string to DateFormat field number
0696: private static final int[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD = {
0697: /*GyM*/DateFormat.ERA_FIELD, DateFormat.YEAR_FIELD,
0698: DateFormat.MONTH_FIELD,
0699: /*dkH*/DateFormat.DATE_FIELD,
0700: DateFormat.HOUR_OF_DAY1_FIELD,
0701: DateFormat.HOUR_OF_DAY0_FIELD,
0702: /*msS*/DateFormat.MINUTE_FIELD, DateFormat.SECOND_FIELD,
0703: DateFormat.FRACTIONAL_SECOND_FIELD,
0704: /*EDF*/DateFormat.DAY_OF_WEEK_FIELD,
0705: DateFormat.DAY_OF_YEAR_FIELD,
0706: DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD,
0707: /*wWa*/DateFormat.WEEK_OF_YEAR_FIELD,
0708: DateFormat.WEEK_OF_MONTH_FIELD, DateFormat.AM_PM_FIELD,
0709: /*hKz*/DateFormat.HOUR1_FIELD, DateFormat.HOUR0_FIELD,
0710: DateFormat.TIMEZONE_FIELD,
0711: /*Yeu*/DateFormat.YEAR_WOY_FIELD,
0712: DateFormat.DOW_LOCAL_FIELD, DateFormat.EXTENDED_YEAR_FIELD,
0713: /*gAZ*/DateFormat.JULIAN_DAY_FIELD,
0714: DateFormat.MILLISECONDS_IN_DAY_FIELD,
0715: DateFormat.TIMEZONE_RFC_FIELD,
0716: /*v*/DateFormat.TIMEZONE_GENERIC_FIELD,
0717: /*c*/DateFormat.STANDALONE_DAY_FIELD,
0718: /*L*/DateFormat.STANDALONE_MONTH_FIELD,
0719: /*Q*/DateFormat.QUARTER_FIELD,
0720: /*q*/DateFormat.STANDALONE_QUARTER_FIELD, };
0721:
0722: /**
0723: * Format a single field, given its pattern character. Subclasses may
0724: * override this method in order to modify or add formatting
0725: * capabilities.
0726: * @param ch the pattern character
0727: * @param count the number of times ch is repeated in the pattern
0728: * @param beginOffset the offset of the output string at the start of
0729: * this field; used to set pos when appropriate
0730: * @param pos receives the position of a field, when appropriate
0731: * @param formatData the symbols for this formatter
0732: * @stable ICU 2.0
0733: */
0734: protected String subFormat(char ch, int count, int beginOffset,
0735: FieldPosition pos, DateFormatSymbols formatData,
0736: Calendar cal) throws IllegalArgumentException {
0737: // Note: formatData is ignored
0738: StringBuffer buf = new StringBuffer();
0739: subFormat(buf, ch, count, beginOffset, pos, cal);
0740: return buf.toString();
0741: }
0742:
0743: /**
0744: * Format a single field; useFastFormat variant. Reuses a
0745: * StringBuffer for results instead of creating a String on the
0746: * heap for each call.
0747: *
0748: * NOTE We don't really need the beginOffset parameter, EXCEPT for
0749: * the need to support the slow subFormat variant (above) which
0750: * has to pass it in to us.
0751: *
0752: * TODO make this API public
0753: *
0754: * @internal
0755: * @deprecated This API is ICU internal only.
0756: */
0757: protected void subFormat(StringBuffer buf, char ch, int count,
0758: int beginOffset, FieldPosition pos, Calendar cal) {
0759: final int maxIntCount = Integer.MAX_VALUE;
0760: final int bufstart = buf.length();
0761:
0762: final int patternCharIndex = DateFormatSymbols.patternChars
0763: .indexOf(ch);
0764: if (patternCharIndex == -1) {
0765: throw new IllegalArgumentException(
0766: "Illegal pattern character " + "'" + ch + "' in \""
0767: + new String(pattern) + '"');
0768: }
0769:
0770: final int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex];
0771: int value = cal.get(field);
0772:
0773: switch (patternCharIndex) {
0774: case 0: // 'G' - ERA
0775: if (count == 5) {
0776: buf.append(formatData.narrowEras[value]);
0777: } else if (count == 4)
0778: buf.append(formatData.eraNames[value]);
0779: else
0780: buf.append(formatData.eras[value]);
0781: break;
0782: case 1: // 'y' - YEAR
0783: /* According to the specification, if the number of pattern letters ('y') is 2,
0784: * the year is truncated to 2 digits; otherwise it is interpreted as a number.
0785: * But the original code process 'y', 'yy', 'yyy' in the same way. and process
0786: * patterns with 4 or more than 4 'y' characters in the same way.
0787: * So I change the codes to meet the specification. [Richard/GCl]
0788: */
0789: if (count == 2)
0790: zeroPaddingNumber(buf, value, 2, 2); // clip 1996 to 96
0791: else
0792: //count = 1 or count > 2
0793: zeroPaddingNumber(buf, value, count, maxIntCount);
0794: break;
0795: case 2: // 'M' - MONTH
0796: if (count == 5)
0797: buf.append(formatData.narrowMonths[value]);
0798: else if (count == 4)
0799: buf.append(formatData.months[value]);
0800: else if (count == 3)
0801: buf.append(formatData.shortMonths[value]);
0802: else
0803: zeroPaddingNumber(buf, value + 1, count, maxIntCount);
0804: break;
0805: case 4: // 'k' - HOUR_OF_DAY (1..24)
0806: if (value == 0)
0807: zeroPaddingNumber(buf, cal
0808: .getMaximum(Calendar.HOUR_OF_DAY) + 1, count,
0809: maxIntCount);
0810: else
0811: zeroPaddingNumber(buf, value, count, maxIntCount);
0812: break;
0813: case 8: // 'S' - FRACTIONAL_SECOND
0814: // Fractional seconds left-justify
0815: {
0816: numberFormat.setMinimumIntegerDigits(Math.min(3, count));
0817: numberFormat.setMaximumIntegerDigits(maxIntCount);
0818: if (count == 1) {
0819: value = (value + 50) / 100;
0820: } else if (count == 2) {
0821: value = (value + 5) / 10;
0822: }
0823: FieldPosition p = new FieldPosition(-1);
0824: numberFormat.format((long) value, buf, p);
0825: if (count > 3) {
0826: numberFormat.setMinimumIntegerDigits(count - 3);
0827: numberFormat.format(0L, buf, p);
0828: }
0829: }
0830: break;
0831: case 9: // 'E' - DAY_OF_WEEK
0832: if (count == 5) {
0833: buf.append(formatData.narrowWeekdays[value]);
0834: } else if (count == 4)
0835: buf.append(formatData.weekdays[value]);
0836: else
0837: // count <= 3, use abbreviated form if exists
0838: buf.append(formatData.shortWeekdays[value]);
0839: break;
0840: case 14: // 'a' - AM_PM
0841: buf.append(formatData.ampms[value]);
0842: break;
0843: case 15: // 'h' - HOUR (1..12)
0844: if (value == 0)
0845: zeroPaddingNumber(buf, cal
0846: .getLeastMaximum(Calendar.HOUR) + 1, count,
0847: maxIntCount);
0848: else
0849: zeroPaddingNumber(buf, value, count, maxIntCount);
0850: break;
0851: case 17: // 'z' - ZONE_OFFSET
0852: case 24: // 'v' - TIMEZONE_GENERIC
0853: {
0854:
0855: String zid;
0856: String res = null;
0857: zid = ZoneMeta.getCanonicalID(cal.getTimeZone().getID());
0858: boolean isGeneric = patternCharIndex == 24;
0859: if (zid != null) {
0860: if (patternCharIndex == TIMEZONE_GENERIC_FIELD) {
0861: if (count < 4) {
0862: res = formatData
0863: .getZoneString(
0864: zid,
0865: DateFormatSymbols.TIMEZONE_SHORT_GENERIC);
0866: } else {
0867: res = formatData
0868: .getZoneString(
0869: zid,
0870: DateFormatSymbols.TIMEZONE_LONG_GENERIC);
0871: }
0872: } else {
0873: if (cal.get(Calendar.DST_OFFSET) != 0) {
0874: if (count < 4) {
0875: res = formatData
0876: .getZoneString(
0877: zid,
0878: DateFormatSymbols.TIMEZONE_SHORT_DAYLIGHT);
0879: } else {
0880: res = formatData
0881: .getZoneString(
0882: zid,
0883: DateFormatSymbols.TIMEZONE_LONG_DAYLIGHT);
0884: }
0885: } else {
0886: if (count < 4) {
0887: res = formatData
0888: .getZoneString(
0889: zid,
0890: DateFormatSymbols.TIMEZONE_SHORT_STANDARD);
0891: } else {
0892: res = formatData
0893: .getZoneString(
0894: zid,
0895: DateFormatSymbols.TIMEZONE_LONG_STANDARD);
0896: }
0897: }
0898: }
0899: }
0900: if (res == null || res.length() == 0) {
0901: // note, tr35 does not describe the special case for 'no country'
0902: // implemented below, this is from discussion with Mark
0903: if (zid == null || !isGeneric
0904: || ZoneMeta.getCanonicalCountry(zid) == null) {
0905: long offset = cal.get(Calendar.ZONE_OFFSET)
0906: + cal.get(Calendar.DST_OFFSET);
0907: res = ZoneMeta.displayGMT(offset, locale);
0908: } else {
0909: /*
0910: String city = formatData.getZoneString(zid, DateFormatSymbols.TIMEZONE_EXEMPLAR_CITY);
0911: res = ZoneMeta.displayFallback(zid, city, locale);
0912: */
0913: res = formatData.getZoneString(zid,
0914: DateFormatSymbols.TIMEZONE_EXEMPLAR_CITY);
0915:
0916: if (res == null) {
0917: res = ZoneMeta.displayFallback(zid, null,
0918: locale);
0919: }
0920: }
0921: }
0922:
0923: if (res.length() == 0) {
0924: appendGMT(buf, cal);
0925: } else {
0926: buf.append(res);
0927: }
0928: }
0929: break;
0930: case 23: // 'Z' - TIMEZONE_RFC
0931: {
0932: if (count < 4) {
0933: // 'short' (standard Java) form, must use ASCII digits
0934: long val = (cal.get(Calendar.ZONE_OFFSET) + cal
0935: .get(Calendar.DST_OFFSET))
0936: / millisPerMinute;
0937: char sign = '+';
0938: if (val < 0) {
0939: val = -val;
0940: sign = '-';
0941: }
0942: val = (val / 60) * 100 + (val % 60); // minutes => KKmm
0943: buf.append(sign);
0944: fastZeroPaddingNubmer(buf, (int) val, 4, 4, '0');
0945: } else {
0946: // long form, localized GMT pattern
0947: // not in 3.4 locale data, need to add, so use same default as for general time zone names
0948: long val = cal.get(Calendar.ZONE_OFFSET)
0949: + cal.get(Calendar.DST_OFFSET);
0950: buf.append(ZoneMeta.displayGMT(val, locale));
0951: }
0952: }
0953: break;
0954: case 25: // 'c' - STANDALONE DAY
0955: if (count == 5)
0956: buf.append(formatData.standaloneNarrowWeekdays[value]);
0957: else if (count == 4)
0958: buf.append(formatData.standaloneWeekdays[value]);
0959: else if (count == 3)
0960: buf.append(formatData.standaloneShortWeekdays[value]);
0961: else
0962: zeroPaddingNumber(buf, value, 1, maxIntCount);
0963: break;
0964: case 26: // 'L' - STANDALONE MONTH
0965: if (count == 5)
0966: buf.append(formatData.standaloneNarrowMonths[value]);
0967: else if (count == 4)
0968: buf.append(formatData.standaloneMonths[value]);
0969: else if (count == 3)
0970: buf.append(formatData.standaloneShortMonths[value]);
0971: else
0972: zeroPaddingNumber(buf, value + 1, count, maxIntCount);
0973: break;
0974: case 27: // 'Q' - QUARTER
0975: if (count >= 4)
0976: buf.append(formatData.quarters[value / 3]);
0977: else if (count == 3)
0978: buf.append(formatData.shortQuarters[value / 3]);
0979: else
0980: zeroPaddingNumber(buf, (value / 3) + 1, count,
0981: maxIntCount);
0982: break;
0983: case 28: // 'q' - STANDALONE QUARTER
0984: if (count >= 4)
0985: buf.append(formatData.standaloneQuarters[value / 3]);
0986: else if (count == 3)
0987: buf
0988: .append(formatData.standaloneShortQuarters[value / 3]);
0989: else
0990: zeroPaddingNumber(buf, (value / 3) + 1, count,
0991: maxIntCount);
0992: break;
0993: default:
0994: // case 3: // 'd' - DATE
0995: // case 5: // 'H' - HOUR_OF_DAY (0..23)
0996: // case 6: // 'm' - MINUTE
0997: // case 7: // 's' - SECOND
0998: // case 10: // 'D' - DAY_OF_YEAR
0999: // case 11: // 'F' - DAY_OF_WEEK_IN_MONTH
1000: // case 12: // 'w' - WEEK_OF_YEAR
1001: // case 13: // 'W' - WEEK_OF_MONTH
1002: // case 16: // 'K' - HOUR (0..11)
1003: // case 18: // 'Y' - YEAR_WOY
1004: // case 19: // 'e' - DOW_LOCAL
1005: // case 20: // 'u' - EXTENDED_YEAR
1006: // case 21: // 'g' - JULIAN_DAY
1007: // case 22: // 'A' - MILLISECONDS_IN_DAY
1008:
1009: zeroPaddingNumber(buf, value, count, maxIntCount);
1010: break;
1011: } // switch (patternCharIndex)
1012:
1013: // Set the FieldPosition (for the first occurence only)
1014: if (pos.getBeginIndex() == pos.getEndIndex()
1015: && pos.getField() == PATTERN_INDEX_TO_DATE_FORMAT_FIELD[patternCharIndex]) {
1016: pos.setBeginIndex(beginOffset);
1017: pos.setEndIndex(beginOffset + buf.length() - bufstart);
1018: }
1019: }
1020:
1021: private void appendGMT(StringBuffer buf, Calendar cal) {
1022: int value = cal.get(Calendar.ZONE_OFFSET)
1023: + cal.get(Calendar.DST_OFFSET);
1024:
1025: if (value < 0) {
1026: buf.append(GMT_MINUS);
1027: value = -value; // suppress the '-' sign for text display.
1028: } else {
1029: buf.append(GMT_PLUS);
1030: }
1031:
1032: zeroPaddingNumber(buf, (int) (value / millisPerHour), 2, 2);
1033: buf.append((char) 0x003A) /*':'*/;
1034: zeroPaddingNumber(buf,
1035: (int) ((value % millisPerHour) / millisPerMinute), 2, 2);
1036: }
1037:
1038: /**
1039: * Internal method. Returns null if the value of an array is empty, or if the
1040: * index is out of bounds
1041: */
1042: private String getZoneArrayValue(String[] zs, int ix) {
1043: if (ix >= 0 && ix < zs.length) {
1044: String result = zs[ix];
1045: if (result != null && result.length() != 0) {
1046: return result;
1047: }
1048: }
1049: return null;
1050: }
1051:
1052: /**
1053: * Internal high-speed method. Reuses a StringBuffer for results
1054: * instead of creating a String on the heap for each call.
1055: * @internal
1056: * @deprecated This API is ICU internal only.
1057: */
1058: protected void zeroPaddingNumber(StringBuffer buf, int value,
1059: int minDigits, int maxDigits) {
1060: if (useFastZeroPaddingNumber) {
1061: fastZeroPaddingNubmer(buf, value, minDigits, maxDigits,
1062: zeroDigit);
1063: return;
1064: }
1065: FieldPosition pos = new FieldPosition(-1);
1066: numberFormat.setMinimumIntegerDigits(minDigits);
1067: numberFormat.setMaximumIntegerDigits(maxDigits);
1068: numberFormat.format(value, buf, pos);
1069: }
1070:
1071: /**
1072: * Internal faster method. This method does not use NumberFormat
1073: * to format digits.
1074: * @internal
1075: * @deprecated This API is ICU internal only.
1076: */
1077: private void fastZeroPaddingNubmer(StringBuffer buf, int value,
1078: int minDigits, int maxDigits, char zero) {
1079: value = value < 0 ? -value : value; //??
1080: minDigits = minDigits < maxDigits ? minDigits : maxDigits;
1081: int limit = decimalBuf.length < maxDigits ? decimalBuf.length
1082: : maxDigits;
1083: int index = limit - 1;
1084: while (true) {
1085: decimalBuf[index] = (char) ((value % 10) + zero);
1086: value /= 10;
1087: if (index == 0 || value == 0) {
1088: break;
1089: }
1090: index--;
1091: }
1092: int padding = minDigits - (limit - index);
1093: for (; padding > 0; padding--) {
1094: decimalBuf[--index] = zero;
1095: }
1096: buf.append(decimalBuf, index, limit - index);
1097: }
1098:
1099: /**
1100: * Overrides superclass method
1101: * @stable ICU 2.0
1102: */
1103: public void setNumberFormat(NumberFormat newNumberFormat) {
1104: super .setNumberFormat(newNumberFormat);
1105: if (newNumberFormat instanceof DecimalFormat) {
1106: zeroDigit = ((DecimalFormat) newNumberFormat)
1107: .getDecimalFormatSymbols().getZeroDigit();
1108: useFastZeroPaddingNumber = true;
1109: } else {
1110: useFastZeroPaddingNumber = false;
1111: }
1112: }
1113:
1114: /**
1115: * Formats a number with the specified minimum and maximum number of digits.
1116: * @stable ICU 2.0
1117: */
1118: protected String zeroPaddingNumber(long value, int minDigits,
1119: int maxDigits) {
1120: numberFormat.setMinimumIntegerDigits(minDigits);
1121: numberFormat.setMaximumIntegerDigits(maxDigits);
1122: return numberFormat.format(value);
1123: }
1124:
1125: /**
1126: * Format characters that indicate numeric fields. The character
1127: * at index 0 is treated specially.
1128: */
1129: private static final String NUMERIC_FORMAT_CHARS = "MyudhHmsSDFwWkK";
1130:
1131: /**
1132: * Return true if the given format character, occuring count
1133: * times, represents a numeric field.
1134: */
1135: private static final boolean isNumeric(char formatChar, int count) {
1136: int i = NUMERIC_FORMAT_CHARS.indexOf(formatChar);
1137: return (i > 0 || (i == 0 && count < 3));
1138: }
1139:
1140: /**
1141: * Overrides DateFormat
1142: * @see DateFormat
1143: * @stable ICU 2.0
1144: */
1145: public void parse(String text, Calendar cal, ParsePosition parsePos) {
1146: int pos = parsePos.getIndex();
1147: int start = pos;
1148: boolean[] ambiguousYear = { false };
1149: int count = 0;
1150:
1151: // hack, clear parsedTimeZone
1152: parsedTimeZone = null;
1153:
1154: // For parsing abutting numeric fields. 'abutPat' is the
1155: // offset into 'pattern' of the first of 2 or more abutting
1156: // numeric fields. 'abutStart' is the offset into 'text'
1157: // where parsing the fields begins. 'abutPass' starts off as 0
1158: // and increments each time we try to parse the fields.
1159: int abutPat = -1; // If >=0, we are in a run of abutting numeric fields
1160: int abutStart = 0;
1161: int abutPass = 0;
1162: boolean inQuote = false;
1163:
1164: for (int i = 0; i < pattern.length(); ++i) {
1165: char ch = pattern.charAt(i);
1166:
1167: // Handle alphabetic field characters.
1168: if (!inQuote
1169: && (ch >= 'A' && ch <= 'Z' || ch >= 'a'
1170: && ch <= 'z')) {
1171: int fieldPat = i;
1172:
1173: // Count the length of this field specifier
1174: count = 1;
1175: while ((i + 1) < pattern.length()
1176: && pattern.charAt(i + 1) == ch) {
1177: ++count;
1178: ++i;
1179: }
1180:
1181: if (isNumeric(ch, count)) {
1182: if (abutPat < 0) {
1183: // Determine if there is an abutting numeric field. For
1184: // most fields we can just look at the next characters,
1185: // but the 'm' field is either numeric or text,
1186: // depending on the count, so we have to look ahead for
1187: // that field.
1188: if ((i + 1) < pattern.length()) {
1189: boolean abutting;
1190: char nextCh = pattern.charAt(i + 1);
1191: int k = NUMERIC_FORMAT_CHARS
1192: .indexOf(nextCh);
1193: if (k == 0) {
1194: int j = i + 2;
1195: while (j < pattern.length()
1196: && pattern.charAt(j) == nextCh) {
1197: ++j;
1198: }
1199: abutting = (j - i) < 4; // nextCount < 3
1200: } else {
1201: abutting = k > 0;
1202: }
1203:
1204: // Record the start of a set of abutting numeric
1205: // fields.
1206: if (abutting) {
1207: abutPat = fieldPat;
1208: abutStart = pos;
1209: abutPass = 0;
1210: }
1211: }
1212: }
1213: } else {
1214: abutPat = -1; // End of any abutting fields
1215: }
1216:
1217: // Handle fields within a run of abutting numeric fields. Take
1218: // the pattern "HHmmss" as an example. We will try to parse
1219: // 2/2/2 characters of the input text, then if that fails,
1220: // 1/2/2. We only adjust the width of the leftmost field; the
1221: // others remain fixed. This allows "123456" => 12:34:56, but
1222: // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
1223: // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
1224: if (abutPat >= 0) {
1225: // If we are at the start of a run of abutting fields, then
1226: // shorten this field in each pass. If we can't shorten
1227: // this field any more, then the parse of this set of
1228: // abutting numeric fields has failed.
1229: if (fieldPat == abutPat) {
1230: count -= abutPass++;
1231: if (count == 0) {
1232: parsePos.setIndex(start);
1233: parsePos.setErrorIndex(pos);
1234: return;
1235: }
1236: }
1237:
1238: pos = subParse(text, pos, ch, count, true, false,
1239: ambiguousYear, cal);
1240:
1241: // If the parse fails anywhere in the run, back up to the
1242: // start of the run and retry.
1243: if (pos < 0) {
1244: i = abutPat - 1;
1245: pos = abutStart;
1246: continue;
1247: }
1248: }
1249:
1250: // Handle non-numeric fields and non-abutting numeric
1251: // fields.
1252: else {
1253: int s = pos;
1254: pos = subParse(text, pos, ch, count, false, true,
1255: ambiguousYear, cal);
1256:
1257: if (pos < 0) {
1258: parsePos.setErrorIndex(s);
1259: parsePos.setIndex(start);
1260: return;
1261: }
1262: }
1263: }
1264:
1265: // Handle literal pattern characters. These are any
1266: // quoted characters and non-alphabetic unquoted
1267: // characters.
1268: else {
1269:
1270: abutPat = -1; // End of any abutting fields
1271:
1272: // Handle quotes. Two consecutive quotes is a quote
1273: // literal, inside or outside of quotes. Otherwise a
1274: // quote indicates entry or exit from a quoted region.
1275: if (ch == '\'') {
1276: // Match a quote literal '' within OR outside of quotes
1277: if ((i + 1) < pattern.length()
1278: && pattern.charAt(i + 1) == ch) {
1279: ++i; // Skip over doubled quote
1280: // Fall through and treat quote as a literal
1281: } else {
1282: // Enter or exit quoted region
1283: inQuote = !inQuote;
1284: continue;
1285: }
1286: }
1287:
1288: // A run of white space in the pattern matches a run
1289: // of white space in the input text.
1290: if (UCharacterProperty.isRuleWhiteSpace(ch)) {
1291: // Advance over run in pattern
1292: while ((i + 1) < pattern.length()
1293: && UCharacterProperty
1294: .isRuleWhiteSpace(pattern
1295: .charAt(i + 1))) {
1296: ++i;
1297: }
1298:
1299: // Advance over run in input text
1300: int s = pos;
1301: while (pos < text.length()
1302: && UCharacter.isUWhiteSpace(text
1303: .charAt(pos))) {
1304: ++pos;
1305: }
1306:
1307: // Must see at least one white space char in input
1308: if (pos > s) {
1309: continue;
1310: }
1311: } else if (pos < text.length()
1312: && text.charAt(pos) == ch) {
1313: // Match a literal
1314: ++pos;
1315: continue;
1316: }
1317:
1318: // We fall through to this point if the match fails
1319: parsePos.setIndex(start);
1320: parsePos.setErrorIndex(pos);
1321: return;
1322: }
1323: }
1324:
1325: // At this point the fields of Calendar have been set. Calendar
1326: // will fill in default values for missing fields when the time
1327: // is computed.
1328:
1329: parsePos.setIndex(pos);
1330:
1331: // This part is a problem: When we call parsedDate.after, we compute the time.
1332: // Take the date April 3 2004 at 2:30 am. When this is first set up, the year
1333: // will be wrong if we're parsing a 2-digit year pattern. It will be 1904.
1334: // April 3 1904 is a Sunday (unlike 2004) so it is the DST onset day. 2:30 am
1335: // is therefore an "impossible" time, since the time goes from 1:59 to 3:00 am
1336: // on that day. It is therefore parsed out to fields as 3:30 am. Then we
1337: // add 100 years, and get April 3 2004 at 3:30 am. Note that April 3 2004 is
1338: // a Saturday, so it can have a 2:30 am -- and it should. [LIU]
1339: /*
1340: Date parsedDate = cal.getTime();
1341: if( ambiguousYear[0] && !parsedDate.after(defaultCenturyStart) ) {
1342: cal.add(Calendar.YEAR, 100);
1343: parsedDate = cal.getTime();
1344: }
1345: */
1346: // Because of the above condition, save off the fields in case we need to readjust.
1347: // The procedure we use here is not particularly efficient, but there is no other
1348: // way to do this given the API restrictions present in Calendar. We minimize
1349: // inefficiency by only performing this computation when it might apply, that is,
1350: // when the two-digit year is equal to the start year, and thus might fall at the
1351: // front or the back of the default century. This only works because we adjust
1352: // the year correctly to start with in other cases -- see subParse().
1353: try {
1354: if (ambiguousYear[0] || parsedTimeZone != null) {
1355: // We need a copy of the fields, and we need to avoid triggering a call to
1356: // complete(), which will recalculate the fields. Since we can't access
1357: // the fields[] array in Calendar, we clone the entire object. This will
1358: // stop working if Calendar.clone() is ever rewritten to call complete().
1359: Calendar copy = (Calendar) cal.clone();
1360: if (ambiguousYear[0]) { // the two-digit year == the default start year
1361: Date parsedDate = copy.getTime();
1362: if (parsedDate.before(defaultCenturyStart)) {
1363: // We can't use add here because that does a complete() first.
1364: cal.set(Calendar.YEAR,
1365: defaultCenturyStartYear + 100);
1366: }
1367: }
1368:
1369: if (parsedTimeZone != null) {
1370: TimeZone tz = parsedTimeZone;
1371:
1372: // the calendar represents the parse as gmt time
1373: // we need to turn this into local time, so we add the raw offset
1374: // then we ask the timezone to handle this local time
1375: int[] offsets = new int[2];
1376: tz.getOffset(copy.getTimeInMillis()
1377: + tz.getRawOffset(), true, offsets);
1378:
1379: cal.set(Calendar.ZONE_OFFSET, offsets[0]);
1380: cal.set(Calendar.DST_OFFSET, offsets[1]);
1381: cal.setTimeZone(tz);
1382: }
1383: }
1384: }
1385: // An IllegalArgumentException will be thrown by Calendar.getTime()
1386: // if any fields are out of range, e.g., MONTH == 17.
1387: catch (IllegalArgumentException e) {
1388: parsePos.setErrorIndex(pos);
1389: parsePos.setIndex(start);
1390: }
1391: }
1392:
1393: /**
1394: * Attempt to match the text at a given position against an array of
1395: * strings. Since multiple strings in the array may match (for
1396: * example, if the array contains "a", "ab", and "abc", all will match
1397: * the input string "abcd") the longest match is returned. As a side
1398: * effect, the given field of <code>cal</code> is set to the index
1399: * of the best match, if there is one.
1400: * @param text the time text being parsed.
1401: * @param start where to start parsing.
1402: * @param field the date field being parsed.
1403: * @param data the string array to parsed.
1404: * @return the new start position if matching succeeded; a negative
1405: * number indicating matching failure, otherwise. As a side effect,
1406: * sets the <code>cal</code> field <code>field</code> to the index
1407: * of the best match, if matching succeeded.
1408: * @stable ICU 2.0
1409: */
1410: protected int matchString(String text, int start, int field,
1411: String[] data, Calendar cal) {
1412: int i = 0;
1413: int count = data.length;
1414:
1415: if (field == Calendar.DAY_OF_WEEK)
1416: i = 1;
1417:
1418: // There may be multiple strings in the data[] array which begin with
1419: // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
1420: // We keep track of the longest match, and return that. Note that this
1421: // unfortunately requires us to test all array elements.
1422: int bestMatchLength = 0, bestMatch = -1;
1423: for (; i < count; ++i) {
1424: int length = data[i].length();
1425: // Always compare if we have no match yet; otherwise only compare
1426: // against potentially better matches (longer strings).
1427: if (length > bestMatchLength
1428: && text.regionMatches(true, start, data[i], 0,
1429: length)) {
1430: bestMatch = i;
1431: bestMatchLength = length;
1432: }
1433: }
1434: if (bestMatch >= 0) {
1435: cal.set(field, bestMatch);
1436: return start + bestMatchLength;
1437: }
1438: return -start;
1439: }
1440:
1441: /**
1442: * Attempt to match the text at a given position against an array of quarter
1443: * strings. Since multiple strings in the array may match (for
1444: * example, if the array contains "a", "ab", and "abc", all will match
1445: * the input string "abcd") the longest match is returned. As a side
1446: * effect, the given field of <code>cal</code> is set to the index
1447: * of the best match, if there is one.
1448: * @param text the time text being parsed.
1449: * @param start where to start parsing.
1450: * @param field the date field being parsed.
1451: * @param data the string array to parsed.
1452: * @return the new start position if matching succeeded; a negative
1453: * number indicating matching failure, otherwise. As a side effect,
1454: * sets the <code>cal</code> field <code>field</code> to the index
1455: * of the best match, if matching succeeded.
1456: * @stable ICU 2.0
1457: */
1458: protected int matchQuarterString(String text, int start, int field,
1459: String[] data, Calendar cal) {
1460: int i = 0;
1461: int count = data.length;
1462:
1463: // There may be multiple strings in the data[] array which begin with
1464: // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
1465: // We keep track of the longest match, and return that. Note that this
1466: // unfortunately requires us to test all array elements.
1467: int bestMatchLength = 0, bestMatch = -1;
1468: for (; i < count; ++i) {
1469: int length = data[i].length();
1470: // Always compare if we have no match yet; otherwise only compare
1471: // against potentially better matches (longer strings).
1472: if (length > bestMatchLength
1473: && text.regionMatches(true, start, data[i], 0,
1474: length)) {
1475: bestMatch = i;
1476: bestMatchLength = length;
1477: }
1478: }
1479:
1480: if (bestMatch >= 0) {
1481: cal.set(field, bestMatch * 3);
1482: return start + bestMatchLength;
1483: }
1484:
1485: return -start;
1486: }
1487:
1488: /**
1489: * find time zone 'text' matched zoneStrings and set cal
1490: */
1491: private int subParseZoneString(String text, int start, Calendar cal) {
1492: // At this point, check for named time zones by looking through
1493: // the locale data from the DateFormatZoneData strings.
1494: // Want to be able to parse both short and long forms.
1495:
1496: // optimize for calendar's current time zone
1497: TimeZone tz = null;
1498: String zid = null, value = null;
1499: int type = -1;
1500:
1501: DateFormatSymbols.ZoneItem item = formatData
1502: .findZoneIDTypeValue(text, start);
1503: if (item != null) {
1504: zid = item.zid;
1505: value = item.value;
1506: type = item.type;
1507: }
1508:
1509: if (zid != null) {
1510: tz = TimeZone.getTimeZone(zid);
1511: }
1512:
1513: if (tz != null) { // Matched any ?
1514: // always set zone offset, needed to get correct hour in wall time
1515: // when checking daylight savings
1516: cal.set(Calendar.ZONE_OFFSET, tz.getRawOffset());
1517: if (type == DateFormatSymbols.TIMEZONE_SHORT_STANDARD
1518: || type == DateFormatSymbols.TIMEZONE_LONG_STANDARD) {
1519: // standard time
1520: cal.set(Calendar.DST_OFFSET, 0);
1521: tz = null;
1522: } else if (type == DateFormatSymbols.TIMEZONE_SHORT_DAYLIGHT
1523: || type == DateFormatSymbols.TIMEZONE_LONG_DAYLIGHT) {
1524: // daylight time
1525: // use the correct DST SAVINGS for the zone.
1526: // cal.set(UCAL_DST_OFFSET, tz->getDSTSavings());
1527: cal.set(Calendar.DST_OFFSET, millisPerHour);
1528: tz = null;
1529: } else {
1530: // either standard or daylight
1531: // need to finish getting the date, then compute dst offset as
1532: // appropriate
1533: parsedTimeZone = tz;
1534: }
1535: if (value != null) {
1536: return start + value.length();
1537: }
1538: }
1539: // complete failure
1540: return 0;
1541: }
1542:
1543: /**
1544: * Protected method that converts one field of the input string into a
1545: * numeric field value in <code>cal</code>. Returns -start (for
1546: * ParsePosition) if failed. Subclasses may override this method to
1547: * modify or add parsing capabilities.
1548: * @param text the time text to be parsed.
1549: * @param start where to start parsing.
1550: * @param ch the pattern character for the date field text to be parsed.
1551: * @param count the count of a pattern character.
1552: * @param obeyCount if true, then the next field directly abuts this one,
1553: * and we should use the count to know when to stop parsing.
1554: * @param ambiguousYear return parameter; upon return, if ambiguousYear[0]
1555: * is true, then a two-digit year was parsed and may need to be readjusted.
1556: * @return the new start position if matching succeeded; a negative
1557: * number indicating matching failure, otherwise. As a side effect,
1558: * set the appropriate field of <code>cal</code> with the parsed
1559: * value.
1560: * @stable ICU 2.0
1561: */
1562: protected int subParse(String text, int start, char ch, int count,
1563: boolean obeyCount, boolean allowNegative,
1564: boolean[] ambiguousYear, Calendar cal) {
1565: Number number = null;
1566: int value = 0;
1567: int i;
1568: ParsePosition pos = new ParsePosition(0);
1569: int patternCharIndex = DateFormatSymbols.patternChars
1570: .indexOf(ch);
1571:
1572: if (patternCharIndex == -1) {
1573: return -start;
1574: }
1575:
1576: int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex];
1577:
1578: // If there are any spaces here, skip over them. If we hit the end
1579: // of the string, then fail.
1580: for (;;) {
1581: if (start >= text.length()) {
1582: return -start;
1583: }
1584: int c = UTF16.charAt(text, start);
1585: if (!UCharacter.isUWhiteSpace(c)) {
1586: break;
1587: }
1588: start += UTF16.getCharCount(c);
1589: }
1590: pos.setIndex(start);
1591:
1592: // We handle a few special cases here where we need to parse
1593: // a number value. We handle further, more generic cases below. We need
1594: // to handle some of them here because some fields require extra processing on
1595: // the parsed value.
1596: if (patternCharIndex == 4 /*HOUR_OF_DAY1_FIELD*/
1597: || patternCharIndex == 15 /*HOUR1_FIELD*/
1598: || (patternCharIndex == 2 /*MONTH_FIELD*/&& count <= 2)
1599: || patternCharIndex == 1 || patternCharIndex == 8) {
1600: // It would be good to unify this with the obeyCount logic below,
1601: // but that's going to be difficult.
1602: if (obeyCount) {
1603: if ((start + count) > text.length())
1604: return -start;
1605: number = parseInt(text.substring(0, start + count),
1606: pos, allowNegative);
1607: } else
1608: number = parseInt(text, pos, allowNegative);
1609: if (number == null)
1610: return -start;
1611: value = number.intValue();
1612: }
1613:
1614: switch (patternCharIndex) {
1615: case 0: // 'G' - ERA
1616: if (count == 4) {
1617: return matchString(text, start, Calendar.ERA,
1618: formatData.eraNames, cal);
1619: } else {
1620: return matchString(text, start, Calendar.ERA,
1621: formatData.eras, cal);
1622: }
1623: case 1: // 'y' - YEAR
1624: // If there are 3 or more YEAR pattern characters, this indicates
1625: // that the year value is to be treated literally, without any
1626: // two-digit year adjustments (e.g., from "01" to 2001). Otherwise
1627: // we made adjustments to place the 2-digit year in the proper
1628: // century, for parsed strings from "00" to "99". Any other string
1629: // is treated literally: "2250", "-1", "1", "002".
1630: /* 'yy' is the only special case, 'y' is interpreted as number. [Richard/GCL]*/
1631: if (count == 2 && (pos.getIndex() - start) == 2
1632: && Character.isDigit(text.charAt(start))
1633: && Character.isDigit(text.charAt(start + 1))) {
1634: // Assume for example that the defaultCenturyStart is 6/18/1903.
1635: // This means that two-digit years will be forced into the range
1636: // 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02
1637: // correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
1638: // to 1904, 1905, etc. If the year is 03, then it is 2003 if the
1639: // other fields specify a date before 6/18, or 1903 if they specify a
1640: // date afterwards. As a result, 03 is an ambiguous year. All other
1641: // two-digit years are unambiguous.
1642: int ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
1643: ambiguousYear[0] = value == ambiguousTwoDigitYear;
1644: value += (defaultCenturyStartYear / 100) * 100
1645: + (value < ambiguousTwoDigitYear ? 100 : 0);
1646: }
1647: cal.set(Calendar.YEAR, value);
1648: return pos.getIndex();
1649: case 2: // 'M' - MONTH
1650: if (count <= 2) // i.e., M or MM.
1651: {
1652: // Don't want to parse the month if it is a string
1653: // while pattern uses numeric style: M or MM.
1654: // [We computed 'value' above.]
1655: cal.set(Calendar.MONTH, value - 1);
1656: return pos.getIndex();
1657: } else {
1658: // count >= 3 // i.e., MMM or MMMM
1659: // Want to be able to parse both short and long forms.
1660: // Try count == 4 first:
1661: int newStart = matchString(text, start, Calendar.MONTH,
1662: formatData.months, cal);
1663: if (newStart > 0) {
1664: return newStart;
1665: } else { // count == 4 failed, now try count == 3
1666: return matchString(text, start, Calendar.MONTH,
1667: formatData.shortMonths, cal);
1668: }
1669: }
1670: case 26: // 'L' - STAND_ALONE_MONTH
1671: if (count <= 2) // i.e., M or MM.
1672: {
1673: // Don't want to parse the month if it is a string
1674: // while pattern uses numeric style: M or MM.
1675: // [We computed 'value' above.]
1676: cal.set(Calendar.MONTH, value - 1);
1677: return pos.getIndex();
1678: } else {
1679: // count >= 3 // i.e., MMM or MMMM
1680: // Want to be able to parse both short and long forms.
1681: // Try count == 4 first:
1682: int newStart = matchString(text, start, Calendar.MONTH,
1683: formatData.standaloneMonths, cal);
1684: if (newStart > 0) {
1685: return newStart;
1686: } else { // count == 4 failed, now try count == 3
1687: return matchString(text, start, Calendar.MONTH,
1688: formatData.standaloneShortMonths, cal);
1689: }
1690: }
1691: case 4: // 'k' - HOUR_OF_DAY (1..24)
1692: // [We computed 'value' above.]
1693: if (value == cal.getMaximum(Calendar.HOUR_OF_DAY) + 1)
1694: value = 0;
1695: cal.set(Calendar.HOUR_OF_DAY, value);
1696: return pos.getIndex();
1697: case 8: // 'S' - FRACTIONAL_SECOND
1698: // Fractional seconds left-justify
1699: i = pos.getIndex() - start;
1700: if (i < 3) {
1701: while (i < 3) {
1702: value *= 10;
1703: i++;
1704: }
1705: } else {
1706: int a = 1;
1707: while (i > 3) {
1708: a *= 10;
1709: i--;
1710: }
1711: value = (value + (a >> 1)) / a;
1712: }
1713: cal.set(Calendar.MILLISECOND, value);
1714: return pos.getIndex();
1715: case 9: { // 'E' - DAY_OF_WEEK
1716: // Want to be able to parse both short and long forms.
1717: // Try count == 4 (EEEE) first:
1718: int newStart = matchString(text, start,
1719: Calendar.DAY_OF_WEEK, formatData.weekdays, cal);
1720: if (newStart > 0) {
1721: return newStart;
1722: } else { // EEEE failed, now try EEE
1723: return matchString(text, start, Calendar.DAY_OF_WEEK,
1724: formatData.shortWeekdays, cal);
1725: }
1726: }
1727: case 25: { // 'c' - STAND_ALONE_DAY_OF_WEEK
1728: // Want to be able to parse both short and long forms.
1729: // Try count == 4 (cccc) first:
1730: int newStart = matchString(text, start,
1731: Calendar.DAY_OF_WEEK,
1732: formatData.standaloneWeekdays, cal);
1733: if (newStart > 0) {
1734: return newStart;
1735: } else { // cccc failed, now try ccc
1736: return matchString(text, start, Calendar.DAY_OF_WEEK,
1737: formatData.standaloneShortWeekdays, cal);
1738: }
1739: }
1740: case 14: // 'a' - AM_PM
1741: return matchString(text, start, Calendar.AM_PM,
1742: formatData.ampms, cal);
1743: case 15: // 'h' - HOUR (1..12)
1744: // [We computed 'value' above.]
1745: if (value == cal.getLeastMaximum(Calendar.HOUR) + 1)
1746: value = 0;
1747: cal.set(Calendar.HOUR, value);
1748: return pos.getIndex();
1749: case 17: // 'z' - ZONE_OFFSET
1750: case 23: // 'Z' - TIMEZONE_RFC
1751: case 24: // 'v' - TIMEZONE_GENERIC
1752: // First try to parse generic forms such as GMT-07:00. Do this first
1753: // in case localized DateFormatZoneData contains the string "GMT"
1754: // for a zone; in that case, we don't want to match the first three
1755: // characters of GMT+/-HH:MM etc.
1756: {
1757: int sign = 0;
1758: int offset;
1759:
1760: // For time zones that have no known names, look for strings
1761: // of the form:
1762: // GMT[+-]hours:minutes or
1763: // GMT[+-]hhmm or
1764: // GMT.
1765: if ((text.length() - start) >= GMT.length()
1766: && text.regionMatches(true, start, GMT, 0, GMT
1767: .length())) {
1768: cal.set(Calendar.DST_OFFSET, 0);
1769:
1770: pos.setIndex(start + GMT.length());
1771:
1772: try { // try-catch for "GMT" only time zone string
1773: switch (text.charAt(pos.getIndex())) {
1774: case '+':
1775: sign = 1;
1776: break;
1777: case '-':
1778: sign = -1;
1779: break;
1780: }
1781: } catch (StringIndexOutOfBoundsException e) {
1782: }
1783: if (sign == 0) {
1784: cal.set(Calendar.ZONE_OFFSET, 0);
1785: return pos.getIndex();
1786: }
1787:
1788: // Look for hours:minutes or hhmm.
1789: pos.setIndex(pos.getIndex() + 1);
1790: int st = pos.getIndex();
1791: Number tzNumber = numberFormat.parse(text, pos);
1792: if (tzNumber == null) {
1793: return -start;
1794: }
1795: if (pos.getIndex() < text.length()
1796: && text.charAt(pos.getIndex()) == ':') {
1797:
1798: // This is the hours:minutes case
1799: offset = tzNumber.intValue() * 60;
1800: pos.setIndex(pos.getIndex() + 1);
1801: tzNumber = numberFormat.parse(text, pos);
1802: if (tzNumber == null) {
1803: return -start;
1804: }
1805: offset += tzNumber.intValue();
1806: } else {
1807: // This is the hhmm case.
1808: offset = tzNumber.intValue();
1809: // Assume "-23".."+23" refers to hours.
1810: if (offset < 24 && (pos.getIndex() - st) <= 2)
1811: offset *= 60;
1812: else
1813: // todo: this looks questionable, should have more error checking
1814: offset = offset % 100 + offset / 100 * 60;
1815: }
1816:
1817: // Fall through for final processing below of 'offset' and 'sign'.
1818: } else {
1819: // At this point, check for named time zones by looking through
1820: // the locale data from the DateFormatZoneData strings.
1821: // Want to be able to parse both short and long forms.
1822: i = subParseZoneString(text, start, cal);
1823: if (i != 0)
1824: return i;
1825:
1826: // As a last resort, look for numeric timezones of the form
1827: // [+-]hhmm as specified by RFC 822. This code is actually
1828: // a little more permissive than RFC 822. It will try to do
1829: // its best with numbers that aren't strictly 4 digits long.
1830: DecimalFormat fmt = new DecimalFormat("+####;-####");
1831: fmt.setParseIntegerOnly(true);
1832: Number tzNumber = fmt.parse(text, pos);
1833: if (tzNumber == null) {
1834: return -start; // Wasn't actually a number.
1835: }
1836: offset = tzNumber.intValue();
1837: sign = 1;
1838: if (offset < 0) {
1839: sign = -1;
1840: offset = -offset;
1841: }
1842: // Assume "-23".."+23" refers to hours. Length includes sign.
1843: if (offset < 24 && (pos.getIndex() - start) <= 3)
1844: offset = offset * 60;
1845: else
1846: offset = offset % 100 + offset / 100 * 60;
1847:
1848: // Fall through for final processing below of 'offset' and 'sign'.
1849: }
1850:
1851: // Do the final processing for both of the above cases. We only
1852: // arrive here if the form GMT+/-... or an RFC 822 form was seen.
1853:
1854: // assert (sign != 0) : sign; // enable when guaranteed JDK >= 1.4
1855: offset *= millisPerMinute * sign;
1856:
1857: if (cal.getTimeZone().useDaylightTime()) {
1858: cal.set(Calendar.DST_OFFSET, millisPerHour);
1859: offset -= millisPerHour;
1860: }
1861: cal.set(Calendar.ZONE_OFFSET, offset);
1862:
1863: return pos.getIndex();
1864: }
1865:
1866: case 27: // 'Q' - QUARTER
1867: if (count <= 2) // i.e., Q or QQ.
1868: {
1869: // Don't want to parse the quarter if it is a string
1870: // while pattern uses numeric style: Q or QQ.
1871: // [We computed 'value' above.]
1872: cal.set(Calendar.MONTH, (value - 1) * 3);
1873: return pos.getIndex();
1874: } else {
1875: // count >= 3 // i.e., QQQ or QQQQ
1876: // Want to be able to parse both short and long forms.
1877: // Try count == 4 first:
1878: int newStart = matchQuarterString(text, start,
1879: Calendar.MONTH, formatData.quarters, cal);
1880: if (newStart > 0) {
1881: return newStart;
1882: } else { // count == 4 failed, now try count == 3
1883: return matchQuarterString(text, start,
1884: Calendar.MONTH, formatData.shortQuarters,
1885: cal);
1886: }
1887: }
1888:
1889: case 28: // 'q' - STANDALONE QUARTER
1890: if (count <= 2) // i.e., q or qq.
1891: {
1892: // Don't want to parse the quarter if it is a string
1893: // while pattern uses numeric style: q or qq.
1894: // [We computed 'value' above.]
1895: cal.set(Calendar.MONTH, (value - 1) * 3);
1896: return pos.getIndex();
1897: } else {
1898: // count >= 3 // i.e., qqq or qqqq
1899: // Want to be able to parse both short and long forms.
1900: // Try count == 4 first:
1901: int newStart = matchQuarterString(text, start,
1902: Calendar.MONTH, formatData.standaloneQuarters,
1903: cal);
1904: if (newStart > 0) {
1905: return newStart;
1906: } else { // count == 4 failed, now try count == 3
1907: return matchQuarterString(text, start,
1908: Calendar.MONTH,
1909: formatData.standaloneShortQuarters, cal);
1910: }
1911: }
1912:
1913: default:
1914: // case 3: // 'd' - DATE
1915: // case 5: // 'H' - HOUR_OF_DAY (0..23)
1916: // case 6: // 'm' - MINUTE
1917: // case 7: // 's' - SECOND
1918: // case 10: // 'D' - DAY_OF_YEAR
1919: // case 11: // 'F' - DAY_OF_WEEK_IN_MONTH
1920: // case 12: // 'w' - WEEK_OF_YEAR
1921: // case 13: // 'W' - WEEK_OF_MONTH
1922: // case 16: // 'K' - HOUR (0..11)
1923: // case 18: // 'Y' - YEAR_WOY
1924: // case 19: // 'e' - DOW_LOCAL
1925: // case 20: // 'u' - EXTENDED_YEAR
1926: // case 21: // 'g' - JULIAN_DAY
1927: // case 22: // 'A' - MILLISECONDS_IN_DAY
1928:
1929: // Handle "generic" fields
1930: if (obeyCount) {
1931: if ((start + count) > text.length())
1932: return -start;
1933: number = parseInt(text.substring(0, start + count),
1934: pos, allowNegative);
1935: } else
1936: number = parseInt(text, pos, allowNegative);
1937: if (number != null) {
1938: cal.set(field, number.intValue());
1939: return pos.getIndex();
1940: }
1941: return -start;
1942: }
1943: }
1944:
1945: /**
1946: * Parse an integer using fNumberFormat. This method is semantically
1947: * const, but actually may modify fNumberFormat.
1948: */
1949: private Number parseInt(String text, ParsePosition pos,
1950: boolean allowNegative) {
1951: String oldPrefix = null;
1952: DecimalFormat df = null;
1953: if (!allowNegative) {
1954: try {
1955: df = (DecimalFormat) numberFormat;
1956: oldPrefix = df.getNegativePrefix();
1957: df.setNegativePrefix(SUPPRESS_NEGATIVE_PREFIX);
1958: } catch (ClassCastException e1) {
1959: }
1960: }
1961: Number number = numberFormat.parse(text, pos);
1962: if (df != null) {
1963: df.setNegativePrefix(oldPrefix);
1964: }
1965: return number;
1966: }
1967:
1968: /**
1969: * Translate a pattern, mapping each character in the from string to the
1970: * corresponding character in the to string.
1971: */
1972: private String translatePattern(String pattern, String from,
1973: String to) {
1974: StringBuffer result = new StringBuffer();
1975: boolean inQuote = false;
1976: for (int i = 0; i < pattern.length(); ++i) {
1977: char c = pattern.charAt(i);
1978: if (inQuote) {
1979: if (c == '\'')
1980: inQuote = false;
1981: } else {
1982: if (c == '\'')
1983: inQuote = true;
1984: else if ((c >= 'a' && c <= 'z')
1985: || (c >= 'A' && c <= 'Z')) {
1986: int ci = from.indexOf(c);
1987: if (ci != -1) {
1988: c = to.charAt(ci);
1989: }
1990: // do not worry on translatepattern if the character is not listed
1991: // we do the validity check elsewhere
1992: }
1993: }
1994: result.append(c);
1995: }
1996: if (inQuote)
1997: throw new IllegalArgumentException(
1998: "Unfinished quote in pattern");
1999: return result.toString();
2000: }
2001:
2002: /**
2003: * Return a pattern string describing this date format.
2004: * @stable ICU 2.0
2005: */
2006: public String toPattern() {
2007: return pattern;
2008: }
2009:
2010: /**
2011: * Return a localized pattern string describing this date format.
2012: * @stable ICU 2.0
2013: */
2014: public String toLocalizedPattern() {
2015: return translatePattern(pattern,
2016: DateFormatSymbols.patternChars,
2017: formatData.localPatternChars);
2018: }
2019:
2020: /**
2021: * Apply the given unlocalized pattern string to this date format.
2022: * @stable ICU 2.0
2023: */
2024: public void applyPattern(String pattern) {
2025: this .pattern = pattern;
2026: setLocale(null, null);
2027: }
2028:
2029: /**
2030: * Apply the given localized pattern string to this date format.
2031: * @stable ICU 2.0
2032: */
2033: public void applyLocalizedPattern(String pattern) {
2034: this .pattern = translatePattern(pattern,
2035: formatData.localPatternChars,
2036: DateFormatSymbols.patternChars);
2037: setLocale(null, null);
2038: }
2039:
2040: /**
2041: * Gets the date/time formatting data.
2042: * @return a copy of the date-time formatting data associated
2043: * with this date-time formatter.
2044: * @stable ICU 2.0
2045: */
2046: public DateFormatSymbols getDateFormatSymbols() {
2047: return (DateFormatSymbols) formatData.clone();
2048: }
2049:
2050: /**
2051: * Allows you to set the date/time formatting data.
2052: * @param newFormatSymbols the new symbols
2053: * @stable ICU 2.0
2054: */
2055: public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
2056: this .formatData = (DateFormatSymbols) newFormatSymbols.clone();
2057: }
2058:
2059: /**
2060: * Method for subclasses to access the DateFormatSymbols.
2061: * @stable ICU 2.0
2062: */
2063: protected DateFormatSymbols getSymbols() {
2064: return formatData;
2065: }
2066:
2067: /**
2068: * Overrides Cloneable
2069: * @stable ICU 2.0
2070: */
2071: public Object clone() {
2072: SimpleDateFormat other = (SimpleDateFormat) super .clone();
2073: other.formatData = (DateFormatSymbols) formatData.clone();
2074: return other;
2075: }
2076:
2077: /**
2078: * Override hashCode.
2079: * Generates the hash code for the SimpleDateFormat object
2080: * @stable ICU 2.0
2081: */
2082: public int hashCode() {
2083: return pattern.hashCode();
2084: // just enough fields for a reasonable distribution
2085: }
2086:
2087: /**
2088: * Override equals.
2089: * @stable ICU 2.0
2090: */
2091: public boolean equals(Object obj) {
2092: if (!super .equals(obj))
2093: return false; // super does class check
2094: SimpleDateFormat that = (SimpleDateFormat) obj;
2095: return (pattern.equals(that.pattern) && formatData
2096: .equals(that.formatData));
2097: }
2098:
2099: /**
2100: * Override readObject.
2101: */
2102: private void readObject(ObjectInputStream stream)
2103: throws IOException, ClassNotFoundException {
2104: stream.defaultReadObject();
2105: ///CLOVER:OFF
2106: // don't have old serial data to test with
2107: if (serialVersionOnStream < 1) {
2108: // didn't have defaultCenturyStart field
2109: initializeDefaultCentury();
2110: }
2111: ///CLOVER:ON
2112: else {
2113: // fill in dependent transient field
2114: parseAmbiguousDatesAsAfter(defaultCenturyStart);
2115: }
2116: serialVersionOnStream = currentSerialVersion;
2117: locale = getLocale(ULocale.VALID_LOCALE);
2118: }
2119: }
|