| /* |
| ******************************************************************************* |
| * Copyright (C) 1996-2004, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ******************************************************************************* |
| */ |
| |
| package com.ibm.icu.text; |
| |
| import com.ibm.icu.util.Calendar; |
| import com.ibm.icu.lang.UCharacter; |
| import com.ibm.icu.impl.ICUResourceBundle; |
| import com.ibm.icu.impl.CalendarData; |
| import com.ibm.icu.impl.UCharacterProperty; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.ULocale; |
| import com.ibm.icu.util.UResourceBundle; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.lang.ref.WeakReference; |
| import java.text.FieldPosition; |
| import java.text.MessageFormat; |
| import java.text.ParsePosition; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Hashtable; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * <code>SimpleDateFormat</code> is a concrete class for formatting and |
| * parsing dates in a locale-sensitive manner. It allows for formatting |
| * (date -> text), parsing (text -> date), and normalization. |
| * |
| * <p> |
| * <code>SimpleDateFormat</code> allows you to start by choosing |
| * any user-defined patterns for date-time formatting. However, you |
| * are encouraged to create a date-time formatter with either |
| * <code>getTimeInstance</code>, <code>getDateInstance</code>, or |
| * <code>getDateTimeInstance</code> in <code>DateFormat</code>. Each |
| * of these class methods can return a date/time formatter initialized |
| * with a default format pattern. You may modify the format pattern |
| * using the <code>applyPattern</code> methods as desired. |
| * For more information on using these methods, see |
| * {@link DateFormat}. |
| * |
| * <p> |
| * <strong>Time Format Syntax:</strong> |
| * <p> |
| * To specify the time format use a <em>time pattern</em> string. |
| * In this pattern, all ASCII letters are reserved as pattern letters, |
| * which are defined as the following: |
| * <blockquote> |
| * <pre> |
| * Symbol Meaning Presentation Example |
| * ------ ------- ------------ ------- |
| * G era designator (Text) AD |
| * y year (Number) 1996 |
| * Y year (week of year) (Number) 1997 |
| * u extended year (Number) 4601 |
| * M month in year (Text & Number) July & 07 |
| * d day in month (Number) 10 |
| * h hour in am/pm (1~12) (Number) 12 |
| * H hour in day (0~23) (Number) 0 |
| * m minute in hour (Number) 30 |
| * s second in minute (Number) 55 |
| * S fractional second (Number) 978 |
| * E day of week (Text) Tuesday |
| * e day of week (local 1~7) (Number) 2 |
| * D day in year (Number) 189 |
| * F day of week in month (Number) 2 (2nd Wed in July) |
| * w week in year (Number) 27 |
| * W week in month (Number) 2 |
| * a am/pm marker (Text) PM |
| * k hour in day (1~24) (Number) 24 |
| * K hour in am/pm (0~11) (Number) 0 |
| * z time zone (Text) Pacific Standard Time |
| * Z time zone (RFC 822) (Number) -0800 |
| * g Julian day (Number) 2451334 |
| * A milliseconds in day (Number) 69540000 |
| * ' escape for text (Delimiter) 'Date=' |
| * '' single quote (Literal) 'o''clock' |
| * </pre> |
| * </blockquote> |
| * The count of pattern letters determine the format. |
| * <p> |
| * <strong>(Text)</strong>: 4 or more pattern letters--use full form, |
| * < 4--use short or abbreviated form if one exists. |
| * <p> |
| * <strong>(Number)</strong>: the minimum number of digits. Shorter |
| * numbers are zero-padded to this amount. Year is handled specially; |
| * that is, if the count of 'y' is 2, the Year will be truncated to 2 digits. |
| * (e.g., if "yyyy" produces "1997", "yy" produces "97".) |
| * Unlike other fields, fractional seconds are padded on the right with zero. |
| * <p> |
| * <strong>(Text & Number)</strong>: 3 or over, use text, otherwise use number. |
| * <p> |
| * Any characters in the pattern that are not in the ranges of ['a'..'z'] |
| * and ['A'..'Z'] will be treated as quoted text. For instance, characters |
| * like ':', '.', ' ', '#' and '@' will appear in the resulting time text |
| * even they are not embraced within single quotes. |
| * <p> |
| * A pattern containing any invalid pattern letter will result in a thrown |
| * exception during formatting or parsing. |
| * |
| * <p> |
| * <strong>Examples Using the US Locale:</strong> |
| * <blockquote> |
| * <pre> |
| * Format Pattern Result |
| * -------------- ------- |
| * "yyyy.MM.dd G 'at' HH:mm:ss z" ->> 1996.07.10 AD at 15:08:56 PDT |
| * "EEE, MMM d, ''yy" ->> Wed, July 10, '96 |
| * "h:mm a" ->> 12:08 PM |
| * "hh 'o''clock' a, zzzz" ->> 12 o'clock PM, Pacific Daylight Time |
| * "K:mm a, z" ->> 0:00 PM, PST |
| * "yyyyy.MMMMM.dd GGG hh:mm aaa" ->> 01996.July.10 AD 12:08 PM |
| * </pre> |
| * </blockquote> |
| * <strong>Code Sample:</strong> |
| * <blockquote> |
| * <pre> |
| * SimpleTimeZone pdt = new SimpleTimeZone(-8 * 60 * 60 * 1000, "PST"); |
| * pdt.setStartRule(Calendar.APRIL, 1, Calendar.SUNDAY, 2*60*60*1000); |
| * pdt.setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2*60*60*1000); |
| * <br> |
| * // Format the current time. |
| * SimpleDateFormat formatter |
| * = new SimpleDateFormat ("yyyy.MM.dd G 'at' hh:mm:ss a zzz"); |
| * Date currentTime_1 = new Date(); |
| * String dateString = formatter.format(currentTime_1); |
| * <br> |
| * // Parse the previous string back into a Date. |
| * ParsePosition pos = new ParsePosition(0); |
| * Date currentTime_2 = formatter.parse(dateString, pos); |
| * </pre> |
| * </blockquote> |
| * In the example, the time value <code>currentTime_2</code> obtained from |
| * parsing will be equal to <code>currentTime_1</code>. However, they may not be |
| * equal if the am/pm marker 'a' is left out from the format pattern while |
| * the "hour in am/pm" pattern symbol is used. This information loss can |
| * happen when formatting the time in PM. |
| * |
| * <p> |
| * When parsing a date string using the abbreviated year pattern ("yy"), |
| * SimpleDateFormat 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 SimpleDateFormat |
| * instance is created. For example, using a pattern of "MM/dd/yy" and a |
| * SimpleDateFormat instance created on Jan 1, 1997, the string |
| * "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64" |
| * would be interpreted as May 4, 1964. |
| * During parsing, only strings consisting of exactly two digits, as defined by |
| * {@link java.lang.Character#isDigit(char)}, will be parsed into the default |
| * century. |
| * Any other numeric string, such as a one digit string, a three or more digit |
| * string, or a two digit string that isn't all digits (for example, "-1"), is |
| * interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the |
| * same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC. |
| * |
| * <p> |
| * If the year pattern does not have exactly two 'y' characters, the year is |
| * interpreted literally, regardless of the number of digits. So using the |
| * pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D. |
| * |
| * <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> |
| * For time zones that have no names, use strings GMT+hours:minutes or |
| * GMT-hours:minutes. |
| * |
| * <p> |
| * The calendar defines what is the first day of the week, the first week |
| * of the year, whether hours are zero based or not (0 vs 12 or 24), and the |
| * time zone. There is one common decimal format to handle all the numbers; |
| * the digit count is handled programmatically according to the pattern. |
| * |
| * <h4>Synchronization</h4> |
| * |
| * Date formats are not synchronized. It is recommended to create separate |
| * format instances for each thread. If multiple threads access a format |
| * concurrently, it must be synchronized externally. |
| * |
| * @see com.ibm.icu.util.Calendar |
| * @see com.ibm.icu.util.GregorianCalendar |
| * @see com.ibm.icu.util.TimeZone |
| * @see DateFormat |
| * @see DateFormatSymbols |
| * @see DecimalFormat |
| * @author Mark Davis, Chen-Lieh Huang, Alan Liu |
| * @stable ICU 2.0 |
| */ |
| public class SimpleDateFormat extends DateFormat { |
| |
| // the official serial version ID which says cryptically |
| // which version we're compatible with |
| static final long serialVersionUID = 4774881970558875024L; |
| |
| // the internal serial version which says which version was written |
| // - 0 (default) for version up to JDK 1.1.3 |
| // - 1 for version from JDK 1.1.4, which includes a new field |
| static final int currentSerialVersion = 1; |
| |
| /** |
| * The version of the serialized data on the stream. Possible values: |
| * <ul> |
| * <li><b>0</b> or not present on stream: JDK 1.1.3. This version |
| * has no <code>defaultCenturyStart</code> on stream. |
| * <li><b>1</b> JDK 1.1.4 or later. This version adds |
| * <code>defaultCenturyStart</code>. |
| * </ul> |
| * When streaming out this class, the most recent format |
| * and the highest allowable <code>serialVersionOnStream</code> |
| * is written. |
| * @serial |
| */ |
| private int serialVersionOnStream = currentSerialVersion; |
| |
| /** |
| * The pattern string of this formatter. This is always a non-localized |
| * pattern. May not be null. See class documentation for details. |
| * @serial |
| */ |
| private String pattern; |
| |
| /** |
| * The symbols used by this formatter for week names, month names, |
| * etc. May not be null. |
| * @serial |
| * @see DateFormatSymbols |
| */ |
| private DateFormatSymbols formatData; |
| |
| /** |
| * We map dates with two-digit years into the century starting at |
| * <code>defaultCenturyStart</code>, which may be any date. May |
| * not be null. |
| * @serial |
| * @since JDK1.1.4 |
| */ |
| private Date defaultCenturyStart; |
| |
| transient private int defaultCenturyStartYear; |
| |
| private static final int millisPerHour = 60 * 60 * 1000; |
| private static final int millisPerMinute = 60 * 1000; |
| |
| // For time zones that have no names, use strings GMT+minutes and |
| // GMT-minutes. For instance, in France the time zone is GMT+60. |
| private static final String GMT_PLUS = "GMT+"; |
| private static final String GMT_MINUS = "GMT-"; |
| private static final String GMT = "GMT"; |
| |
| // This prefix is designed to NEVER MATCH real text, in order to |
| // suppress the parsing of negative numbers. Adjust as needed (if |
| // this becomes valid Unicode). |
| private static final String SUPPRESS_NEGATIVE_PREFIX = "\uAB00"; |
| |
| /** |
| * Cache to hold the DateTimePatterns of a Locale. |
| */ |
| private static Hashtable cachedLocaleData = new Hashtable(3); |
| |
| /** |
| * Construct a SimpleDateFormat using the default pattern for the default |
| * locale. <b>Note:</b> Not all locales support SimpleDateFormat; for full |
| * generality, use the factory methods in the DateFormat class. |
| * |
| * @see DateFormat |
| * @stable ICU 2.0 |
| */ |
| public SimpleDateFormat() { |
| this(SHORT, SHORT, Locale.getDefault()); |
| } |
| |
| /** |
| * Construct a SimpleDateFormat using the given pattern in the default |
| * locale. <b>Note:</b> Not all locales support SimpleDateFormat; for full |
| * generality, use the factory methods in the DateFormat class. |
| * @stable ICU 2.0 |
| */ |
| public SimpleDateFormat(String pattern) |
| { |
| this(pattern, Locale.getDefault()); |
| } |
| |
| /** |
| * Construct a SimpleDateFormat using the given pattern and locale. |
| * <b>Note:</b> Not all locales support SimpleDateFormat; for full |
| * generality, use the factory methods in the DateFormat class. |
| * @stable ICU 2.0 |
| */ |
| public SimpleDateFormat(String pattern, Locale loc) |
| { |
| this.pattern = pattern; |
| this.formatData = new DateFormatSymbols(loc); |
| initialize(loc); |
| } |
| |
| /** |
| * Construct a SimpleDateFormat using the given pattern and |
| * locale-specific symbol data. |
| * @stable ICU 2.0 |
| */ |
| public SimpleDateFormat(String pattern, DateFormatSymbols formatData) |
| { |
| this.pattern = pattern; |
| this.formatData = (DateFormatSymbols) formatData.clone(); |
| initialize(Locale.getDefault()); |
| } |
| |
| // try caching |
| private static final boolean CACHE = true; |
| private static long cacheAge; |
| private static WeakReference highCacheRef; |
| |
| /* Package-private, called by DateFormat factory methods */ |
| SimpleDateFormat(int timeStyle, int dateStyle, Locale loc) { |
| // try a high level cache first! |
| |
| Map map = null; |
| String key = null; |
| if (CACHE) { |
| // age test is so we don't have to compute the century start all the time... once a day is enough. |
| long time = System.currentTimeMillis(); |
| if (((time - cacheAge) < 1000*60*60*24L) && highCacheRef != null) { |
| map = (Map)highCacheRef.get(); |
| } |
| if (map == null) { |
| map = new HashMap(3); |
| highCacheRef = new WeakReference(map); |
| cacheAge = time; |
| } |
| key = loc.toString() + timeStyle + dateStyle; |
| SimpleDateFormat target = (SimpleDateFormat)map.get(key); |
| if (target != null) { // kindof skanky |
| // if ("en_US22".equals(key)) |
| // System.out.println("\nfound key: " + key + " pat: " + target.pattern + |
| // " cal: " + target.calendar + " fmt: " + target.numberFormat); |
| this.pattern = target.pattern; |
| this.formatData = target.formatData; |
| this.defaultCenturyStart = target.defaultCenturyStart; |
| this.defaultCenturyStartYear = target.defaultCenturyStartYear; |
| this.calendar = (Calendar)target.calendar.clone(); |
| this.calendar.setTimeZone(TimeZone.getDefault()); // might have changed since cached |
| this.numberFormat = (NumberFormat)target.numberFormat.clone(); |
| return; |
| } |
| } |
| |
| /* try the cache first */ |
| String[] dateTimePatterns = (String[]) cachedLocaleData.get(loc); |
| if (dateTimePatterns == null) { /* cache miss */ |
| CalendarData calData = new CalendarData(new ULocale(loc), null); // TODO: type? |
| // TODO: get correct actual/valid locale here |
| ULocale uloc = calData.getULocale(); |
| setLocale(uloc, uloc); |
| |
| dateTimePatterns = calData.getStringArray("DateTimePatterns"); |
| /* update cache */ |
| cachedLocaleData.put(loc, dateTimePatterns); |
| } |
| formatData = new DateFormatSymbols(loc); |
| if ((timeStyle >= 0) && (dateStyle >= 0)) { |
| Object[] dateTimeArgs = {dateTimePatterns[timeStyle], |
| dateTimePatterns[dateStyle + 4]}; |
| pattern = MessageFormat.format(dateTimePatterns[8], dateTimeArgs); |
| } |
| else if (timeStyle >= 0) { |
| pattern = dateTimePatterns[timeStyle]; |
| } |
| else if (dateStyle >= 0) { |
| pattern = dateTimePatterns[dateStyle + 4]; |
| } |
| else { |
| throw new IllegalArgumentException("No date or time style specified"); |
| } |
| |
| initialize(loc); |
| |
| if (CACHE) { |
| // if ("en_US22".equals(key)) |
| // System.out.println("\nregister key: " + key + " pat: " + this.pattern + |
| // " cal: " + this.calendar + " fmt: " + this.numberFormat); |
| map.put(key, this.clone()); // ok if we stomp existing target due to threading |
| } |
| } |
| |
| /* Initialize calendar and numberFormat fields */ |
| private void initialize(Locale loc) { |
| // The format object must be constructed using the symbols for this zone. |
| // However, the calendar should use the current default TimeZone. |
| // If this is not contained in the locale zone strings, then the zone |
| // will be formatted using generic GMT+/-H:MM nomenclature. |
| calendar = Calendar.getInstance(TimeZone.getDefault(), loc); |
| numberFormat = NumberFormat.getInstance(loc); |
| numberFormat.setGroupingUsed(false); |
| ///CLOVER:OFF |
| // difficult to test for case where NumberFormat.getInstance does not |
| // return a DecimalFormat |
| if (numberFormat instanceof DecimalFormat) |
| ((DecimalFormat)numberFormat).setDecimalSeparatorAlwaysShown(false); |
| ///CLOVER:ON |
| numberFormat.setParseIntegerOnly(true); /* So that dd.MM.yy can be parsed */ |
| numberFormat.setMinimumFractionDigits(0); // To prevent "Jan 1.00, 1997.00" |
| |
| initializeDefaultCentury(); |
| } |
| |
| /* Initialize the fields we use to disambiguate ambiguous years. Separate |
| * so we can call it from readObject(). |
| */ |
| private void initializeDefaultCentury() { |
| calendar.setTime( new Date() ); |
| calendar.add( Calendar.YEAR, -80 ); |
| parseAmbiguousDatesAsAfter(calendar.getTime()); |
| } |
| |
| /* Define one-century window into which to disambiguate dates using |
| * two-digit years. |
| */ |
| private void parseAmbiguousDatesAsAfter(Date startDate) { |
| defaultCenturyStart = startDate; |
| calendar.setTime(startDate); |
| defaultCenturyStartYear = calendar.get(Calendar.YEAR); |
| } |
| |
| /** |
| * Sets the 100-year period 2-digit years will be interpreted as being in |
| * to begin on the date the user specifies. |
| * @param startDate During parsing, two digit years will be placed in the range |
| * <code>startDate</code> to <code>startDate + 100 years</code>. |
| * @stable ICU 2.0 |
| */ |
| public void set2DigitYearStart(Date startDate) { |
| parseAmbiguousDatesAsAfter(startDate); |
| } |
| |
| /** |
| * Returns the beginning date of the 100-year period 2-digit years are interpreted |
| * as being within. |
| * @return the start of the 100-year period into which two digit years are |
| * parsed |
| * @stable ICU 2.0 |
| */ |
| public Date get2DigitYearStart() { |
| return defaultCenturyStart; |
| } |
| |
| /** |
| * Overrides DateFormat. |
| * <p>Formats a date or time, which is the standard millis |
| * since January 1, 1970, 00:00:00 GMT. |
| * <p>Example: using the US locale: |
| * "yyyy.MM.dd G 'at' HH:mm:ss zzz" ->> 1996.07.10 AD at 15:08:56 PDT |
| * @param cal the calendar whose date-time value is to be formatted into a date-time string |
| * @param toAppendTo where the new date-time text is to be appended |
| * @param pos the formatting position. On input: an alignment field, |
| * if desired. On output: the offsets of the alignment field. |
| * @return the formatted date-time string. |
| * @see DateFormat |
| * @stable ICU 2.0 |
| */ |
| public StringBuffer format(Calendar cal, StringBuffer toAppendTo, |
| FieldPosition pos) |
| { |
| // Initialize |
| pos.setBeginIndex(0); |
| pos.setEndIndex(0); |
| |
| boolean inQuote = false; // true when between single quotes |
| char prevCh = 0; // previous pattern character |
| int count = 0; // number of time prevCh repeated |
| for (int i=0; i<pattern.length(); ++i) { |
| char ch = pattern.charAt(i); |
| // Use subFormat() to format a repeated pattern character |
| // when a different pattern or non-pattern character is seen |
| if (ch != prevCh && count > 0) { |
| toAppendTo.append( |
| subFormat(prevCh, count, toAppendTo.length(), pos, formatData, cal)); |
| count = 0; |
| } |
| if (ch == '\'') { |
| // Consecutive single quotes are a single quote literal, |
| // either outside of quotes or between quotes |
| if ((i+1)<pattern.length() && pattern.charAt(i+1) == '\'') { |
| toAppendTo.append('\''); |
| ++i; |
| } else { |
| inQuote = !inQuote; |
| } |
| } else if (!inQuote |
| && (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 |
| prevCh = ch; |
| ++count; |
| } |
| else { |
| // Append quoted characters and unquoted non-pattern characters |
| toAppendTo.append(ch); |
| } |
| } |
| // Format the last item in the pattern, if any |
| if (count > 0) { |
| toAppendTo.append( |
| subFormat(prevCh, count, toAppendTo.length(), pos, formatData, cal)); |
| } |
| return toAppendTo; |
| } |
| |
| // Map index into pattern character string to Calendar field number |
| private static final int[] PATTERN_INDEX_TO_CALENDAR_FIELD = |
| { |
| /*GyM*/ Calendar.ERA, Calendar.YEAR, Calendar.MONTH, |
| /*dkH*/ Calendar.DATE, Calendar.HOUR_OF_DAY, Calendar.HOUR_OF_DAY, |
| /*msS*/ Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND, |
| /*EDF*/ Calendar.DAY_OF_WEEK, Calendar.DAY_OF_YEAR, Calendar.DAY_OF_WEEK_IN_MONTH, |
| /*wWa*/ Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH, Calendar.AM_PM, |
| /*hKz*/ Calendar.HOUR, Calendar.HOUR, Calendar.ZONE_OFFSET, |
| /*Yeu*/ Calendar.YEAR_WOY, Calendar.DOW_LOCAL, Calendar.EXTENDED_YEAR, |
| /*gAZ*/ Calendar.JULIAN_DAY, Calendar.MILLISECONDS_IN_DAY, Calendar.ZONE_OFFSET |
| }; |
| |
| // Map index into pattern character string to DateFormat field number |
| private static final int[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD = { |
| /*GyM*/ DateFormat.ERA_FIELD, DateFormat.YEAR_FIELD, DateFormat.MONTH_FIELD, |
| /*dkH*/ DateFormat.DATE_FIELD, DateFormat.HOUR_OF_DAY1_FIELD, DateFormat.HOUR_OF_DAY0_FIELD, |
| /*msS*/ DateFormat.MINUTE_FIELD, DateFormat.SECOND_FIELD, DateFormat.FRACTIONAL_SECOND_FIELD, |
| /*EDF*/ DateFormat.DAY_OF_WEEK_FIELD, DateFormat.DAY_OF_YEAR_FIELD, DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD, |
| /*wWa*/ DateFormat.WEEK_OF_YEAR_FIELD, DateFormat.WEEK_OF_MONTH_FIELD, DateFormat.AM_PM_FIELD, |
| /*hKz*/ DateFormat.HOUR1_FIELD, DateFormat.HOUR0_FIELD, DateFormat.TIMEZONE_FIELD, |
| /*Yeu*/ DateFormat.YEAR_WOY_FIELD, DateFormat.DOW_LOCAL_FIELD, DateFormat.EXTENDED_YEAR_FIELD, |
| /*gAZ*/ DateFormat.JULIAN_DAY_FIELD, DateFormat.MILLISECONDS_IN_DAY_FIELD, DateFormat.TIMEZONE_RFC_FIELD |
| }; |
| |
| /** |
| * Format a single field, given its pattern character. Subclasses may |
| * override this method in order to modify or add formatting |
| * capabilities. |
| * @param ch the pattern character |
| * @param count the number of times ch is repeated in the pattern |
| * @param beginOffset the offset of the output string at the start of |
| * this field; used to set pos when appropriate |
| * @param pos receives the position of a field, when appropriate |
| * @param formatData the symbols for this formatter |
| * @stable ICU 2.0 |
| */ |
| protected String subFormat(char ch, int count, int beginOffset, |
| FieldPosition pos, DateFormatSymbols formatData, |
| Calendar cal) |
| throws IllegalArgumentException |
| { |
| int maxIntCount = Integer.MAX_VALUE; |
| String current = ""; |
| |
| int patternCharIndex = DateFormatSymbols.patternChars.indexOf(ch); |
| if (patternCharIndex == -1) { |
| throw new IllegalArgumentException("Illegal pattern character " + |
| "'" + ch + "'"); |
| } |
| |
| int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; |
| int value = cal.get(field); |
| |
| switch (patternCharIndex) { |
| case 0: // 'G' - ERA |
| current = formatData.eras[value]; |
| break; |
| case 1: // 'y' - YEAR |
| /* According to the specification, if the number of pattern letters ('y') is 2, |
| * the year is truncated to 2 digits; otherwise it is interpreted as a number. |
| * But the original code process 'y', 'yy', 'yyy' in the same way. and process |
| * patterns with 4 or more than 4 'y' characters in the same way. |
| * So I change the codes to meet the specification. [Richard/GCl] |
| */ |
| if (count == 2) |
| current = zeroPaddingNumber(value, 2, 2); // clip 1996 to 96 |
| else //count = 1 or count > 2 |
| current = zeroPaddingNumber(value, count, maxIntCount); |
| break; |
| case 2: // 'M' - MONTH |
| if (count >= 4) |
| current = formatData.months[value]; |
| else if (count == 3) |
| current = formatData.shortMonths[value]; |
| else |
| current = zeroPaddingNumber(value+1, count, maxIntCount); |
| break; |
| case 4: // 'k' - HOUR_OF_DAY (1..24) |
| if (value == 0) |
| current = zeroPaddingNumber( |
| cal.getMaximum(Calendar.HOUR_OF_DAY)+1, |
| count, maxIntCount); |
| else |
| current = zeroPaddingNumber(value, count, maxIntCount); |
| break; |
| case 8: // 'S' - FRACTIONAL_SECOND |
| // Fractional seconds left-justify |
| { |
| StringBuffer buf = new StringBuffer(); |
| numberFormat.setMinimumIntegerDigits(Math.min(3, count)); |
| numberFormat.setMaximumIntegerDigits(maxIntCount); |
| if (count == 1) { |
| value = (value + 50) / 100; |
| } else if (count == 2) { |
| value = (value + 5) / 10; |
| } |
| FieldPosition p = new FieldPosition(0); |
| numberFormat.format((long) value, buf, p); |
| if (count > 3) { |
| numberFormat.setMinimumIntegerDigits(count - 3); |
| numberFormat.format(0L, buf, p); |
| } |
| current = buf.toString(); |
| } |
| break; |
| case 9: // 'E' - DAY_OF_WEEK |
| if (count >= 4) |
| current = formatData.weekdays[value]; |
| else // count < 4, use abbreviated form if exists |
| current = formatData.shortWeekdays[value]; |
| break; |
| case 14: // 'a' - AM_PM |
| current = formatData.ampms[value]; |
| break; |
| case 15: // 'h' - HOUR (1..12) |
| if (value == 0) |
| current = zeroPaddingNumber( |
| cal.getLeastMaximum(Calendar.HOUR)+1, |
| count, maxIntCount); |
| else |
| current = zeroPaddingNumber(value, count, maxIntCount); |
| break; |
| case 17: // 'z' - ZONE_OFFSET |
| int zoneIndex |
| = formatData.getZoneIndex (cal.getTimeZone().getID()); |
| if (zoneIndex == -1) |
| { |
| // For time zones that have no names, use strings |
| // GMT+hours:minutes and GMT-hours:minutes. |
| // For instance, France time zone uses GMT+01:00. |
| StringBuffer zoneString = new StringBuffer(); |
| |
| value = cal.get(Calendar.ZONE_OFFSET) + |
| cal.get(Calendar.DST_OFFSET); |
| |
| if (value < 0) |
| { |
| zoneString.append(GMT_MINUS); |
| value = -value; // suppress the '-' sign for text display. |
| } |
| else |
| zoneString.append(GMT_PLUS); |
| zoneString.append( |
| zeroPaddingNumber((int)(value/millisPerHour), 2, 2)); |
| zoneString.append(':'); |
| zoneString.append( |
| zeroPaddingNumber( |
| (int)((value%millisPerHour)/millisPerMinute), 2, 2)); |
| current = zoneString.toString(); |
| } |
| else if (cal.get(Calendar.DST_OFFSET) != 0) |
| { |
| if (count >= 4) |
| current = formatData.zoneStrings[zoneIndex][3]; |
| else |
| // count < 4, use abbreviated form if exists |
| current = formatData.zoneStrings[zoneIndex][4]; |
| } |
| else |
| { |
| if (count >= 4) |
| current = formatData.zoneStrings[zoneIndex][1]; |
| else |
| current = formatData.zoneStrings[zoneIndex][2]; |
| } |
| break; |
| case 23: // 'Z' - TIMEZONE_RFC |
| { |
| char sign = '+'; |
| value = (cal.get(Calendar.ZONE_OFFSET) + |
| cal.get(Calendar.DST_OFFSET)) / millisPerMinute; |
| if (value < 0) { |
| value = -value; |
| sign = '-'; |
| } |
| value = (value / 3) * 5 + (value % 60); // minutes => KKmm |
| StringBuffer buf = new StringBuffer(); |
| buf.append(sign).append(zeroPaddingNumber(value, 4, 4)); |
| current = buf.toString(); |
| } |
| break; |
| default: |
| // case 3: // 'd' - DATE |
| // case 5: // 'H' - HOUR_OF_DAY (0..23) |
| // case 6: // 'm' - MINUTE |
| // case 7: // 's' - SECOND |
| // case 10: // 'D' - DAY_OF_YEAR |
| // case 11: // 'F' - DAY_OF_WEEK_IN_MONTH |
| // case 12: // 'w' - WEEK_OF_YEAR |
| // case 13: // 'W' - WEEK_OF_MONTH |
| // case 16: // 'K' - HOUR (0..11) |
| // case 18: // 'Y' - YEAR_WOY |
| // case 19: // 'e' - DOW_LOCAL |
| // case 20: // 'u' - EXTENDED_YEAR |
| // case 21: // 'g' - JULIAN_DAY |
| // case 22: // 'A' - MILLISECONDS_IN_DAY |
| |
| current = zeroPaddingNumber(value, count, maxIntCount); |
| break; |
| } // switch (patternCharIndex) |
| |
| // Set the FieldPosition (for the first occurence only) |
| if (pos.getBeginIndex() == pos.getEndIndex() && |
| pos.getField() == PATTERN_INDEX_TO_DATE_FORMAT_FIELD[patternCharIndex]) { |
| pos.setBeginIndex(beginOffset); |
| pos.setEndIndex(beginOffset + current.length()); |
| } |
| |
| return current; |
| } |
| |
| /** |
| * Formats a number with the specified minimum and maximum number of digits. |
| * @stable ICU 2.0 |
| */ |
| protected String zeroPaddingNumber(long value, int minDigits, int maxDigits) |
| { |
| numberFormat.setMinimumIntegerDigits(minDigits); |
| numberFormat.setMaximumIntegerDigits(maxDigits); |
| return numberFormat.format(value); |
| } |
| |
| /** |
| * Format characters that indicate numeric fields. The character |
| * at index 0 is treated specially. |
| */ |
| private static final String NUMERIC_FORMAT_CHARS = "MyudhHmsSDFwWkK"; |
| |
| /** |
| * Return true if the given format character, occuring count |
| * times, represents a numeric field. |
| */ |
| private static final boolean isNumeric(char formatChar, int count) { |
| int i = NUMERIC_FORMAT_CHARS.indexOf(formatChar); |
| return (i > 0 || (i == 0 && count < 3)); |
| } |
| |
| /** |
| * Overrides DateFormat |
| * @see DateFormat |
| * @stable ICU 2.0 |
| */ |
| public void parse(String text, Calendar cal, ParsePosition parsePos) |
| { |
| int pos = parsePos.getIndex(); |
| int start = pos; |
| boolean[] ambiguousYear = {false}; |
| int count = 0; |
| |
| // 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; |
| boolean inQuote = false; |
| |
| for (int i=0; i<pattern.length(); ++i) { |
| char ch = pattern.charAt(i); |
| |
| // Handle alphabetic field characters. |
| if (!inQuote && (ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z')) { |
| int fieldPat = i; |
| |
| // Count the length of this field specifier |
| count = 1; |
| while ((i+1)<pattern.length() && |
| pattern.charAt(i+1) == ch) { |
| ++count; |
| ++i; |
| } |
| |
| if (isNumeric(ch, count)) { |
| if (abutPat < 0) { |
| // Determine if there is an abutting numeric field. For |
| // most fields we can just look at the next characters, |
| // but the 'm' field is either numeric or text, |
| // depending on the count, so we have to look ahead for |
| // that field. |
| if ((i+1)<pattern.length()) { |
| boolean abutting; |
| char nextCh = pattern.charAt(i+1); |
| int k = NUMERIC_FORMAT_CHARS.indexOf(nextCh); |
| if (k == 0) { |
| int j = i+2; |
| while (j<pattern.length() && |
| pattern.charAt(j) == nextCh) { |
| ++j; |
| } |
| abutting = (j-i) < 4; // nextCount < 3 |
| } else { |
| abutting = k > 0; |
| } |
| |
| // Record the start of a set of abutting numeric |
| // fields. |
| if (abutting) { |
| abutPat = fieldPat; |
| abutStart = pos; |
| abutPass = 0; |
| } |
| } |
| } |
| } else { |
| abutPat = -1; // End of any abutting fields |
| } |
| |
| // 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. |
| if (fieldPat == abutPat) { |
| count -= abutPass++; |
| if (count == 0) { |
| parsePos.setIndex(start); |
| parsePos.setErrorIndex(pos); |
| return; |
| } |
| } |
| |
| pos = subParse(text, pos, ch, count, |
| true, false, ambiguousYear, cal); |
| |
| // If the parse fails anywhere in the run, back up to the |
| // start of the run and retry. |
| if (pos < 0) { |
| i = abutPat - 1; |
| pos = abutStart; |
| continue; |
| } |
| } |
| |
| // Handle non-numeric fields and non-abutting numeric |
| // fields. |
| else { |
| int s = pos; |
| pos = subParse(text, pos, ch, count, |
| false, true, ambiguousYear, cal); |
| |
| if (pos < 0) { |
| parsePos.setErrorIndex(s); |
| parsePos.setIndex(start); |
| return; |
| } |
| } |
| } |
| |
| // Handle literal pattern characters. These are any |
| // quoted characters and non-alphabetic unquoted |
| // characters. |
| else { |
| |
| abutPat = -1; // End of any abutting fields |
| |
| // Handle quotes. Two consecutive quotes is a quote |
| // literal, inside or outside of quotes. Otherwise a |
| // quote indicates entry or exit from a quoted region. |
| if (ch == '\'') { |
| // Match a quote literal '' within OR outside of quotes |
| if ((i+1)<pattern.length() && pattern.charAt(i+1)==ch) { |
| ++i; // Skip over doubled quote |
| // Fall through and treat quote as a literal |
| } else { |
| // Enter or exit quoted region |
| inQuote = !inQuote; |
| continue; |
| } |
| } |
| |
| // A run of white space in the pattern matches a run |
| // of white space in the input text. |
| if (UCharacterProperty.isRuleWhiteSpace(ch)) { |
| // Advance over run in pattern |
| while ((i+1)<pattern.length() && |
| UCharacterProperty.isRuleWhiteSpace(pattern.charAt(i+1))) { |
| ++i; |
| } |
| |
| // Advance over run in input text |
| int s = pos; |
| while (pos<text.length() && |
| UCharacter.isUWhiteSpace(text.charAt(pos))) { |
| ++pos; |
| } |
| |
| // Must see at least one white space char in input |
| if (pos > s) { |
| continue; |
| } |
| } else if (pos<text.length() && text.charAt(pos)==ch) { |
| // Match a literal |
| ++pos; |
| continue; |
| } |
| |
| // We fall through to this point if the match fails |
| parsePos.setIndex(start); |
| parsePos.setErrorIndex(pos); |
| return; |
| } |
| } |
| |
| // At this point the fields of Calendar have been set. Calendar |
| // will fill in default values for missing fields when the time |
| // is computed. |
| |
| parsePos.setIndex(pos); |
| |
| // This part is a problem: When we call parsedDate.after, we compute the time. |
| // Take the date April 3 2004 at 2:30 am. When this is first set up, the year |
| // will be wrong if we're parsing a 2-digit year pattern. It will be 1904. |
| // April 3 1904 is a Sunday (unlike 2004) so it is the DST onset day. 2:30 am |
| // is therefore an "impossible" time, since the time goes from 1:59 to 3:00 am |
| // on that day. It is therefore parsed out to fields as 3:30 am. Then we |
| // add 100 years, and get April 3 2004 at 3:30 am. Note that April 3 2004 is |
| // a Saturday, so it can have a 2:30 am -- and it should. [LIU] |
| /* |
| Date parsedDate = cal.getTime(); |
| if( ambiguousYear[0] && !parsedDate.after(defaultCenturyStart) ) { |
| cal.add(Calendar.YEAR, 100); |
| parsedDate = cal.getTime(); |
| } |
| */ |
| // Because of the above condition, save off the fields in case we need to readjust. |
| // The procedure we use here is not particularly efficient, but there is no other |
| // way to do this given the API restrictions present in Calendar. We minimize |
| // inefficiency by only performing this computation when it might apply, that is, |
| // when the two-digit year is equal to the start year, and thus might fall at the |
| // front or the back of the default century. This only works because we adjust |
| // the year correctly to start with in other cases -- see subParse(). |
| try { |
| if (ambiguousYear[0]) // If this is true then the two-digit year == the default start year |
| { |
| // We need a copy of the fields, and we need to avoid triggering a call to |
| // complete(), which will recalculate the fields. Since we can't access |
| // the fields[] array in Calendar, we clone the entire object. This will |
| // stop working if Calendar.clone() is ever rewritten to call complete(). |
| Calendar copy = (Calendar)cal.clone(); |
| Date parsedDate = copy.getTime(); |
| if (parsedDate.before(defaultCenturyStart)) |
| { |
| // We can't use add here because that does a complete() first. |
| cal.set(Calendar.YEAR, defaultCenturyStartYear + 100); |
| } |
| } |
| } |
| // An IllegalArgumentException will be thrown by Calendar.getTime() |
| // if any fields are out of range, e.g., MONTH == 17. |
| catch (IllegalArgumentException e) { |
| parsePos.setErrorIndex(pos); |
| parsePos.setIndex(start); |
| } |
| } |
| |
| /** |
| * Attempt 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. As a side |
| * effect, the given field of <code>cal</code> is set to the index |
| * of the best match, if there is one. |
| * @param text the time text being parsed. |
| * @param start where to start parsing. |
| * @param field the date field being parsed. |
| * @param data the string array to parsed. |
| * @return the new start position if matching succeeded; a negative |
| * number indicating matching failure, otherwise. As a side effect, |
| * sets the <code>cal</code> field <code>field</code> to the index |
| * of the best match, if matching succeeded. |
| * @stable ICU 2.0 |
| */ |
| protected int matchString(String text, int start, int field, String[] data, Calendar cal) |
| { |
| int i = 0; |
| int count = data.length; |
| |
| if (field == Calendar.DAY_OF_WEEK) i = 1; |
| |
| // 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; |
| for (; 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 && |
| text.regionMatches(true, start, data[i], 0, length)) |
| { |
| bestMatch = i; |
| bestMatchLength = length; |
| } |
| } |
| if (bestMatch >= 0) |
| { |
| cal.set(field, bestMatch); |
| return start + bestMatchLength; |
| } |
| return -start; |
| } |
| |
| private int matchZoneString(String text, int start, int zoneIndex) { |
| int j; |
| for (j = 1; j <= 4; ++j) { |
| // Checking long and short zones [1 & 2], |
| // and long and short daylight [3 & 4]. |
| if (text.regionMatches(true, start, |
| formatData.zoneStrings[zoneIndex][j], 0, |
| formatData.zoneStrings[zoneIndex][j].length())) { |
| break; |
| } |
| } |
| return (j > 4) ? -1 : j; |
| } |
| |
| /** |
| * find time zone 'text' matched zoneStrings and set cal |
| */ |
| private int subParseZoneString(String text, int start, Calendar 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. |
| int zoneIndex = |
| formatData.getZoneIndex (getTimeZone().getID()); |
| TimeZone tz = null; |
| int j = 0, i = 0; |
| if (zoneIndex != -1) { |
| j = matchZoneString(text, start, zoneIndex); |
| if (j > 0) { |
| tz = TimeZone.getTimeZone(formatData.zoneStrings[zoneIndex][0]); |
| i = zoneIndex; |
| } |
| } |
| |
| if (tz == null) { |
| zoneIndex = |
| formatData.getZoneIndex (TimeZone.getDefault().getID()); |
| if (zoneIndex != -1) { |
| j = matchZoneString(text, start, zoneIndex); |
| if (j > 0) { |
| tz = TimeZone.getTimeZone(formatData.zoneStrings[zoneIndex][0]); |
| i = zoneIndex; |
| } |
| } |
| } |
| |
| if (tz == null) { |
| for (i = 0; i < formatData.zoneStrings.length; i++) { |
| j = matchZoneString(text, start, i); |
| if (j > 0) { |
| tz = TimeZone.getTimeZone(formatData.zoneStrings[i][0]); |
| break; |
| } |
| } |
| } |
| |
| if (tz != null) { // Matched any ? |
| cal.set(Calendar.ZONE_OFFSET, tz.getRawOffset()); |
| int savings = 0; |
| if (j >= 3) { |
| savings = tz.getDSTSavings(); |
| } |
| cal.set(Calendar.DST_OFFSET, savings); |
| return (start + formatData.zoneStrings[i][j].length()); |
| } |
| return 0; |
| } |
| |
| /** |
| * Protected method that converts one field of the input string into a |
| * numeric field value in <code>cal</code>. Returns -start (for |
| * ParsePosition) if failed. Subclasses may override this method to |
| * modify or add parsing capabilities. |
| * @param text the time text to be parsed. |
| * @param start where to start parsing. |
| * @param ch the pattern character for the date field text to be parsed. |
| * @param count the count of a pattern character. |
| * @param obeyCount if true, then the next field directly abuts this one, |
| * and we should use the count to know when to stop parsing. |
| * @param ambiguousYear return parameter; upon return, if ambiguousYear[0] |
| * is true, then a two-digit year was parsed and may need to be readjusted. |
| * @return the new start position if matching succeeded; a negative |
| * number indicating matching failure, otherwise. As a side effect, |
| * set the appropriate field of <code>cal</code> with the parsed |
| * value. |
| * @stable ICU 2.0 |
| */ |
| protected int subParse(String text, int start, char ch, int count, |
| boolean obeyCount, boolean allowNegative, |
| boolean[] ambiguousYear, Calendar cal) |
| { |
| Number number = null; |
| int value = 0; |
| int i; |
| ParsePosition pos = new ParsePosition(0); |
| int patternCharIndex = DateFormatSymbols.patternChars.indexOf(ch); |
| |
| if (patternCharIndex == -1) { |
| return -start; |
| } |
| |
| int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; |
| |
| // If there are any spaces here, skip over them. If we hit the end |
| // of the string, then fail. |
| for (;;) { |
| if (start >= text.length()) { |
| return -start; |
| } |
| int c = UTF16.charAt(text, start); |
| if (!UCharacter.isUWhiteSpace(c)) { |
| break; |
| } |
| start += UTF16.getCharCount(c); |
| } |
| pos.setIndex(start); |
| |
| // We handle a few special cases here where we need to parse |
| // a number value. We handle further, more generic cases below. We need |
| // to handle some of them here because some fields require extra processing on |
| // the parsed value. |
| if (patternCharIndex == 4 /*HOUR_OF_DAY1_FIELD*/ || |
| patternCharIndex == 15 /*HOUR1_FIELD*/ || |
| (patternCharIndex == 2 /*MONTH_FIELD*/ && count <= 2) || |
| patternCharIndex == 1 || |
| patternCharIndex == 8) |
| { |
| // It would be good to unify this with the obeyCount logic below, |
| // but that's going to be difficult. |
| if (obeyCount) |
| { |
| if ((start+count) > text.length()) return -start; |
| number = parseInt(text.substring(0, start+count), pos, allowNegative); |
| } |
| else number = parseInt(text, pos, allowNegative); |
| if (number == null) |
| return -start; |
| value = number.intValue(); |
| } |
| |
| switch (patternCharIndex) |
| { |
| case 0: // 'G' - ERA |
| return matchString(text, start, Calendar.ERA, formatData.eras, cal); |
| case 1: // 'y' - YEAR |
| // If there are 3 or more YEAR pattern characters, this indicates |
| // that the year value is to be treated literally, without any |
| // two-digit year adjustments (e.g., from "01" to 2001). Otherwise |
| // we made adjustments to place the 2-digit year in the proper |
| // century, for parsed strings from "00" to "99". Any other string |
| // is treated literally: "2250", "-1", "1", "002". |
| /* 'yy' is the only special case, 'y' is interpreted as number. [Richard/GCL]*/ |
| if (count == 2 && (pos.getIndex() - start) == 2 |
| && Character.isDigit(text.charAt(start)) |
| && Character.isDigit(text.charAt(start+1))) |
| { |
| // 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. |
| int ambiguousTwoDigitYear = defaultCenturyStartYear % 100; |
| ambiguousYear[0] = value == ambiguousTwoDigitYear; |
| value += (defaultCenturyStartYear/100)*100 + |
| (value < ambiguousTwoDigitYear ? 100 : 0); |
| } |
| cal.set(Calendar.YEAR, value); |
| return pos.getIndex(); |
| case 2: // 'M' - MONTH |
| if (count <= 2) // i.e., M or MM. |
| { |
| // Don't want to parse the month if it is a string |
| // while pattern uses numeric style: M or MM. |
| // [We computed 'value' above.] |
| cal.set(Calendar.MONTH, value - 1); |
| return pos.getIndex(); |
| } |
| else |
| { |
| // count >= 3 // i.e., MMM or MMMM |
| // Want to be able to parse both short and long forms. |
| // Try count == 4 first: |
| int newStart = matchString(text, start, Calendar.MONTH, |
| formatData.months, cal); |
| if (newStart > 0) { |
| return newStart; |
| } else { // count == 4 failed, now try count == 3 |
| return matchString(text, start, Calendar.MONTH, |
| formatData.shortMonths, cal); |
| } |
| } |
| case 4: // 'k' - HOUR_OF_DAY (1..24) |
| // [We computed 'value' above.] |
| if (value == cal.getMaximum(Calendar.HOUR_OF_DAY)+1) value = 0; |
| cal.set(Calendar.HOUR_OF_DAY, value); |
| return pos.getIndex(); |
| case 8: // 'S' - FRACTIONAL_SECOND |
| // Fractional seconds left-justify |
| i = pos.getIndex() - 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.set(Calendar.MILLISECOND, value); |
| return pos.getIndex(); |
| case 9: { // 'E' - DAY_OF_WEEK |
| // Want to be able to parse both short and long forms. |
| // Try count == 4 (DDDD) first: |
| int newStart = matchString(text, start, Calendar.DAY_OF_WEEK, |
| formatData.weekdays, cal); |
| if (newStart > 0) { |
| return newStart; |
| } else { // DDDD failed, now try DDD |
| return matchString(text, start, Calendar.DAY_OF_WEEK, |
| formatData.shortWeekdays, cal); |
| } |
| } |
| case 14: // 'a' - AM_PM |
| return matchString(text, start, Calendar.AM_PM, formatData.ampms, cal); |
| case 15: // 'h' - HOUR (1..12) |
| // [We computed 'value' above.] |
| if (value == cal.getLeastMaximum(Calendar.HOUR)+1) value = 0; |
| cal.set(Calendar.HOUR, value); |
| return pos.getIndex(); |
| case 17: // 'z' - ZONE_OFFSET |
| case 23: // 'Z' - TIMEZONE_RFC |
| // 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. |
| { |
| int sign = 0; |
| int offset; |
| |
| // For time zones that have no known names, look for strings |
| // of the form: |
| // GMT[+-]hours:minutes or |
| // GMT[+-]hhmm or |
| // GMT. |
| if ((text.length() - start) >= GMT.length() && |
| text.regionMatches(true, start, GMT, 0, GMT.length())) |
| { |
| cal.set(Calendar.DST_OFFSET, 0); |
| |
| pos.setIndex(start + GMT.length()); |
| |
| try { // try-catch for "GMT" only time zone string |
| switch (text.charAt(pos.getIndex())) { |
| case '+': |
| sign = 1; |
| break; |
| case '-': |
| sign = -1; |
| break; |
| } |
| } catch(StringIndexOutOfBoundsException e) { |
| } |
| if (sign == 0) { |
| cal.set(Calendar.ZONE_OFFSET, 0 ); |
| return pos.getIndex(); |
| } |
| |
| // Look for hours:minutes or hhmm. |
| pos.setIndex(pos.getIndex() + 1); |
| int st = pos.getIndex(); |
| Number tzNumber = numberFormat.parse(text, pos); |
| if( tzNumber == null) { |
| return -start; |
| } |
| if( pos.getIndex() < text.length() && |
| text.charAt(pos.getIndex()) == ':' ) { |
| |
| // This is the hours:minutes case |
| offset = tzNumber.intValue() * 60; |
| pos.setIndex(pos.getIndex() + 1); |
| tzNumber = numberFormat.parse(text, pos); |
| if( tzNumber == null) { |
| return -start; |
| } |
| offset += tzNumber.intValue(); |
| } |
| else { |
| // This is the hhmm case. |
| offset = tzNumber.intValue(); |
| // Assume "-23".."+23" refers to hours. |
| if( offset < 24 && (pos.getIndex() - st) <= 2) |
| offset *= 60; |
| else |
| // todo: this looks questionable, should have more error checking |
| offset = offset % 100 + offset / 100 * 60; |
| } |
| |
| // Fall through for final processing below of 'offset' and 'sign'. |
| } |
| else { |
| // 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. |
| DecimalFormat fmt = new DecimalFormat("+####;-####"); |
| fmt.setParseIntegerOnly(true); |
| Number tzNumber = fmt.parse( text, pos ); |
| if( tzNumber == null) { |
| return -start; // Wasn't actually a number. |
| } |
| offset = tzNumber.intValue(); |
| sign = 1; |
| if( offset < 0 ) { |
| sign = -1; |
| offset = -offset; |
| } |
| // Assume "-23".."+23" refers to hours. Length includes sign. |
| if( offset < 24 && (pos.getIndex() - start) <= 3) |
| offset = offset * 60; |
| else |
| offset = offset % 100 + offset / 100 * 60; |
| |
| // Fall through for final processing below of 'offset' and 'sign'. |
| } |
| |
| // Do the final processing for both of the above cases. We only |
| // arrive here if the form GMT+/-... or an RFC 822 form was seen. |
| |
| // assert (sign != 0) : sign; // enable when guaranteed JDK >= 1.4 |
| offset *= millisPerMinute * sign; |
| |
| if (cal.getTimeZone().useDaylightTime()) |
| { |
| cal.set(Calendar.DST_OFFSET, millisPerHour); |
| offset -= millisPerHour; |
| } |
| cal.set(Calendar.ZONE_OFFSET, offset); |
| |
| return pos.getIndex(); |
| } |
| |
| default: |
| // case 3: // 'd' - DATE |
| // case 5: // 'H' - HOUR_OF_DAY (0..23) |
| // case 6: // 'm' - MINUTE |
| // case 7: // 's' - SECOND |
| // case 10: // 'D' - DAY_OF_YEAR |
| // case 11: // 'F' - DAY_OF_WEEK_IN_MONTH |
| // case 12: // 'w' - WEEK_OF_YEAR |
| // case 13: // 'W' - WEEK_OF_MONTH |
| // case 16: // 'K' - HOUR (0..11) |
| // case 18: // 'Y' - YEAR_WOY |
| // case 19: // 'e' - DOW_LOCAL |
| // case 20: // 'u' - EXTENDED_YEAR |
| // case 21: // 'g' - JULIAN_DAY |
| // case 22: // 'A' - MILLISECONDS_IN_DAY |
| |
| // Handle "generic" fields |
| if (obeyCount) |
| { |
| if ((start+count) > text.length()) return -start; |
| number = parseInt(text.substring(0, start+count), pos, allowNegative); |
| } |
| else number = parseInt(text, pos, allowNegative); |
| if (number != null) { |
| cal.set(field, number.intValue()); |
| return pos.getIndex(); |
| } |
| return -start; |
| } |
| } |
| |
| /** |
| * Parse an integer using fNumberFormat. This method is semantically |
| * const, but actually may modify fNumberFormat. |
| */ |
| private Number parseInt(String text, |
| ParsePosition pos, |
| boolean allowNegative) { |
| String oldPrefix = null; |
| DecimalFormat df = null; |
| if (!allowNegative) { |
| try { |
| df = (DecimalFormat)numberFormat; |
| oldPrefix = df.getNegativePrefix(); |
| df.setNegativePrefix(SUPPRESS_NEGATIVE_PREFIX); |
| } catch (ClassCastException e1) {} |
| } |
| Number number = numberFormat.parse(text, pos); |
| if (df != null) { |
| df.setNegativePrefix(oldPrefix); |
| } |
| return number; |
| } |
| |
| /** |
| * Translate a pattern, mapping each character in the from string to the |
| * corresponding character in the to string. |
| */ |
| private String translatePattern(String pattern, String from, String to) { |
| StringBuffer result = new StringBuffer(); |
| boolean inQuote = false; |
| for (int i = 0; i < pattern.length(); ++i) { |
| char c = pattern.charAt(i); |
| if (inQuote) { |
| if (c == '\'') |
| inQuote = false; |
| } |
| else { |
| if (c == '\'') |
| inQuote = true; |
| else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { |
| int ci = from.indexOf(c); |
| if (ci == -1) |
| throw new IllegalArgumentException("Illegal pattern " + |
| " character '" + |
| c + "'"); |
| c = to.charAt(ci); |
| } |
| } |
| result.append(c); |
| } |
| if (inQuote) |
| throw new IllegalArgumentException("Unfinished quote in pattern"); |
| return result.toString(); |
| } |
| |
| /** |
| * Return a pattern string describing this date format. |
| * @stable ICU 2.0 |
| */ |
| public String toPattern() { |
| return pattern; |
| } |
| |
| /** |
| * Return a localized pattern string describing this date format. |
| * @stable ICU 2.0 |
| */ |
| public String toLocalizedPattern() { |
| return translatePattern(pattern, |
| DateFormatSymbols.patternChars, |
| formatData.localPatternChars); |
| } |
| |
| /** |
| * Apply the given unlocalized pattern string to this date format. |
| * @stable ICU 2.0 |
| */ |
| public void applyPattern(String pattern) |
| { |
| this.pattern = pattern; |
| setLocale(null, null); |
| } |
| |
| /** |
| * Apply the given localized pattern string to this date format. |
| * @stable ICU 2.0 |
| */ |
| public void applyLocalizedPattern(String pattern) { |
| this.pattern = translatePattern(pattern, |
| formatData.localPatternChars, |
| DateFormatSymbols.patternChars); |
| setLocale(null, null); |
| } |
| |
| /** |
| * Gets the date/time formatting data. |
| * @return a copy of the date-time formatting data associated |
| * with this date-time formatter. |
| * @stable ICU 2.0 |
| */ |
| public DateFormatSymbols getDateFormatSymbols() |
| { |
| return (DateFormatSymbols)formatData.clone(); |
| } |
| |
| /** |
| * Allows you to set the date/time formatting data. |
| * @param newFormatSymbols the new symbols |
| * @stable ICU 2.0 |
| */ |
| public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) |
| { |
| this.formatData = (DateFormatSymbols)newFormatSymbols.clone(); |
| } |
| |
| /** |
| * Method for subclasses to access the DateFormatSymbols. |
| * @stable ICU 2.0 |
| */ |
| protected DateFormatSymbols getSymbols() { |
| return formatData; |
| } |
| |
| /** |
| * Overrides Cloneable |
| * @stable ICU 2.0 |
| */ |
| public Object clone() { |
| SimpleDateFormat other = (SimpleDateFormat) super.clone(); |
| other.formatData = (DateFormatSymbols) formatData.clone(); |
| return other; |
| } |
| |
| /** |
| * Override hashCode. |
| * Generates the hash code for the SimpleDateFormat object |
| * @stable ICU 2.0 |
| */ |
| public int hashCode() |
| { |
| return pattern.hashCode(); |
| // just enough fields for a reasonable distribution |
| } |
| |
| /** |
| * Override equals. |
| * @stable ICU 2.0 |
| */ |
| public boolean equals(Object obj) |
| { |
| if (!super.equals(obj)) return false; // super does class check |
| SimpleDateFormat that = (SimpleDateFormat) obj; |
| return (pattern.equals(that.pattern) |
| && formatData.equals(that.formatData)); |
| } |
| |
| /** |
| * Override readObject. |
| */ |
| private void readObject(ObjectInputStream stream) |
| throws IOException, ClassNotFoundException { |
| stream.defaultReadObject(); |
| ///CLOVER:OFF |
| // don't have old serial data to test with |
| if (serialVersionOnStream < 1) { |
| // didn't have defaultCenturyStart field |
| initializeDefaultCentury(); |
| } |
| ///CLOVER:ON |
| else { |
| // fill in dependent transient field |
| parseAmbiguousDatesAsAfter(defaultCenturyStart); |
| } |
| serialVersionOnStream = currentSerialVersion; |
| } |
| } |