All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.SimpleDateFormat Maven / Gradle / Ivy

Go to download

International Component for Unicode for Java (ICU4J) is a mature, widely used Java library providing Unicode and Globalization support

The newest version!
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */

package com.ibm.icu.text;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.UUID;

import com.ibm.icu.impl.DateNumberFormat;
import com.ibm.icu.impl.DayPeriodRules;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.JavaTimeConverters;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.TimeZoneFormat.Style;
import com.ibm.icu.text.TimeZoneFormat.TimeType;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.BasicTimeZone.LocalOption;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.HebrewCalendar;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import com.ibm.icu.util.UResourceBundle;



/**
 * {@icuenhanced java.text.SimpleDateFormat}.{@icu _usage_}
 *
 * 

SimpleDateFormat 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. * *

* Clients are encouraged to create a date-time formatter using * DateFormat.getDateInstance(), DateFormat.getDateInstance(), * or DateFormat.getDateTimeInstance() rather than * explicitly constructing an instance of SimpleDateFormat. This way, the client * is guaranteed to get an appropriate formatting pattern for whatever locale the * program is running in. If the client needs more control, they should consider using * DateFormat.getInstanceForSkeleton(). * However, if the client needs something more unusual than * the default patterns in the locales, he can construct a SimpleDateFormat directly * and give it an appropriate pattern (or use one of the factory methods on DateFormat * and modify the pattern after the fact with toPattern() and applyPattern(). * For more information on using these methods, see * {@link DateFormat}. * *

Date and Time Patterns:

* *

Date and time formats are specified by date and time pattern strings. * The full syntax for date and time patterns can be found at * https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns.

* *

Within date and time pattern strings, all unquoted ASCII letters [A-Za-z] are reserved * as pattern letters representing calendar fields. Some of the most commonly used pattern letters are:

*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Sym.No.ExampleDescription
G1..3ADEra - Replaced with the Era string for the current date. One to three letters for the * abbreviated form, four letters for the long (wide) form, five for the narrow form.
y1..n1996Year. Normally the length specifies the padding, but for two letters it also specifies the maximum * length. Example:
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Yearyyyyyyyyyyyyyyy
AD 1101001000100001
AD 121212012001200012
AD 12312323123012300123
AD 12341234341234123401234
AD 123451234545123451234512345
*
*
Q1..202Quarter - Use one or two for the numerical quarter, three for the abbreviation, or four * for the full (wide) name (five for the narrow name is not yet supported).
3Q2
42nd quarter
M1..209Month - Use one or two for the numerical month, three for the abbreviation, four for * the full (wide) name, or five for the narrow name. With two ("MM"), the month number is zero-padded * if necessary (e.g. "08").
3Sep
4September
5S
d1..21Date - Day of the month. Use "d" to show the minimum number of digits, or "dd" to always show * two digits (zero-padding if necessary, e.g. "08").
E1..3TueDay of week - Use one through three letters for the short day, four for the full (wide) name, * five for the narrow name, or six for the short name.
4Tuesday
5T
6Tu
a1AMAM or PM
h1..211Hour [1-12]. When used in skeleton data or in a skeleton passed in an API for flexible data pattern * generation, it should match the 12-hour-cycle format preferred by the locale (h or K); it should not match * a 24-hour-cycle format (H or k). Use hh for zero padding.
H1..213Hour [0-23]. When used in skeleton data or in a skeleton passed in an API for flexible data pattern * generation, it should match the 24-hour-cycle format preferred by the locale (H or k); it should not match a * 12-hour-cycle format (h or K). Use HH for zero padding.
m1..259Minute. Use "m" to show the minimum number of digits, or "mm" to always show two digits * (zero-padding if necessary, e.g. "08")..
s1..212Second. Use "s" to show the minimum number of digits, or "ss" to always show two digits * (zero-padding if necessary, e.g. "08").
z1..3PDTTime zone. The short specific non-location format. * Where that is unavailable, falls back to the short localized GMT format ("O").
4Pacific Daylight TimeThe long specific non-location format. * Where that is unavailable, falls back to the long localized GMT format ("OOOO").
v1PTTime zone. The short generic non-location format. * Where that is unavailable, falls back to the generic location format ("VVVV"), * then the short localized GMT format as the final fallback.
4Pacific TimeThe long generic non-location format. * Where that is unavailable, falls back to generic location format ("VVVV"). *
* *
*

* 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. *

* A pattern containing any invalid pattern letter will result in a thrown * exception during formatting or parsing. * *

* Examples Using the US Locale: *

*
 * Format Pattern                         Result
 * --------------                         -------
 * "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->>  1996.07.10 AD at 15:08:56 Pacific Time
 * "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, vvv"                     ->>  0:00 PM, PT
 * "yyyyy.MMMMM.dd GGG hh:mm aaa"    ->>  01996.July.10 AD 12:08 PM
 * 
*
* Code Sample: *
*
 * 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);
 * 
* // 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); *
* // Parse the previous string back into a Date. * ParsePosition pos = new ParsePosition(0); * Date currentTime_2 = formatter.parse(dateString, pos); *
*
* In the example, the time value currentTime_2 obtained from * parsing will be equal to currentTime_1. 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. * *

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 com.ibm.icu.lang.UCharacter#isDigit(int)}, 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. * *

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. * *

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. * *

For time zones that have no names, use strings GMT+hours:minutes or * GMT-hours:minutes. * *

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. * *

Synchronization

* * 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 * @see TimeZoneFormat * @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 private 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 // - 2 we write additional int for capitalizationSetting static final int currentSerialVersion = 2; static boolean DelayedHebrewMonthCheck = false; /* * From calendar field to its level. * Used to order calendar field. * For example, calendar fields can be defined in the following order: * year > month > date > am-pm > hour > minute * YEAR --> 10, MONTH -->20, DATE --> 30; * AM_PM -->40, HOUR --> 50, MINUTE -->60 */ private static final int[] CALENDAR_FIELD_TO_LEVEL = { /*GyM*/ 0, 10, 20, /*wW*/ 20, 30, /*dDEF*/ 30, 20, 30, 30, /*ahHm*/ 40, 50, 50, 60, /*sS*/ 70, 80, /*z?Y*/ 0, 0, 10, /*eug*/ 30, 10, 0, /*A?*/ 40, 0, 0 }; /* * From calendar field letter to its level. * Used to order calendar field. * For example, calendar fields can be defined in the following order: * year > month > date > am-pm > hour > minute * 'y' --> 10, 'M' -->20, 'd' --> 30; 'a' -->40, 'h' --> 50, 'm' -->60 */ private static final int[] PATTERN_CHAR_TO_LEVEL = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ! " # $ % & ' ( ) * + , - . / -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 1 2 3 4 5 6 7 8 9 : ; < = > ? -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // @ A B C D E F G H I J K L M N O -1, 40, -1, -1, 20, 30, 30, 0, 50, -1, -1, 50, 20, 20, -1, 0, // P Q R S T U V W X Y Z [ \ ] ^ _ -1, 20, -1, 80, -1, 10, 0, 30, 0, 10, 0, -1, -1, -1, -1, -1, // ` a b c d e f g h i j k l m n o -1, 40, -1, 30, 30, 30, -1, 0, 50, -1, -1, 50, -1, 60, -1, -1, // p q r s t u v w x y z { | } ~ -1, 20, 10, 70, -1, 10, 0, 20, 0, 10, 0, -1, -1, -1, -1, -1, }; /** * Map calendar field letter into calendar field level. */ private static int getLevelFromChar(char ch) { return ch < PATTERN_CHAR_TO_LEVEL.length ? PATTERN_CHAR_TO_LEVEL[ch & 0xff] : -1; } private static final boolean[] PATTERN_CHAR_IS_SYNTAX = { // false, false, false, false, false, false, false, false, // false, false, false, false, false, false, false, false, // false, false, false, false, false, false, false, false, // false, false, false, false, false, false, false, false, // ! " # $ % & ' false, false, false, false, false, false, false, false, // ( ) * + , - . / false, false, false, false, false, false, false, false, // 0 1 2 3 4 5 6 7 false, false, false, false, false, false, false, false, // 8 9 : ; < = > ? false, false, false, false, false, false, false, false, // @ A B C D E F G false, true, true, true, true, true, true, true, // H I J K L M N O true, true, true, true, true, true, true, true, // P Q R S T U V W true, true, true, true, true, true, true, true, // X Y Z [ \ ] ^ _ true, true, true, false, false, false, false, false, // ` a b c d e f g false, true, true, true, true, true, true, true, // h i j k l m n o true, true, true, true, true, true, true, true, // p q r s t u v w true, true, true, true, true, true, true, true, // x y z { | } ~ true, true, true, false, false, false, false, false, }; /** * Tell if a character can be used to define a field in a format string. */ private static boolean isSyntaxChar(char ch) { return ch < PATTERN_CHAR_IS_SYNTAX.length ? PATTERN_CHAR_IS_SYNTAX[ch & 0xff] : false; } // When calendar uses hebr numbering (i.e. he@calendar=hebrew), // offset the years within the current millenium down to 1-999 private static final int HEBREW_CAL_CUR_MILLENIUM_START_YEAR = 5000; private static final int HEBREW_CAL_CUR_MILLENIUM_END_YEAR = 6000; /** * The version of the serialized data on the stream. Possible values: *
    *
  • 0 or not present on stream: JDK 1.1.3. This version * has no defaultCenturyStart on stream. *
  • 1 JDK 1.1.4 or later. This version adds * defaultCenturyStart. *
  • 2 This version writes an additional int for * capitalizationSetting. *
* When streaming out this class, the most recent format * and the highest allowable serialVersionOnStream * 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 override string of this formatter. Used to override the * numbering system for one or more fields. * @serial */ private String override; /** * The hash map used for number format overrides. * @serial */ private HashMap numberFormatters; /** * The hash map used for number format overrides. * @serial */ private HashMap overrideMap; /** * The symbols used by this formatter for week names, month names, * etc. May not be null. * @serial * @see DateFormatSymbols */ private DateFormatSymbols formatData; private transient ULocale locale; /** * We map dates with two-digit years into the century starting at * defaultCenturyStart, which may be any date. May * not be null. * @serial * @since JDK1.1.4 */ private Date defaultCenturyStart; private transient int defaultCenturyStartYear; // defaultCenturyBase is set when an instance is created // and may be used for calculating defaultCenturyStart when needed. private transient long defaultCenturyBase; private static final int millisPerHour = 60 * 60 * 1000; // When possessing ISO format, the ERA may be ommitted is the // year specifier is a negative number. private static final int ISOSpecialEra = -32000; // 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"; /** * If true, this object supports fast formatting using the * subFormat variant that takes a StringBuffer. */ private transient boolean useFastFormat; /* * The time zone sub-formatter, introduced in ICU 4.8 */ private volatile TimeZoneFormat tzFormat; /** * BreakIterator to use for capitalization (will be cloned for actual use) */ private transient BreakIterator capitalizationBrkIter = null; /** * DateFormat pattern contains the minute field. */ private transient boolean hasMinute; /** * DateFormat pattern contains the second field. */ private transient boolean hasSecond; /** * DateFormat pattern contains the Han year character \u5E74=年, => non-numeric E Asian format. */ private transient boolean hasHanYearChar; /* * Capitalization setting, introduced in ICU 50 * Special serialization, see writeObject & readObject below * * Hoisted to DateFormat in ICU 53, get value with * getContext(DisplayContext.Type.CAPITALIZATION) */ // private transient DisplayContext capitalizationSetting; /* * Old defaultCapitalizationContext field * from ICU 49.1: */ //private ContextValue defaultCapitalizationContext; /** * Old ContextValue enum, preserved only to avoid * deserialization errs from ICU 49.1. */ @SuppressWarnings("unused") private enum ContextValue { UNKNOWN, CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE, CAPITALIZATION_FOR_UI_LIST_OR_MENU, CAPITALIZATION_FOR_STANDALONE } /** * Constructs a SimpleDateFormat using the default pattern for the default FORMAT * locale. Note: Not all locales support SimpleDateFormat; for full * generality, use the factory methods in the DateFormat class. * * @see DateFormat * @see Category#FORMAT * @stable ICU 2.0 */ public SimpleDateFormat() { this(getDefaultPattern(), null, null, null, null, true, null); } /** * Constructs a SimpleDateFormat using the given pattern in the default FORMAT * locale. Note: Not all locales support SimpleDateFormat; for full * generality, use the factory methods in the DateFormat class. * @see Category#FORMAT * @stable ICU 2.0 */ public SimpleDateFormat(String pattern) { this(pattern, null, null, null, null, true, null); } /** * Constructs a SimpleDateFormat using the given pattern and locale. * Note: 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, null, null, null, ULocale.forLocale(loc), true, null); } /** * Constructs a SimpleDateFormat using the given pattern and locale. * Note: Not all locales support SimpleDateFormat; for full * generality, use the factory methods in the DateFormat class. * @stable ICU 3.2 */ public SimpleDateFormat(String pattern, ULocale loc) { this(pattern, null, null, null, loc, true, null); } /** * Constructs a SimpleDateFormat using the given pattern , override and locale. * @param pattern The pattern to be used * @param override The override string. A numbering system override string can take one of the following forms: * 1). If just a numbering system name is specified, it applies to all numeric fields in the date format pattern. * 2). To specify an alternate numbering system on a field by field basis, use the field letters from the pattern * followed by an = sign, followed by the numbering system name. For example, to specify that just the year * be formatted using Hebrew digits, use the override "y=hebr". Multiple overrides can be specified in a single * string by separating them with a semi-colon. For example, the override string "m=thai;y=deva" would format using * Thai digits for the month and Devanagari digits for the year. * @param loc The locale to be used * @stable ICU 4.2 */ public SimpleDateFormat(String pattern, String override, ULocale loc) { this(pattern, null, null, null, loc, false,override); } /** * Constructs a SimpleDateFormat using the given pattern and * locale-specific symbol data. * Warning: uses default FORMAT locale for digits! * @stable ICU 2.0 */ public SimpleDateFormat(String pattern, DateFormatSymbols formatData) { this(pattern, (DateFormatSymbols)formatData.clone(), null, null, null, true, null); } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public SimpleDateFormat(String pattern, DateFormatSymbols formatData, ULocale loc) { this(pattern, (DateFormatSymbols)formatData.clone(), null, null, loc, true,null); } /** * Package-private constructor that allows a subclass to specify * whether it supports fast formatting. * * TODO make this API public. */ SimpleDateFormat(String pattern, DateFormatSymbols formatData, Calendar calendar, ULocale locale, boolean useFastFormat, String override) { this(pattern, (DateFormatSymbols)formatData.clone(), (Calendar)calendar.clone(), null, locale, useFastFormat,override); } /* * The constructor called from all other SimpleDateFormat constructors */ private SimpleDateFormat(String pattern, DateFormatSymbols formatData, Calendar calendar, NumberFormat numberFormat, ULocale locale, boolean useFastFormat,String override) { this.pattern = pattern; this.formatData = formatData; this.calendar = calendar; this.numberFormat = numberFormat; this.locale = locale; // time zone formatting this.useFastFormat = useFastFormat; this.override = override; initialize(); } /** * Creates an instance of SimpleDateFormat for the given format configuration * @param formatConfig the format configuration * @return A SimpleDateFormat instance * @internal * @deprecated This API is ICU internal only. */ @Deprecated public static SimpleDateFormat getInstance(Calendar.FormatConfiguration formatConfig) { String ostr = formatConfig.getOverrideString(); boolean useFast = ( ostr != null && ostr.length() > 0 ); return new SimpleDateFormat(formatConfig.getPatternString(), formatConfig.getDateFormatSymbols(), formatConfig.getCalendar(), null, formatConfig.getLocale(), useFast, formatConfig.getOverrideString()); } /* * Initialized fields */ private void initialize() { if (locale == null) { locale = ULocale.getDefault(Category.FORMAT); } if (formatData == null) { formatData = new DateFormatSymbols(locale); } if (calendar == null) { calendar = Calendar.getInstance(locale); } if (numberFormat == null) { NumberingSystem ns = NumberingSystem.getInstance(locale); String digitString = ns.getDescription(); // DateNumberFormat does not support non-BMP digits at this moment. if (ns.isAlgorithmic() || digitString.length() != 10) { numberFormat = NumberFormat.getInstance(locale); } else { String nsName = ns.getName(); // Use a NumberFormat optimized for date formatting numberFormat = new DateNumberFormat(locale, digitString, nsName); } } if (numberFormat instanceof DecimalFormat) { fixNumberFormatForDates(numberFormat); } // Note: deferring calendar calculation until when we really need it. // Instead, we just record time of construction for backward compatibility. defaultCenturyBase = System.currentTimeMillis(); setLocale(calendar.getLocale(ULocale.VALID_LOCALE ), calendar.getLocale(ULocale.ACTUAL_LOCALE)); initLocalZeroPaddingNumberFormat(); parsePattern(); // Need this before initNumberFormatters(), to set hasHanYearChar // Simple-minded hack to force Gannen year numbering for ja@calendar=japanese // if format is non-numeric (includes 年) and overrides are not already specified. // Now this does get updated if applyPattern subsequently changes the pattern type. if (override == null && hasHanYearChar && calendar != null && calendar.getType().equals("japanese") && locale != null && locale.getLanguage().equals("ja")) { override = "y=jpanyear"; } if (override != null) { initNumberFormatters(locale); } } /** * Private method lazily instantiate the TimeZoneFormat field * @param bForceUpdate when true, check if tzFormat is synchronized with * the current numberFormat and update its digits if necessary. When false, * this check is skipped. */ private synchronized void initializeTimeZoneFormat(boolean bForceUpdate) { if (bForceUpdate || tzFormat == null) { tzFormat = TimeZoneFormat.getInstance(locale); String digits = null; if (numberFormat instanceof DecimalFormat) { DecimalFormatSymbols decsym = ((DecimalFormat) numberFormat).getDecimalFormatSymbols(); String[] strDigits = decsym.getDigitStringsLocal(); // Note: TimeZoneFormat#setGMTOffsetDigits() does not support string array, // so we need to concatenate digits to make a single string. StringBuilder digitsBuf = new StringBuilder(); for (String digit : strDigits) { digitsBuf.append(digit); } digits = digitsBuf.toString(); } else if (numberFormat instanceof DateNumberFormat) { digits = new String(((DateNumberFormat)numberFormat).getDigits()); } if (digits != null) { if (!tzFormat.getGMTOffsetDigits().equals(digits)) { if (tzFormat.isFrozen()) { tzFormat = tzFormat.cloneAsThawed(); } tzFormat.setGMTOffsetDigits(digits); } } } } /** * Private method, returns non-null TimeZoneFormat. * @return the TimeZoneFormat used by this formatter. */ private TimeZoneFormat tzFormat() { if (tzFormat == null) { initializeTimeZoneFormat(false); } return tzFormat; } // privates for the default pattern private static ULocale cachedDefaultLocale = null; private static String cachedDefaultPattern = null; private static final String FALLBACKPATTERN = "yy/MM/dd HH:mm"; /* * Returns the default date and time pattern (SHORT) for the default locale. * This method is only used by the default SimpleDateFormat constructor. */ private static synchronized String getDefaultPattern() { ULocale defaultLocale = ULocale.getDefault(Category.FORMAT); if (!defaultLocale.equals(cachedDefaultLocale)) { cachedDefaultLocale = defaultLocale; Calendar cal = Calendar.getInstance(cachedDefaultLocale); try { // Load the calendar data directly. ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance( ICUData.ICU_BASE_NAME, cachedDefaultLocale); String resourcePath = "calendar/" + cal.getType() + "/DateTimePatterns"; ICUResourceBundle patternsRb= rb.findWithFallback(resourcePath); if (patternsRb == null) { patternsRb = rb.findWithFallback("calendar/gregorian/DateTimePatterns"); } if (patternsRb == null || patternsRb.getSize() < 9) { cachedDefaultPattern = FALLBACKPATTERN; } else { String basePattern = Calendar.getDateAtTimePattern(cal, cachedDefaultLocale, SHORT); cachedDefaultPattern = SimpleFormatterImpl.formatRawPattern( basePattern, 2, 2, patternsRb.getString(SHORT), patternsRb.getString(SHORT + 4)); } } catch (MissingResourceException e) { cachedDefaultPattern = FALLBACKPATTERN; } } return cachedDefaultPattern; } /* 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); } /* Initialize defaultCenturyStart and defaultCenturyStartYear by base time. * The default start time is 80 years before the creation time of this object. */ private void initializeDefaultCenturyStart(long baseTime) { defaultCenturyBase = baseTime; // clone to avoid messing up date stored in calendar object // when this method is called while parsing Calendar tmpCal = (Calendar)calendar.clone(); tmpCal.setTimeInMillis(baseTime); tmpCal.add(Calendar.YEAR, -80); defaultCenturyStart = tmpCal.getTime(); defaultCenturyStartYear = tmpCal.get(Calendar.YEAR); } /* Gets the default century start date for this object */ private Date getDefaultCenturyStart() { if (defaultCenturyStart == null) { // not yet initialized initializeDefaultCenturyStart(defaultCenturyBase); } return defaultCenturyStart; } /* Gets the default century start year for this object */ private int getDefaultCenturyStartYear() { if (defaultCenturyStart == null) { // not yet initialized initializeDefaultCenturyStart(defaultCenturyBase); } return defaultCenturyStartYear; } /** * 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 * startDate to startDate + 100 years. * @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 getDefaultCenturyStart(); } /** * {@icu} Set a particular DisplayContext value in the formatter, * such as CAPITALIZATION_FOR_STANDALONE. Note: For getContext, see * DateFormat. * * @param context The DisplayContext value to set. * @stable ICU 53 */ // Here we override the DateFormat implementation in order to lazily initialize relevant items @Override public void setContext(DisplayContext context) { super.setContext(context); if (capitalizationBrkIter == null && (context==DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE || context==DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU || context==DisplayContext.CAPITALIZATION_FOR_STANDALONE)) { capitalizationBrkIter = BreakIterator.getSentenceInstance(locale); } } /** * Formats a date or time, which is the standard millis * since January 1, 1970, 00:00:00 GMT. *

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 */ @Override public StringBuffer format(Calendar cal, StringBuffer toAppendTo, FieldPosition pos) { return format(cal, toAppendTo, pos, null); } /** Internal formatting method that accepts an attributes list. */ StringBuffer format(Calendar cal, StringBuffer toAppendTo, FieldPosition pos, List attributes) { TimeZone backupTZ = null; if (cal != calendar && !cal.getType().equals(calendar.getType())) { // Different calendar type // We use the time and time zone from the input calendar, but // do not use the input calendar for field calculation. calendar.setTimeInMillis(cal.getTimeInMillis()); backupTZ = calendar.getTimeZone(); calendar.setTimeZone(cal.getTimeZone()); cal = calendar; } StringBuffer result = format(cal, getContext(DisplayContext.Type.CAPITALIZATION), toAppendTo, pos, attributes); if (backupTZ != null) { // Restore the original time zone calendar.setTimeZone(backupTZ); } return result; } // The actual method to format date. If List attributes is not null, // then attribute information will be recorded. private StringBuffer format(Calendar cal, DisplayContext capitalizationContext, StringBuffer toAppendTo, FieldPosition pos, List attributes) { // Initialize pos.setBeginIndex(0); pos.setEndIndex(0); // Careful: For best performance, minimize the number of calls // to StringBuffer.append() by consolidating appends when // possible. Object[] items = getPatternItems(); for (int i = 0; i < items.length; i++) { if (items[i] instanceof String) { toAppendTo.append((String)items[i]); } else { PatternItem item = (PatternItem)items[i]; int start = 0; if (attributes != null) { // Save the current length start = toAppendTo.length(); } if (useFastFormat) { subFormat(toAppendTo, item.type, item.length, toAppendTo.length(), i, capitalizationContext, pos, item.type, cal); } else { toAppendTo.append(subFormat(item.type, item.length, toAppendTo.length(), i, capitalizationContext, pos, item.type, cal)); } if (attributes != null) { // Check the sub format length int end = toAppendTo.length(); if (end - start > 0) { // Append the attribute to the list DateFormat.Field attr = patternCharToDateFormatField(item.type); FieldPosition fp = new FieldPosition(attr); fp.setBeginIndex(start); fp.setEndIndex(end); attributes.add(fp); } } } } return toAppendTo; } // Map pattern character to index private static final int[] PATTERN_CHAR_TO_INDEX = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ! " # $ % & ' ( ) * + , - . / -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 1 2 3 4 5 6 7 8 9 : ; < = > ? -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // @ A B C D E F G H I J K L M N O -1, 22, 36, -1, 10, 9, 11, 0, 5, -1, -1, 16, 26, 2, -1, 31, // P Q R S T U V W X Y Z [ \ ] ^ _ -1, 27, -1, 8, -1, 30, 29, 13, 32, 18, 23, -1, -1, -1, -1, -1, // ` a b c d e f g h i j k l m n o -1, 14, 35, 25, 3, 19, -1, 21, 15, -1, -1, 4, -1, 6, -1, -1, // p q r s t u v w x y z { | } ~ -1, 28, 34, 7, -1, 20, 24, 12, 33, 1, 17, -1, -1, -1, -1, -1, }; private static int getIndexFromChar(char ch) { return ch < PATTERN_CHAR_TO_INDEX.length ? PATTERN_CHAR_TO_INDEX[ch & 0xff] : -1; } // Map pattern character index 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 /* also DST_OFFSET */, /*v*/ Calendar.ZONE_OFFSET /* also DST_OFFSET */, /*c*/ Calendar.DOW_LOCAL, /*L*/ Calendar.MONTH, /*Qq*/ Calendar.MONTH, Calendar.MONTH, /*V*/ Calendar.ZONE_OFFSET /* also DST_OFFSET */, /*U*/ Calendar.YEAR, /*O*/ Calendar.ZONE_OFFSET /* also DST_OFFSET */, /*Xx*/ Calendar.ZONE_OFFSET /* also DST_OFFSET */, Calendar.ZONE_OFFSET /* also DST_OFFSET */, /*r*/ Calendar.EXTENDED_YEAR /* not an exact match */, /*bB*/ -1, -1 /* am/pm/midnight/noon and flexible day period fields; no mapping to calendar fields */ /*:*/ -1, /* => no useful mapping to any calendar field, can't use protected Calendar.BASE_FIELD_COUNT */ }; // Map pattern character index 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, /*v*/ DateFormat.TIMEZONE_GENERIC_FIELD, /*c*/ DateFormat.STANDALONE_DAY_FIELD, /*L*/ DateFormat.STANDALONE_MONTH_FIELD, /*Qq*/ DateFormat.QUARTER_FIELD, DateFormat.STANDALONE_QUARTER_FIELD, /*V*/ DateFormat.TIMEZONE_SPECIAL_FIELD, /*U*/ DateFormat.YEAR_NAME_FIELD, /*O*/ DateFormat.TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD, /*Xx*/ DateFormat.TIMEZONE_ISO_FIELD, DateFormat.TIMEZONE_ISO_LOCAL_FIELD, /*r*/ DateFormat.RELATED_YEAR, /*bB*/ DateFormat.AM_PM_MIDNIGHT_NOON_FIELD, DateFormat.FLEXIBLE_DAY_PERIOD_FIELD, /*(no pattern character defined for this)*/ DateFormat.TIME_SEPARATOR, }; // Map pattern character index to DateFormat.Field private static final DateFormat.Field[] PATTERN_INDEX_TO_DATE_FORMAT_ATTRIBUTE = { /*GyM*/ DateFormat.Field.ERA, DateFormat.Field.YEAR, DateFormat.Field.MONTH, /*dkH*/ DateFormat.Field.DAY_OF_MONTH, DateFormat.Field.HOUR_OF_DAY1, DateFormat.Field.HOUR_OF_DAY0, /*msS*/ DateFormat.Field.MINUTE, DateFormat.Field.SECOND, DateFormat.Field.MILLISECOND, /*EDF*/ DateFormat.Field.DAY_OF_WEEK, DateFormat.Field.DAY_OF_YEAR, DateFormat.Field.DAY_OF_WEEK_IN_MONTH, /*wWa*/ DateFormat.Field.WEEK_OF_YEAR, DateFormat.Field.WEEK_OF_MONTH, DateFormat.Field.AM_PM, /*hKz*/ DateFormat.Field.HOUR1, DateFormat.Field.HOUR0, DateFormat.Field.TIME_ZONE, /*Yeu*/ DateFormat.Field.YEAR_WOY, DateFormat.Field.DOW_LOCAL, DateFormat.Field.EXTENDED_YEAR, /*gAZ*/ DateFormat.Field.JULIAN_DAY, DateFormat.Field.MILLISECONDS_IN_DAY, DateFormat.Field.TIME_ZONE, /*v*/ DateFormat.Field.TIME_ZONE, /*c*/ DateFormat.Field.DAY_OF_WEEK, /*L*/ DateFormat.Field.MONTH, /*Qq*/ DateFormat.Field.QUARTER, DateFormat.Field.QUARTER, /*V*/ DateFormat.Field.TIME_ZONE, /*U*/ DateFormat.Field.YEAR, /*O*/ DateFormat.Field.TIME_ZONE, /*Xx*/ DateFormat.Field.TIME_ZONE, DateFormat.Field.TIME_ZONE, /*r*/ DateFormat.Field.RELATED_YEAR, /*bB*/ DateFormat.Field.AM_PM_MIDNIGHT_NOON, DateFormat.Field.FLEXIBLE_DAY_PERIOD, /*(no pattern character defined for this)*/ DateFormat.Field.TIME_SEPARATOR, }; /** * Returns a DateFormat.Field constant associated with the specified format pattern * character. * * @param ch The pattern character * @return DateFormat.Field associated with the pattern character * * @stable ICU 3.8 */ protected DateFormat.Field patternCharToDateFormatField(char ch) { int patternCharIndex = getIndexFromChar(ch); if (patternCharIndex != -1) { return PATTERN_INDEX_TO_DATE_FORMAT_ATTRIBUTE[patternCharIndex]; } return null; } /** * Formats 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 fmtData the symbols for this formatter * @stable ICU 2.0 */ protected String subFormat(char ch, int count, int beginOffset, FieldPosition pos, DateFormatSymbols fmtData, Calendar cal) throws IllegalArgumentException { // Note: formatData is ignored return subFormat(ch, count, beginOffset, 0, DisplayContext.CAPITALIZATION_NONE, pos, ch, cal); } /** * Formats a single field. This is the version called internally; it * adds fieldNum and capitalizationContext parameters. * * @internal * @deprecated This API is ICU internal only. */ @Deprecated protected String subFormat(char ch, int count, int beginOffset, int fieldNum, DisplayContext capitalizationContext, FieldPosition pos, char patternCharToOutput, Calendar cal) { StringBuffer buf = new StringBuffer(); subFormat(buf, ch, count, beginOffset, fieldNum, capitalizationContext, pos, patternCharToOutput, cal); return buf.toString(); } /** * Formats a single field; useFastFormat variant. Reuses a * StringBuffer for results instead of creating a String on the * heap for each call. * * NOTE We don't really need the beginOffset parameter, EXCEPT for * the need to support the slow subFormat variant (above) which * has to pass it in to us. * * @internal * @deprecated This API is ICU internal only. */ @Deprecated @SuppressWarnings("fallthrough") protected void subFormat(StringBuffer buf, char ch, int count, int beginOffset, int fieldNum, DisplayContext capitalizationContext, FieldPosition pos, char patternCharToOutput, Calendar cal) { final int maxIntCount = Integer.MAX_VALUE; final int bufstart = buf.length(); TimeZone tz = cal.getTimeZone(); long date = cal.getTimeInMillis(); String result = null; int patternCharIndex = getIndexFromChar(ch); if (patternCharIndex == -1) { if (ch == 'l') { // (SMALL LETTER L) deprecated placeholder for leap month marker, ignore return; } else { throw new IllegalArgumentException("Illegal pattern character " + "'" + ch + "' in \"" + pattern + '"'); } } final int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; int value = 0; // Don't get value unless it is useful if (field >= 0) { value = (patternCharIndex != DateFormat.RELATED_YEAR)? cal.get(field): cal.getRelatedYear(); } NumberFormat currentNumberFormat = getNumberFormat(ch); DateFormatSymbols.CapitalizationContextUsage capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.OTHER; switch (patternCharIndex) { case 0: // 'G' - ERA if ( cal.getType().equals("chinese") || cal.getType().equals("dangi") ) { // moved from ChineseDateFormat zeroPaddingNumber(currentNumberFormat, buf, value, 1, 9); } else { if (count == 5) { safeAppend(formatData.narrowEras, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.ERA_NARROW; } else if (count == 4) { safeAppend(formatData.eraNames, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.ERA_WIDE; } else { safeAppend(formatData.eras, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.ERA_ABBREV; } } break; case 30: // 'U' - YEAR_NAME_FIELD if (formatData.shortYearNames != null && value <= formatData.shortYearNames.length) { safeAppend(formatData.shortYearNames, value-1, buf); break; } // else fall through to numeric year handling, do not break here case 1: // 'y' - YEAR case 18: // 'Y' - YEAR_WOY if ( override != null && (override.compareTo("hebr") == 0 || override.indexOf("y=hebr") >= 0) && value > HEBREW_CAL_CUR_MILLENIUM_START_YEAR && value < HEBREW_CAL_CUR_MILLENIUM_END_YEAR ) { value -= HEBREW_CAL_CUR_MILLENIUM_START_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) { zeroPaddingNumber(currentNumberFormat,buf, value, 2, 2); // clip 1996 to 96 } else { //count = 1 or count > 2 zeroPaddingNumber(currentNumberFormat,buf, value, count, maxIntCount); } break; case 2: // 'M' - MONTH case 26: // 'L' - STANDALONE MONTH if ( cal.getType().equals("hebrew")) { boolean isLeap = HebrewCalendar.isLeapYear(cal.get(Calendar.YEAR)); if (isLeap && value == 6 && count >= 3 ) { value = 13; // Show alternate form for Adar II in leap years in Hebrew calendar. } if (!isLeap && value >= 6 && count < 3 ) { value--; // Adjust the month number down 1 in Hebrew non-leap years, i.e. Adar is 6, not 7. } } int isLeapMonth = (formatData.leapMonthPatterns != null && formatData.leapMonthPatterns.length >= DateFormatSymbols.DT_MONTH_PATTERN_COUNT)? cal.get(Calendar.IS_LEAP_MONTH): 0; // should consolidate the next section by using arrays of pointers & counts for the right symbols... if (count == 5) { if (patternCharIndex == 2) { safeAppendWithMonthPattern(formatData.narrowMonths, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_NARROW]: null); } else { safeAppendWithMonthPattern(formatData.standaloneNarrowMonths, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_NARROW]: null); } capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.MONTH_NARROW; } else if (count == 4) { if (patternCharIndex == 2) { safeAppendWithMonthPattern(formatData.months, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_WIDE]: null); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.MONTH_FORMAT; } else { safeAppendWithMonthPattern(formatData.standaloneMonths, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_WIDE]: null); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.MONTH_STANDALONE; } } else if (count == 3) { if (patternCharIndex == 2) { safeAppendWithMonthPattern(formatData.shortMonths, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_ABBREV]: null); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.MONTH_FORMAT; } else { safeAppendWithMonthPattern(formatData.standaloneShortMonths, value, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_ABBREV]: null); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.MONTH_STANDALONE; } } else { StringBuffer monthNumber = new StringBuffer(); zeroPaddingNumber(currentNumberFormat, monthNumber, value+1, count, maxIntCount); String[] monthNumberStrings = new String[1]; monthNumberStrings[0] = monthNumber.toString(); safeAppendWithMonthPattern(monthNumberStrings, 0, buf, (isLeapMonth!=0)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_NUMERIC]: null); } break; case 4: // 'k' - HOUR_OF_DAY (1..24) if (value == 0) { zeroPaddingNumber(currentNumberFormat,buf, cal.getMaximum(Calendar.HOUR_OF_DAY)+1, count, maxIntCount); } else { zeroPaddingNumber(currentNumberFormat,buf, value, count, maxIntCount); } break; case 8: // 'S' - FRACTIONAL_SECOND // Fractional seconds left-justify { numberFormat.setMinimumIntegerDigits(Math.min(3, count)); numberFormat.setMaximumIntegerDigits(maxIntCount); if (count == 1) { value /= 100; } else if (count == 2) { value /= 10; } FieldPosition p = new FieldPosition(-1); numberFormat.format(value, buf, p); if (count > 3) { numberFormat.setMinimumIntegerDigits(count - 3); numberFormat.format(0L, buf, p); } } break; case 19: // 'e' - DOW_LOCAL (use DOW_LOCAL for numeric, DAY_OF_WEEK for format names) if (count < 3) { zeroPaddingNumber(currentNumberFormat,buf, value, count, maxIntCount); break; } // For alpha day-of-week, we don't want DOW_LOCAL, // we need the standard DAY_OF_WEEK. value = cal.get(Calendar.DAY_OF_WEEK); // fall through, do not break here case 9: // 'E' - DAY_OF_WEEK if (count == 5) { safeAppend(formatData.narrowWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_NARROW; } else if (count == 4) { safeAppend(formatData.weekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_FORMAT; } else if (count == 6 && formatData.shorterWeekdays != null) { safeAppend(formatData.shorterWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_FORMAT; } else {// count <= 3, use abbreviated form if exists safeAppend(formatData.shortWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_FORMAT; } break; case 14: // 'a' - AM_PM // formatData.ampmsNarrow may be null when deserializing DateFormatSymbolsfrom old version if (count < 5 || formatData.ampmsNarrow == null) { safeAppend(formatData.ampms, value, buf); } else { safeAppend(formatData.ampmsNarrow, value, buf); } break; case 15: // 'h' - HOUR (1..12) if (value == 0) { zeroPaddingNumber(currentNumberFormat,buf, cal.getLeastMaximum(Calendar.HOUR)+1, count, maxIntCount); } else { zeroPaddingNumber(currentNumberFormat,buf, value, count, maxIntCount); } break; case 17: // 'z' - TIMEZONE_FIELD if (count < 4) { // "z", "zz", "zzz" result = tzFormat().format(Style.SPECIFIC_SHORT, tz, date); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.METAZONE_SHORT; } else { result = tzFormat().format(Style.SPECIFIC_LONG, tz, date); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.METAZONE_LONG; } buf.append(result); break; case 23: // 'Z' - TIMEZONE_RFC_FIELD if (count < 4) { // RFC822 format - equivalent to ISO 8601 local offset fixed width format result = tzFormat().format(Style.ISO_BASIC_LOCAL_FULL, tz, date); } else if (count == 5) { // ISO 8601 extended format result = tzFormat().format(Style.ISO_EXTENDED_FULL, tz, date); } else { // long form, localized GMT pattern result = tzFormat().format(Style.LOCALIZED_GMT, tz, date); } buf.append(result); break; case 24: // 'v' - TIMEZONE_GENERIC_FIELD if (count == 1) { // "v" result = tzFormat().format(Style.GENERIC_SHORT, tz, date); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.METAZONE_SHORT; } else if (count == 4) { // "vvvv" result = tzFormat().format(Style.GENERIC_LONG, tz, date); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.METAZONE_LONG; } buf.append(result); break; case 29: // 'V' - TIMEZONE_SPECIAL_FIELD if (count == 1) { // "V" result = tzFormat().format(Style.ZONE_ID_SHORT, tz, date); } else if (count == 2) { // "VV" result = tzFormat().format(Style.ZONE_ID, tz, date); } else if (count == 3) { // "VVV" result = tzFormat().format(Style.EXEMPLAR_LOCATION, tz, date); } else if (count == 4) { // "VVVV" result = tzFormat().format(Style.GENERIC_LOCATION, tz, date); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.ZONE_LONG; } buf.append(result); break; case 31: // 'O' - TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD if (count == 1) { // "O" - Short Localized GMT format result = tzFormat().format(Style.LOCALIZED_GMT_SHORT, tz, date); } else if (count == 4) { // "OOOO" - Localized GMT format result = tzFormat().format(Style.LOCALIZED_GMT, tz, date); } buf.append(result); break; case 32: // 'X' - TIMEZONE_ISO_FIELD if (count == 1) { // "X" - ISO Basic/Short result = tzFormat().format(Style.ISO_BASIC_SHORT, tz, date); } else if (count == 2) { // "XX" - ISO Basic/Fixed result = tzFormat().format(Style.ISO_BASIC_FIXED, tz, date); } else if (count == 3) { // "XXX" - ISO Extended/Fixed result = tzFormat().format(Style.ISO_EXTENDED_FIXED, tz, date); } else if (count == 4) { // "XXXX" - ISO Basic/Optional second field result = tzFormat().format(Style.ISO_BASIC_FULL, tz, date); } else if (count == 5) { // "XXXXX" - ISO Extended/Optional second field result = tzFormat().format(Style.ISO_EXTENDED_FULL, tz, date); } buf.append(result); break; case 33: // 'x' - TIMEZONE_ISO_LOCAL_FIELD if (count == 1) { // "x" - ISO Local Basic/Short result = tzFormat().format(Style.ISO_BASIC_LOCAL_SHORT, tz, date); } else if (count == 2) { // "x" - ISO Local Basic/Fixed result = tzFormat().format(Style.ISO_BASIC_LOCAL_FIXED, tz, date); } else if (count == 3) { // "xxx" - ISO Local Extended/Fixed result = tzFormat().format(Style.ISO_EXTENDED_LOCAL_FIXED, tz, date); } else if (count == 4) { // "xxxx" - ISO Local Basic/Optional second field result = tzFormat().format(Style.ISO_BASIC_LOCAL_FULL, tz, date); } else if (count == 5) { // "xxxxx" - ISO Local Extended/Optional second field result = tzFormat().format(Style.ISO_EXTENDED_LOCAL_FULL, tz, date); } buf.append(result); break; case 25: // 'c' - STANDALONE DAY (use DOW_LOCAL for numeric, DAY_OF_WEEK for standalone) if (count < 3) { zeroPaddingNumber(currentNumberFormat,buf, value, 1, maxIntCount); break; } // For alpha day-of-week, we don't want DOW_LOCAL, // we need the standard DAY_OF_WEEK. value = cal.get(Calendar.DAY_OF_WEEK); if (count == 5) { safeAppend(formatData.standaloneNarrowWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_NARROW; } else if (count == 4) { safeAppend(formatData.standaloneWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_STANDALONE; } else if (count == 6 && formatData.standaloneShorterWeekdays != null) { safeAppend(formatData.standaloneShorterWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_STANDALONE; } else { // count == 3 safeAppend(formatData.standaloneShortWeekdays, value, buf); capContextUsageType = DateFormatSymbols.CapitalizationContextUsage.DAY_STANDALONE; } break; case 27: // 'Q' - QUARTER if (count >= 5) { safeAppend(formatData.narrowQuarters, value/3, buf); } else if (count == 4) { safeAppend(formatData.quarters, value/3, buf); } else if (count == 3) { safeAppend(formatData.shortQuarters, value/3, buf); } else { zeroPaddingNumber(currentNumberFormat,buf, (value/3)+1, count, maxIntCount); } break; case 28: // 'q' - STANDALONE QUARTER if (count >= 5) { safeAppend(formatData.standaloneNarrowQuarters, value/3, buf); } else if (count == 4) { safeAppend(formatData.standaloneQuarters, value/3, buf); } else if (count == 3) { safeAppend(formatData.standaloneShortQuarters, value/3, buf); } else { zeroPaddingNumber(currentNumberFormat,buf, (value/3)+1, count, maxIntCount); } break; case 35: // 'b' - am/pm/noon/midnight { // Note: "midnight" can be ambiguous as to whether it refers to beginning of day or end of day. // For ICU 57 output of "midnight" is temporarily suppressed. int hour = cal.get(Calendar.HOUR_OF_DAY); String toAppend = null; // For "midnight" and "noon": // Time, as displayed, must be exactly noon or midnight. // This means minutes and seconds, if present, must be zero. if ((/*hour == 0 ||*/ hour == 12) && (!hasMinute || cal.get(Calendar.MINUTE) == 0) && (!hasSecond || cal.get(Calendar.SECOND) == 0)) { // Stealing am/pm value to use as our array index. // It works out: am/midnight are both 0, pm/noon are both 1, // 12 am is 12 midnight, and 12 pm is 12 noon. value = cal.get(Calendar.AM_PM); if (count <= 3) { toAppend = formatData.abbreviatedDayPeriods[value]; } else if (count == 4 || count > 5) { toAppend = formatData.wideDayPeriods[value]; } else { // count == 5 toAppend = formatData.narrowDayPeriods[value]; } } if (toAppend == null) { // Time isn't exactly midnight or noon (as displayed) or localized string doesn't // exist for requested period. Fall back to am/pm instead. // We are passing a different patternCharToOutput because we want to add // 'b' to field position. This makes this fallback stable when // there is a data change on locales. subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'b', cal); } else { buf.append(toAppend); } break; } case 36: // 'B' - flexible day period { // TODO: Maybe fetch the DayperiodRules during initialization (instead of at the first // loading of an instance) if a relevant pattern character (b or B) is used. DayPeriodRules ruleSet = DayPeriodRules.getInstance(getLocale()); if (ruleSet == null) { // Data doesn't exist for the locale we're looking for. // Fall back to am/pm. // We are passing a different patternCharToOutput because we want to add // 'B' to field position. This makes this fallback stable when // there is a data change on locales. subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'B', cal); return; } // Get current display time. int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = 0; int second = 0; if (hasMinute) { minute = cal.get(Calendar.MINUTE); } if (hasSecond) { second = cal.get(Calendar.SECOND); } // Determine day period. DayPeriodRules.DayPeriod periodType; if (hour == 0 && minute == 0 && second == 0 && ruleSet.hasMidnight()) { periodType = DayPeriodRules.DayPeriod.MIDNIGHT; } else if (hour == 12 && minute == 0 && second == 0 && ruleSet.hasNoon()) { periodType = DayPeriodRules.DayPeriod.NOON; } else { periodType = ruleSet.getDayPeriodForHour(hour); } // Note: "midnight" can be ambiguous as to whether it refers to beginning of day or end of day. // For ICU 57 output of "midnight" is temporarily suppressed. // Rule set exists, therefore periodType can't be null. // Get localized string. assert(periodType != null); String toAppend = null; int index; if (periodType != DayPeriodRules.DayPeriod.AM && periodType != DayPeriodRules.DayPeriod.PM && periodType != DayPeriodRules.DayPeriod.MIDNIGHT) { index = periodType.ordinal(); if (count <= 3) { toAppend = formatData.abbreviatedDayPeriods[index]; // i.e. short } else if (count == 4 || count > 5) { toAppend = formatData.wideDayPeriods[index]; } else { // count == 5 toAppend = formatData.narrowDayPeriods[index]; } } // Fallback schedule: // Midnight/Noon -> General Periods -> AM/PM. // Midnight/Noon -> General Periods. if (toAppend == null && (periodType == DayPeriodRules.DayPeriod.MIDNIGHT || periodType == DayPeriodRules.DayPeriod.NOON)) { periodType = ruleSet.getDayPeriodForHour(hour); index = periodType.ordinal(); if (count <= 3) { toAppend = formatData.abbreviatedDayPeriods[index]; // i.e. short } else if (count == 4 || count > 5) { toAppend = formatData.wideDayPeriods[index]; } else { // count == 5 toAppend = formatData.narrowDayPeriods[index]; } } // General Periods -> AM/PM. if (periodType == DayPeriodRules.DayPeriod.AM || periodType == DayPeriodRules.DayPeriod.PM || toAppend == null) { // We are passing a different patternCharToOutput because we want to add // 'B' to field position. This makes this fallback stable when // there is a data change on locales. subFormat(buf, 'a', count, beginOffset, fieldNum, capitalizationContext, pos, 'B', cal); return; } else { buf.append(toAppend); } break; } case 37: // TIME SEPARATOR (no pattern character currently defined, we should // not get here but leave support in for future definition. buf.append(formatData.getTimeSeparatorString()); 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 20: // 'u' - EXTENDED_YEAR // case 21: // 'g' - JULIAN_DAY // case 22: // 'A' - MILLISECONDS_IN_DAY zeroPaddingNumber(currentNumberFormat,buf, value, count, maxIntCount); break; } // switch (patternCharIndex) if (fieldNum == 0 && capitalizationContext != null && buf.length() > bufstart && UCharacter.isLowerCase(buf.codePointAt(bufstart))) { boolean titlecase = false; switch (capitalizationContext) { case CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE: titlecase = true; break; case CAPITALIZATION_FOR_UI_LIST_OR_MENU: case CAPITALIZATION_FOR_STANDALONE: if (formatData.capitalization != null) { boolean[] transforms = formatData.capitalization.get(capContextUsageType); titlecase = (capitalizationContext==DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU)? transforms[0]: transforms[1]; } break; default: break; } if (titlecase) { if (capitalizationBrkIter == null) { // should only happen when deserializing, etc. capitalizationBrkIter = BreakIterator.getSentenceInstance(locale); } // Note, the call to UCharacter.toTitleCase below is the only place that // (the clone of) capitalizationBrkIter is actually used. BreakIterator mutableCapitalizationBrkIter = (BreakIterator)capitalizationBrkIter.clone(); String firstField = buf.substring(bufstart); // bufstart or beginOffset, should be the same String firstFieldTitleCase = UCharacter.toTitleCase(locale, firstField, mutableCapitalizationBrkIter, UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT); buf.replace(bufstart, buf.length(), firstFieldTitleCase); } } // Set the FieldPosition (for the first occurrence only) int outputCharIndex = getIndexFromChar(patternCharToOutput); if (pos.getBeginIndex() == pos.getEndIndex()) { if (pos.getField() == PATTERN_INDEX_TO_DATE_FORMAT_FIELD[outputCharIndex]) { pos.setBeginIndex(beginOffset); pos.setEndIndex(beginOffset + buf.length() - bufstart); } else if (pos.getFieldAttribute() == PATTERN_INDEX_TO_DATE_FORMAT_ATTRIBUTE[outputCharIndex]) { pos.setBeginIndex(beginOffset); pos.setEndIndex(beginOffset + buf.length() - bufstart); } } } private static void safeAppend(String[] array, int value, StringBuffer appendTo) { if (array != null && value >= 0 && value < array.length) { appendTo.append(array[value]); } } private static void safeAppendWithMonthPattern(String[] array, int value, StringBuffer appendTo, String monthPattern) { if (array != null && value >= 0 && value < array.length) { if (monthPattern == null) { appendTo.append(array[value]); } else { String s = SimpleFormatterImpl.formatRawPattern(monthPattern, 1, 1, array[value]); appendTo.append(s); } } } /* * PatternItem store parsed date/time field pattern information. */ private static class PatternItem { final char type; final int length; final boolean isNumeric; PatternItem(char type, int length) { this.type = type; this.length = length; isNumeric = isNumeric(type, length); } } private static ICUCache PARSED_PATTERN_CACHE = new SimpleCache<>(); private transient Object[] patternItems; /* * Returns parsed pattern items. Each item is either String or * PatternItem. */ private Object[] getPatternItems() { if (patternItems != null) { return patternItems; } patternItems = PARSED_PATTERN_CACHE.get(pattern); if (patternItems != null) { return patternItems; } boolean isPrevQuote = false; boolean inQuote = false; StringBuilder text = new StringBuilder(); char itemType = 0; // 0 for string literal, otherwise date/time pattern character int itemLength = 1; List items = new ArrayList<>(); for (int i = 0; i < pattern.length(); i++) { char ch = pattern.charAt(i); if (ch == '\'') { if (isPrevQuote) { text.append('\''); isPrevQuote = false; } else { isPrevQuote = true; if (itemType != 0) { items.add(new PatternItem(itemType, itemLength)); itemType = 0; } } inQuote = !inQuote; } else { isPrevQuote = false; if (inQuote) { text.append(ch); } else { if (isSyntaxChar(ch)) { // a date/time pattern character if (ch == itemType) { itemLength++; } else { if (itemType == 0) { if (text.length() > 0) { items.add(text.toString()); text.setLength(0); } } else { items.add(new PatternItem(itemType, itemLength)); } itemType = ch; itemLength = 1; } } else { // a string literal if (itemType != 0) { items.add(new PatternItem(itemType, itemLength)); itemType = 0; } text.append(ch); } } } } // handle last item if (itemType == 0) { if (text.length() > 0) { items.add(text.toString()); text.setLength(0); } } else { items.add(new PatternItem(itemType, itemLength)); } patternItems = items.toArray(new Object[items.size()]); PARSED_PATTERN_CACHE.put(pattern, patternItems); return patternItems; } /** * Internal high-speed method. Reuses a StringBuffer for results * instead of creating a String on the heap for each call. * @internal * @deprecated This API is ICU internal only. */ @Deprecated protected void zeroPaddingNumber(NumberFormat nf,StringBuffer buf, int value, int minDigits, int maxDigits) { // Note: Indian calendar uses negative value for a calendar // field. fastZeroPaddingNumber cannot handle negative numbers. // BTW, it looks like a design bug in the Indian calendar... if (useLocalZeroPaddingNumberFormat && value >= 0) { fastZeroPaddingNumber(buf, value, minDigits, maxDigits); } else { nf.setMinimumIntegerDigits(minDigits); nf.setMaximumIntegerDigits(maxDigits); nf.format(value, buf, new FieldPosition(-1)); } } /** * Overrides superclass method and * This method also clears per field NumberFormat instances * previously set by {@link #setNumberFormat(String, NumberFormat)} * * @stable ICU 2.0 */ @Override public void setNumberFormat(NumberFormat newNumberFormat) { // Override this method to update local zero padding number formatter super.setNumberFormat(newNumberFormat); initLocalZeroPaddingNumberFormat(); initializeTimeZoneFormat(true); if (numberFormatters != null) { numberFormatters = null; } if (overrideMap != null) { overrideMap = null; } } /* * Initializes transient fields for fast simple numeric formatting * code. This method should be called whenever number format is updated. */ private void initLocalZeroPaddingNumberFormat() { if (numberFormat instanceof DecimalFormat) { DecimalFormatSymbols tmpDecfs = ((DecimalFormat)numberFormat).getDecimalFormatSymbols(); String[] tmpDigits = tmpDecfs.getDigitStringsLocal(); useLocalZeroPaddingNumberFormat = true; decDigits = new char[10]; for (int i = 0; i < 10; i++) { if (tmpDigits[i].length() > 1) { useLocalZeroPaddingNumberFormat = false; break; } decDigits[i] = tmpDigits[i].charAt(0); } } else if (numberFormat instanceof DateNumberFormat) { decDigits = ((DateNumberFormat)numberFormat).getDigits(); useLocalZeroPaddingNumberFormat = true; } else { useLocalZeroPaddingNumberFormat = false; } if (useLocalZeroPaddingNumberFormat) { decimalBuf = new char[DECIMAL_BUF_SIZE]; } } // If true, use local version of zero padding number format private transient boolean useLocalZeroPaddingNumberFormat; private transient char[] decDigits; // read-only - can be shared by multiple instances private transient char[] decimalBuf; // mutable - one per instance private static final int DECIMAL_BUF_SIZE = 10; // sufficient for int numbers /* * Lightweight zero padding integer number format function. * * Note: This implementation is almost equivalent to format method in DateNumberFormat. * In the method zeroPaddingNumber above should be able to use the one in DateNumberFormat, * but, it does not help IBM J9's JIT to optimize the performance much. In simple repeative * date format test case, having local implementation is ~10% faster than using one in * DateNumberFormat on IBM J9 VM. On Sun Hotspot VM, I do not see such difference. * * -Yoshito */ private void fastZeroPaddingNumber(StringBuffer buf, int value, int minDigits, int maxDigits) { int limit = decimalBuf.length < maxDigits ? decimalBuf.length : maxDigits; int index = limit - 1; while (true) { decimalBuf[index] = decDigits[(value % 10)]; value /= 10; if (index == 0 || value == 0) { break; } index--; } int padding = minDigits - (limit - index); while (padding > 0 && index > 0) { decimalBuf[--index] = decDigits[0]; padding--; } while (padding > 0) { // when pattern width is longer than decimalBuf, need extra // leading zeros - ticke#7595 buf.append(decDigits[0]); padding--; } buf.append(decimalBuf, index, limit - index); } /** * 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 always. */ private static final String NUMERIC_FORMAT_CHARS = "ADdFgHhKkmrSsuWwYy"; /** * Format characters that indicate numeric fields when pattern length * is up to 2. */ private static final String NUMERIC_FORMAT_CHARS2 = "ceLMQq"; /** * Return true if the given format character, occurring count * times, represents a numeric field. */ private static final boolean isNumeric(char formatChar, int count) { return NUMERIC_FORMAT_CHARS.indexOf(formatChar) >= 0 || (count <= 2 && NUMERIC_FORMAT_CHARS2.indexOf(formatChar) >= 0); } /** * Maximum range for detecting daylight offset of a time zone when parsed time zone * string indicates it's daylight saving time, but the detected time zone does not * observe daylight saving time at the parsed date. */ private static final long MAX_DAYLIGHT_DETECTION_RANGE = 30*365*24*60*60*1000L; /** * Overrides DateFormat * @see DateFormat * @stable ICU 2.0 */ @Override public void parse(String text, Calendar cal, ParsePosition parsePos) { TimeZone backupTZ = null; Calendar resultCal = null; if (cal != calendar && !cal.getType().equals(calendar.getType())) { // Different calendar type // We use the time/zone from the input calendar, but // do not use the input calendar for field calculation. calendar.setTimeInMillis(cal.getTimeInMillis()); backupTZ = calendar.getTimeZone(); calendar.setTimeZone(cal.getTimeZone()); resultCal = cal; cal = calendar; } int pos = parsePos.getIndex(); if(pos < 0) { parsePos.setErrorIndex(0); return; } int start = pos; // Hold the day period until everything else is parsed, because we need // the hour to interpret time correctly. // Using an one-element array for output parameter. Output dayPeriod = new Output<>(null); Output tzTimeType = new Output<>(TimeType.UNKNOWN); boolean[] ambiguousYear = { false }; // item index for the first numeric field within a contiguous numeric run int numericFieldStart = -1; // item length for the first numeric field within a contiguous numeric run int numericFieldLength = 0; // start index of numeric text run in the input text int numericStartPos = 0; MessageFormat numericLeapMonthFormatter = null; if (formatData.leapMonthPatterns != null && formatData.leapMonthPatterns.length >= DateFormatSymbols.DT_MONTH_PATTERN_COUNT) { numericLeapMonthFormatter = new MessageFormat(formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_NUMERIC], locale); } Object[] items = getPatternItems(); int i = 0; while (i < items.length) { if (items[i] instanceof PatternItem) { // Handle pattern field PatternItem field = (PatternItem)items[i]; if (field.isNumeric) { // 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 (numericFieldStart == -1) { // check if this field is followed by abutting another numeric field if ((i + 1) < items.length && (items[i + 1] instanceof PatternItem) && ((PatternItem)items[i + 1]).isNumeric) { // record the first numeric field within a numeric text run numericFieldStart = i; numericFieldLength = field.length; numericStartPos = pos; } } } if (numericFieldStart != -1) { // Handle a numeric field within abutting numeric fields int len = field.length; if (numericFieldStart == i) { len = numericFieldLength; } // Parse a numeric field pos = subParse(text, pos, field.type, len, true, false, ambiguousYear, cal, numericLeapMonthFormatter, tzTimeType); if (pos < 0) { // If the parse fails anywhere in the numeric run, back up to the // start of the run and use shorter pattern length for the first // numeric field. --numericFieldLength; if (numericFieldLength == 0) { // can not make shorter any more parsePos.setIndex(start); parsePos.setErrorIndex(pos); if (backupTZ != null) { calendar.setTimeZone(backupTZ); } return; } i = numericFieldStart; pos = numericStartPos; continue; } } else if (field.type != 'l') { // (SMALL LETTER L) obsolete pattern char just gets ignored // Handle a non-numeric field or a non-abutting numeric field numericFieldStart = -1; int s = pos; pos = subParse(text, pos, field.type, field.length, false, true, ambiguousYear, cal, numericLeapMonthFormatter, tzTimeType, dayPeriod); if (pos < 0) { if (pos == ISOSpecialEra) { // era not present, in special cases allow this to continue pos = s; if (i+1 < items.length) { String patl = null; // if it will cause a class cast exception to String, we can't use it try { patl = (String)items[i+1]; } catch(ClassCastException cce) { parsePos.setIndex(start); parsePos.setErrorIndex(s); if (backupTZ != null) { calendar.setTimeZone(backupTZ); } return; } // get next item in pattern if(patl == null) patl = (String)items[i+1]; int plen = patl.length(); int idx=0; // White space characters found in pattern. // Skip contiguous white spaces. while (idx < plen) { char pch = patl.charAt(idx); if (PatternProps.isWhiteSpace(pch) || UCharacter.isUWhiteSpace(pch)) idx++; else break; } // if next item in pattern is all whitespace, skip it if (idx == plen) { i++; } } } else { parsePos.setIndex(start); parsePos.setErrorIndex(s); if (backupTZ != null) { calendar.setTimeZone(backupTZ); } return; } } } } else { // Handle literal pattern text literal numericFieldStart = -1; boolean[] complete = new boolean[1]; pos = matchLiteral(text, pos, items, i, complete); if (!complete[0]) { // Set the position of mismatch parsePos.setIndex(start); parsePos.setErrorIndex(pos); if (backupTZ != null) { calendar.setTimeZone(backupTZ); } return; } } ++i; } // Special hack for trailing "." after non-numeric field. if (pos < text.length()) { char extra = text.charAt(pos); if (extra == '.' && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_WHITESPACE) && items.length != 0) { // only do if the last field is not numeric Object lastItem = items[items.length - 1]; if (lastItem instanceof PatternItem && !((PatternItem)lastItem).isNumeric) { pos++; // skip the extra "." } } } // If dayPeriod is set, use it in conjunction with hour-of-day to determine am/pm. if (dayPeriod.value != null) { DayPeriodRules ruleSet = DayPeriodRules.getInstance(getLocale()); if (!cal.isSet(Calendar.HOUR) && !cal.isSet(Calendar.HOUR_OF_DAY)) { // If hour is not set, set time to the midpoint of current day period, overwriting // minutes if it's set. double midPoint = ruleSet.getMidPointForDayPeriod(dayPeriod.value); // Truncate midPoint toward zero to get the hour. // Any leftover means it was a half-hour. int midPointHour = (int) midPoint; int midPointMinute = (midPoint - midPointHour) > 0 ? 30 : 0; // No need to set am/pm because hour-of-day is set last therefore takes precedence. cal.set(Calendar.HOUR_OF_DAY, midPointHour); cal.set(Calendar.MINUTE, midPointMinute); } else { int hourOfDay; if (cal.isSet(Calendar.HOUR_OF_DAY)) { // Hour is parsed in 24-hour format. hourOfDay = cal.get(Calendar.HOUR_OF_DAY); } else { // Hour is parsed in 12-hour format. hourOfDay = cal.get(Calendar.HOUR); // cal.get() turns 12 to 0 for 12-hour time; change 0 to 12 // so 0 unambiguously means a 24-hour time from above. if (hourOfDay == 0) { hourOfDay = 12; } } assert(0 <= hourOfDay && hourOfDay <= 23); // If hour-of-day is 0 or 13 thru 23 then input time in unambiguously in 24-hour format. if (hourOfDay == 0 || (13 <= hourOfDay && hourOfDay <= 23)) { // Make hour-of-day take precedence over (hour + am/pm) by setting it again. cal.set(Calendar.HOUR_OF_DAY, hourOfDay); } else { // We have a 12-hour time and need to choose between am and pm. // Behave as if dayPeriod spanned 6 hours each way from its center point. // This will parse correctly for consistent time + period (e.g. 10 at night) as // well as provide a reasonable recovery for inconsistent time + period (e.g. // 9 in the afternoon). // Assume current time is in the AM. // - Change 12 back to 0 for easier handling of 12am. // - Append minutes as fractional hours because e.g. 8:15 and 8:45 could be parsed // into different half-days if center of dayPeriod is at 14:30. // - cal.get(MINUTE) will return 0 if MINUTE is unset, which works. if (hourOfDay == 12) { hourOfDay = 0; } double currentHour = hourOfDay + cal.get(Calendar.MINUTE) / 60.0; double midPointHour = ruleSet.getMidPointForDayPeriod(dayPeriod.value); double hoursAheadMidPoint = currentHour - midPointHour; // Assume current time is in the AM. if (-6 <= hoursAheadMidPoint && hoursAheadMidPoint < 6) { // Assumption holds; set time as such. cal.set(Calendar.AM_PM, 0); } else { cal.set(Calendar.AM_PM, 1); } } } } // 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(getDefaultCenturyStart()) ) { 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 { TimeType tztype = tzTimeType.value; if (ambiguousYear[0] || tztype != TimeType.UNKNOWN) { // 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; if (ambiguousYear[0]) { // the two-digit year == the default start year copy = (Calendar)cal.clone(); Date parsedDate = copy.getTime(); if (parsedDate.before(getDefaultCenturyStart())) { // We can't use add here because that does a complete() first. cal.set(Calendar.YEAR, getDefaultCenturyStartYear() + 100); } } if (tztype != TimeType.UNKNOWN) { copy = (Calendar)cal.clone(); TimeZone tz = copy.getTimeZone(); BasicTimeZone btz = null; if (tz instanceof BasicTimeZone) { btz = (BasicTimeZone)tz; } // Get local millis copy.set(Calendar.ZONE_OFFSET, 0); copy.set(Calendar.DST_OFFSET, 0); long localMillis = copy.getTimeInMillis(); // Make sure parsed time zone type (Standard or Daylight) // matches the rule used by the parsed time zone. int[] offsets = new int[2]; if (btz != null) { if (tztype == TimeType.STANDARD) { btz.getOffsetFromLocal(localMillis, LocalOption.STANDARD_FORMER, LocalOption.STANDARD_LATTER, offsets); } else { btz.getOffsetFromLocal(localMillis, LocalOption.DAYLIGHT_FORMER, LocalOption.DAYLIGHT_LATTER, offsets); } } else { // No good way to resolve ambiguous time at transition, // but following code work in most case. tz.getOffset(localMillis, true, offsets); if (tztype == TimeType.STANDARD && offsets[1] != 0 || tztype == TimeType.DAYLIGHT && offsets[1] == 0) { // Roll back one day and try it again. // Note: This code assumes 1. timezone transition only happens // once within 24 hours at max // 2. the difference of local offsets at the transition is // less than 24 hours. tz.getOffset(localMillis - (24*60*60*1000), true, offsets); } } // Now, compare the results with parsed type, either standard or // daylight saving time int resolvedSavings = offsets[1]; if (tztype == TimeType.STANDARD) { if (offsets[1] != 0) { // Override DST_OFFSET = 0 in the result calendar resolvedSavings = 0; } } else { // tztype == TZTYPE_DST if (offsets[1] == 0) { if (btz != null) { // This implementation resolves daylight saving time offset // closest rule after the given time. long baseTime = localMillis + offsets[0]; long time = baseTime; long limit = baseTime + MAX_DAYLIGHT_DETECTION_RANGE; TimeZoneTransition trs = null; // Search for DST rule after the given time while (time < limit) { trs = btz.getNextTransition(time, false); if (trs == null) { break; } resolvedSavings = trs.getTo().getDSTSavings(); if (resolvedSavings != 0) { break; } time = trs.getTime(); } if (resolvedSavings == 0) { // If no DST rule after the given time was found, search for // DST rule before. time = baseTime; limit = baseTime - MAX_DAYLIGHT_DETECTION_RANGE; while (time > limit) { trs = btz.getPreviousTransition(time, true); if (trs == null) { break; } resolvedSavings = trs.getFrom().getDSTSavings(); if (resolvedSavings != 0) { break; } time = trs.getTime() - 1; } if (resolvedSavings == 0) { resolvedSavings = btz.getDSTSavings(); } } } else { resolvedSavings = tz.getDSTSavings(); } if (resolvedSavings == 0) { // Final fallback resolvedSavings = millisPerHour; } } } cal.set(Calendar.ZONE_OFFSET, offsets[0]); cal.set(Calendar.DST_OFFSET, resolvedSavings); } } } // 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); if (backupTZ != null) { calendar.setTimeZone(backupTZ); } return; } // Set the parsed result if local calendar is used // instead of the input calendar if (resultCal != null) { resultCal.setTimeZone(cal.getTimeZone()); resultCal.setTimeInMillis(cal.getTimeInMillis()); } // Restore the original time zone if required if (backupTZ != null) { calendar.setTimeZone(backupTZ); } } /** * Matches text (starting at pos) with patl. Returns the new pos, and sets complete[0] * if it matched the entire text. Whitespace sequences are treated as singletons. *

If isLenient and if we fail to match the first time, some special hacks are put into place. *

  • we are between date and time fields, then one or more whitespace characters * in the text are accepted instead.
  • *
    • we are after a non-numeric field, and the text starts with a ".", we skip it.
    • *
    */ private int matchLiteral(String text, int pos, Object[] items, int itemIndex, boolean[] complete) { int originalPos = pos; String patternLiteral = (String)items[itemIndex]; int plen = patternLiteral.length(); int tlen = text.length(); int idx = 0; while (idx < plen && pos < tlen) { char pch = patternLiteral.charAt(idx); char ich = text.charAt(pos); if ((PatternProps.isWhiteSpace(pch) || UCharacter.isUWhiteSpace(pch)) && (PatternProps.isWhiteSpace(ich) || UCharacter.isUWhiteSpace(ich))) { // White space characters found in both patten and input. // Skip contiguous white spaces. while ((idx + 1) < plen && (PatternProps.isWhiteSpace(patternLiteral.charAt(idx + 1)) || UCharacter.isUWhiteSpace(patternLiteral.charAt(idx + 1)))) { ++idx; } while ((pos + 1) < tlen && (PatternProps.isWhiteSpace(text.charAt(pos + 1)) || UCharacter.isUWhiteSpace(text.charAt(pos + 1)))) { ++pos; } } else if (pch != ich) { if (ich == '.' && pos == originalPos && 0 < itemIndex && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_WHITESPACE)) { Object before = items[itemIndex-1]; if (before instanceof PatternItem) { boolean isNumeric = ((PatternItem) before).isNumeric; if (!isNumeric) { ++pos; // just update pos continue; } } } else if ((pch == ' ' || pch == '.') && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_WHITESPACE)) { ++idx; continue; } else if (pos != originalPos && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_LITERAL_MATCH)) { ++idx; continue; } break; } ++idx; ++pos; } complete[0] = idx == plen; if (complete[0] == false && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_WHITESPACE) && 0 < itemIndex && itemIndex < items.length - 1) { // If fully lenient, accept " "* for any text between a date and a time field // We don't go more lenient, because we don't want to accept "12/31" for "12:31". // People may be trying to parse for a date, then for a time. if (originalPos < tlen) { Object before = items[itemIndex-1]; Object after = items[itemIndex+1]; if (before instanceof PatternItem && after instanceof PatternItem) { char beforeType = ((PatternItem) before).type; char afterType = ((PatternItem) after).type; if (DATE_PATTERN_TYPE.contains(beforeType) != DATE_PATTERN_TYPE.contains(afterType)) { int newPos = originalPos; while (newPos < tlen) { char ich = text.charAt(newPos); if (!PatternProps.isWhiteSpace(ich)) { break; } ++newPos; } complete[0] = newPos > originalPos; pos = newPos; } } } } return pos; } static final UnicodeSet DATE_PATTERN_TYPE = new UnicodeSet("[GyYuUQqMLlwWd]").freeze(); /** * Attempt to match the text at a given position against two arrays of * month symbol 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 cal 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 wideData the string array of wide month symbols * @param shortData the string array of short month symbols * @param cal * @return the new start position if matching succeeded; a negative * number indicating matching failure, otherwise. As a side effect, * sets the cal field field to the index * of the best match, if matching succeeded. * @internal * @deprecated This API is ICU internal only. * Does not handle monthPattern. * field is always Calendar.MONTH */ @Deprecated private int matchAlphaMonthStrings(String text, int start, String[] wideData, String[] shortData, Calendar cal) { int i; int bestMatchLength = 0, bestMatch = -1; for (i = 0; i bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, wideData[i], length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; } } for (i = 0; i bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, shortData[i], length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; } } if (bestMatch >= 0) { cal.set(Calendar.MONTH, bestMatch); return start + bestMatchLength; } return ~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 cal 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. * @param cal * @return the new start position if matching succeeded; a negative * number indicating matching failure, otherwise. As a side effect, * sets the cal field field 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) { return matchString(text, start, field, data, null, cal); } /** * 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 cal 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. * @param monthPattern leap month pattern, or null if none. * @param cal * @return the new start position if matching succeeded; a negative * number indicating matching failure, otherwise. As a side effect, * sets the cal field field to the index * of the best match, if matching succeeded. * @internal * @deprecated This API is ICU internal only. */ @Deprecated private int matchString(String text, int start, int field, String[] data, String monthPattern, 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; int isLeapMonth = 0; int matchLength = 0; for (; i bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, data[i], length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; isLeapMonth = 0; } if (monthPattern != null) { String leapMonthName = SimpleFormatterImpl.formatRawPattern( monthPattern, 1, 1, data[i]); length = leapMonthName.length(); if (length > bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, leapMonthName, length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; isLeapMonth = 1; } } } if (bestMatch >= 0) { if (field >= 0) { if (field == Calendar.YEAR) { bestMatch++; // only get here for cyclic year names, which match 1-based years 1-60 } cal.set(field, bestMatch); if (monthPattern != null) { cal.set(Calendar.IS_LEAP_MONTH, isLeapMonth); } } return start + bestMatchLength; } return ~start; } private int regionMatchesWithOptionalDot(String text, int start, String data, int length) { boolean matches = text.regionMatches(true, start, data, 0, length); if (matches) { return length; } if (data.length() > 0 && data.charAt(data.length()-1) == '.') { if (text.regionMatches(true, start, data, 0, length-1)) { return length - 1; } } return -1; } /** * Attempt to match the text at a given position against an array of quarter * 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 cal 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 cal field field to the index * of the best match, if matching succeeded. * @stable ICU 2.0 */ protected int matchQuarterString(String text, int start, int field, String[] data, Calendar cal) { int i = 0; int count = data.length; // There may be multiple strings in the data[] array which begin with // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech). // We keep track of the longest match, and return that. Note that this // unfortunately requires us to test all array elements. int bestMatchLength = 0, bestMatch = -1; int matchLength = 0; for (; i bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, data[i], length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; } } if (bestMatch >= 0) { cal.set(field, bestMatch * 3); return start + bestMatchLength; } return -start; } /** * Similar to matchQuarterString but customized for day periods. */ private int matchDayPeriodString(String text, int start, String[] data, int dataLength, Output dayPeriod) { int bestMatchLength = 0, bestMatch = -1; int matchLength = 0; for (int i = 0; i < dataLength; ++i) { // Only try matching if the string exists. if (data[i] != null) { int length = data[i].length(); if (length > bestMatchLength && (matchLength = regionMatchesWithOptionalDot(text, start, data[i], length)) >= 0) { bestMatch = i; bestMatchLength = matchLength; } } } if (bestMatch >= 0) { dayPeriod.value = DayPeriodRules.DayPeriod.VALUES[bestMatch]; return start + bestMatchLength; } return -start; } /** * Protected method that converts one field of the input string into a * numeric field value in cal. 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. * @param cal * @return the new start position if matching succeeded; a negative * number indicating matching failure, otherwise. As a side effect, * set the appropriate field of cal 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) { return subParse(text, start, ch, count, obeyCount, allowNegative, ambiguousYear, cal, null, null); } /** * Overloading to provide default argument (null) for day period. */ private int subParse(String text, int start, char ch, int count, boolean obeyCount, boolean allowNegative, boolean[] ambiguousYear, Calendar cal, MessageFormat numericLeapMonthFormatter, Output tzTimeType) { return subParse(text, start, ch, count, obeyCount, allowNegative, ambiguousYear, cal, null, null, null); } /** * Protected method that converts one field of the input string into a * numeric field value in cal. 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. * @param cal * @param numericLeapMonthFormatter if non-null, used to parse numeric leap months. * @param tzTimeType the type of parsed time zone - standard, daylight or unknown (output). * This parameter can be null if caller does not need the information. * @return the new start position if matching succeeded; a negative * number indicating matching failure, otherwise. As a side effect, * set the appropriate field of cal with the parsed * value. * @internal * @deprecated This API is ICU internal only. */ @Deprecated @SuppressWarnings("fallthrough") private int subParse(String text, int start, char ch, int count, boolean obeyCount, boolean allowNegative, boolean[] ambiguousYear, Calendar cal, MessageFormat numericLeapMonthFormatter, Output tzTimeType, Output dayPeriod) { Number number = null; NumberFormat currentNumberFormat = null; int value = 0; int i; ParsePosition pos = new ParsePosition(0); int patternCharIndex = getIndexFromChar(ch); if (patternCharIndex == -1) { return ~start; } currentNumberFormat = getNumberFormat(ch); int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; // -1 if irrelevant if (numericLeapMonthFormatter != null) { numericLeapMonthFormatter.setFormatByArgumentIndex(0, currentNumberFormat); } boolean isChineseCalendar = ( cal.getType().equals("chinese") || cal.getType().equals("dangi") ); // 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); // Changed the following from || to &&, as in ICU4C; needed to skip NBSP, NNBSP. // Only UWhiteSpace includes \u00A0\u202F\u2009\u3000...; only PatternProps.isWhiteSpace includes \u200E\u200F if (!UCharacter.isUWhiteSpace(c) && !PatternProps.isWhiteSpace(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 /*'k' HOUR_OF_DAY1_FIELD*/ || patternCharIndex == 15 /*'h' HOUR1_FIELD*/ || (patternCharIndex == 2 /*'M' MONTH_FIELD*/ && count <= 2) || patternCharIndex == 26 /*'L' STAND_ALONE_MONTH*/ || patternCharIndex == 19 /*'e' DOW_LOCAL*/ || patternCharIndex == 25 /*'c' STAND_ALONE_DAY_OF_WEEK*/ || patternCharIndex == 1 /*'y' YEAR */ || patternCharIndex == 18 /*'Y' YEAR_WOY */ || patternCharIndex == 30 /*'U' YEAR_NAME_FIELD, falls back to numeric */ || (patternCharIndex == 0 /*'G' ERA */ && isChineseCalendar) || patternCharIndex == 27 /* 'Q' - QUARTER*/ || patternCharIndex == 28 /* 'q' - STANDALONE QUARTER*/ || patternCharIndex == 8 /*'S' FRACTIONAL_SECOND */ ) { // It would be good to unify this with the obeyCount logic below, // but that's going to be difficult. boolean parsedNumericLeapMonth = false; if (numericLeapMonthFormatter != null && (patternCharIndex == 2 || patternCharIndex == 26)) { // First see if we can parse month number with leap month pattern Object[] args = numericLeapMonthFormatter.parse(text, pos); if (args != null && pos.getIndex() > start && (args[0] instanceof Number)) { parsedNumericLeapMonth = true; number = (Number)args[0]; cal.set(Calendar.IS_LEAP_MONTH, 1); } else { pos.setIndex(start); cal.set(Calendar.IS_LEAP_MONTH, 0); } } if (!parsedNumericLeapMonth) { if (obeyCount) { if ((start+count) > text.length()) { return ~start; } number = parseInt(text, count, pos, allowNegative,currentNumberFormat); } else { number = parseInt(text, pos, allowNegative,currentNumberFormat); } if (number == null && !allowNumericFallback(patternCharIndex)) { // only return if pattern is NOT one that allows numeric fallback return ~start; } } if (number != null) { value = number.intValue(); } } switch (patternCharIndex) { case 0: // 'G' - ERA if ( isChineseCalendar ) { // Numeric era handling moved from ChineseDateFormat, // If we didn't have a number, already returned -start above cal.set(Calendar.ERA, value); return pos.getIndex(); } int ps = 0; if (count == 5) { ps = matchString(text, start, Calendar.ERA, formatData.narrowEras, null, cal); } else if (count == 4) { ps = matchString(text, start, Calendar.ERA, formatData.eraNames, null, cal); } else { ps = matchString(text, start, Calendar.ERA, formatData.eras, null, cal); } // check return position, if it equals -start, then matchString error // special case the return code so we don't necessarily fail out until we // verify no year information also if (ps == ~start) ps = ISOSpecialEra; return ps; case 1: // 'y' - YEAR case 18: // 'Y' - YEAR_WOY // 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]*/ /* Skip this for Chinese calendar, moved from ChineseDateFormat */ if ( override != null && (override.compareTo("hebr") == 0 || override.indexOf("y=hebr") >= 0) && value < 1000 ) { value += HEBREW_CAL_CUR_MILLENIUM_START_YEAR; } else if (count == 2 && countDigits(text, start, pos.getIndex()) == 2 && cal.haveDefaultCentury()) { // 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 = getDefaultCenturyStartYear() % 100; ambiguousYear[0] = value == ambiguousTwoDigitYear; value += (getDefaultCenturyStartYear()/100)*100 + (value < ambiguousTwoDigitYear ? 100 : 0); } cal.set(field, value); // Delayed checking for adjustment of Hebrew month numbers in non-leap years. if (DelayedHebrewMonthCheck) { if (!HebrewCalendar.isLeapYear(value)) { cal.add(Calendar.MONTH,1); } DelayedHebrewMonthCheck = false; } return pos.getIndex(); case 30: // 'U' - YEAR_NAME_FIELD if (formatData.shortYearNames != null) { int newStart = matchString(text, start, Calendar.YEAR, formatData.shortYearNames, null, cal); if (newStart > 0) { return newStart; } } if ( number != null && (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC) || formatData.shortYearNames == null || value > formatData.shortYearNames.length) ) { cal.set(Calendar.YEAR, value); return pos.getIndex(); } return ~start; case 2: // 'M' - MONTH case 26: // 'L' - STAND_ALONE_MONTH if (count <= 2 || (number != null && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) { // i.e., M/MM, L/LL or lenient & have a number // Don't want to parse the month if it is a string // while pattern uses numeric style: M/MM, L/LL. // [We computed 'value' above.] cal.set(Calendar.MONTH, value - 1); // When parsing month numbers from the Hebrew Calendar, we might need // to adjust the month depending on whether or not it was a leap year. // We may or may not yet know what year it is, so might have to delay // checking until the year is parsed. if (cal.getType().equals("hebrew") && value >= 6) { if (cal.isSet(Calendar.YEAR)) { if (!HebrewCalendar.isLeapYear(cal.get(Calendar.YEAR))) { cal.set(Calendar.MONTH, value); } } else { DelayedHebrewMonthCheck = true; } } return pos.getIndex(); } else { // count >= 3 // i.e., MMM/MMMM or LLL/LLLL // Want to be able to parse both short and long forms. boolean haveMonthPat = (formatData.leapMonthPatterns != null && formatData.leapMonthPatterns.length >= DateFormatSymbols.DT_MONTH_PATTERN_COUNT); // Try count == 4 first:, unless we're strict int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH)&& count>=3 && count <=4 && !haveMonthPat) { newStart = (patternCharIndex == 2)? matchAlphaMonthStrings(text, start, formatData.months, formatData.shortMonths, cal): matchAlphaMonthStrings(text, start, formatData.standaloneMonths, formatData.standaloneShortMonths, cal); if (newStart > 0) { return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { newStart = (patternCharIndex == 2)? matchString(text, start, Calendar.MONTH, formatData.months, (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_WIDE]: null, cal): matchString(text, start, Calendar.MONTH, formatData.standaloneMonths, (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_WIDE]: null, cal); if (newStart > 0) { return newStart; } } // count == 4 failed, now try count == 3 if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { return (patternCharIndex == 2)? matchString(text, start, Calendar.MONTH, formatData.shortMonths, (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_ABBREV]: null, cal): matchString(text, start, Calendar.MONTH, formatData.standaloneShortMonths, (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_ABBREV]: null, cal); } return newStart; } 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 = countDigits(text, start, pos.getIndex()); if (i < 3) { while (i < 3) { value *= 10; i++; } } else { int a = 1; while (i > 3) { a *= 10; i--; } value /= a; } cal.set(Calendar.MILLISECOND, value); return pos.getIndex(); case 19: // 'e' - DOW_LOCAL if(count <= 2 || (number != null && (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) ) { // i.e. e/ee or lenient and have a number cal.set(field, value); return pos.getIndex(); } // else for eee-eeeeee, fall through to EEE-EEEEEE handling //$FALL-THROUGH$ case 9: { // 'E' - DAY_OF_WEEK // Want to be able to parse at least wide, abbrev, short, and narrow forms. int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.weekdays, null, cal)) > 0) { // try EEEE wide return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if ((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.shortWeekdays, null, cal)) > 0) { // try EEE abbrev return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 6) { if (formatData.shorterWeekdays != null) { if((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.shorterWeekdays, null, cal)) > 0) { // try EEEEEE short return newStart; } } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 5) { if (formatData.narrowWeekdays != null) { if((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.narrowWeekdays, null, cal)) > 0) { // try EEEEE narrow return newStart; } } } return newStart; } case 25: { // 'c' - STAND_ALONE_DAY_OF_WEEK if(count == 1 || (number != null && (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) ) { // i.e. c or lenient and have a number cal.set(field, value); return pos.getIndex(); } // Want to be able to parse at least wide, abbrev, short forms. int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.standaloneWeekdays, null, cal)) > 0) { // try cccc wide return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if ((newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.standaloneShortWeekdays, null, cal)) > 0) { // try ccc abbrev return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 6) { if (formatData.standaloneShorterWeekdays != null) { return matchString(text, start, Calendar.DAY_OF_WEEK, formatData.standaloneShorterWeekdays, null, cal); // try cccccc short } } return newStart; } case 14: { // 'a' - AM_PM // Optionally try both wide/abbrev and narrow forms. // formatData.ampmsNarrow may be null when deserializing DateFormatSymbolsfrom old version, // in which case our only option is wide form int newStart = 0; // try wide/abbrev a-aaaa if(formatData.ampmsNarrow == null || count < 5 || getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH)) { if ((newStart = matchString(text, start, Calendar.AM_PM, formatData.ampms, null, cal)) > 0) { return newStart; } } // try narrow aaaaa if(formatData.ampmsNarrow != null && (count >= 5 || getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH))) { if ((newStart = matchString(text, start, Calendar.AM_PM, formatData.ampmsNarrow, null, cal)) > 0) { return newStart; } } // no matches for given options return ~start; } 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 { Style style = (count < 4) ? Style.SPECIFIC_SHORT : Style.SPECIFIC_LONG; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 23: // 'Z' - TIMEZONE_RFC { Style style = (count < 4) ? Style.ISO_BASIC_LOCAL_FULL : ((count == 5) ? Style.ISO_EXTENDED_FULL : Style.LOCALIZED_GMT); TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 24: // 'v' - TIMEZONE_GENERIC { // Note: 'v' only supports count 1 and 4 Style style = (count < 4) ? Style.GENERIC_SHORT : Style.GENERIC_LONG; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 29: // 'V' - TIMEZONE_SPECIAL { Style style = null; switch (count) { case 1: style = Style.ZONE_ID_SHORT; break; case 2: style = Style.ZONE_ID; break; case 3: style = Style.EXEMPLAR_LOCATION; break; default: style = Style.GENERIC_LOCATION; break; } TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 31: // 'O' - TIMEZONE_LOCALIZED_GMT_OFFSET { Style style = (count < 4) ? Style.LOCALIZED_GMT_SHORT : Style.LOCALIZED_GMT; TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 32: // 'X' - TIMEZONE_ISO { Style style; switch (count) { case 1: style = Style.ISO_BASIC_SHORT; break; case 2: style = Style.ISO_BASIC_FIXED; break; case 3: style = Style.ISO_EXTENDED_FIXED; break; case 4: style = Style.ISO_BASIC_FULL; break; default: // count >= 5 style = Style.ISO_EXTENDED_FULL; break; } TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 33: // 'x' - TIMEZONE_ISO_LOCAL { Style style; switch (count) { case 1: style = Style.ISO_BASIC_LOCAL_SHORT; break; case 2: style = Style.ISO_BASIC_LOCAL_FIXED; break; case 3: style = Style.ISO_EXTENDED_LOCAL_FIXED; break; case 4: style = Style.ISO_BASIC_LOCAL_FULL; break; default: // count >= 5 style = Style.ISO_EXTENDED_LOCAL_FULL; break; } TimeZone tz = tzFormat().parse(style, text, pos, tzTimeType); if (tz != null) { cal.setTimeZone(tz); return pos.getIndex(); } return ~start; } case 27: // 'Q' - QUARTER if (count <= 2 && number != null) { // i.e., Q or QQ. // Don't want to parse the quarter if it is a string // while pattern uses numeric style: Q or QQ. // [We computed 'value' above.] cal.set(Calendar.MONTH, (value - 1) * 3); return pos.getIndex(); } else { // count >= 3 // i.e., QQQ or QQQQ // Want to be able to parse short, long, and narrow forms. // Try count == 4 first: int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.quarters, cal)) > 0) { return newStart; } } // count == 4 failed, now try count == 3 if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.shortQuarters, cal)) > 0) { return newStart; } } // count == 3 failed, now try count == 5 if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 5) { if ((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.narrowQuarters, cal)) > 0) { return newStart; } } // if numeric parsing is on and we got the numeric value already, return it if (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC) && number != null) { cal.set(Calendar.MONTH, (value - 1) * 3); return pos.getIndex(); } return newStart; } case 28: // 'q' - STANDALONE QUARTER if (count <= 2 && number != null) { // i.e., q or qq. // Don't want to parse the quarter if it is a string // while pattern uses numeric style: q or qq. // [We computed 'value' above.] cal.set(Calendar.MONTH, (value - 1) * 3); return pos.getIndex(); } else { // count >= 3 // i.e., qqq or qqqq // Want to be able to parse short, long, and narrow forms. // Try count == 4 first: int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.standaloneQuarters, cal)) > 0) { return newStart; } } // count == 4 failed, now try count == 3 if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.standaloneShortQuarters, cal)) > 0) { return newStart; } } // count == 3 failed, now try count == 5 if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 5) { if ((newStart = matchQuarterString(text, start, Calendar.MONTH, formatData.standaloneNarrowQuarters, cal)) > 0) { return newStart; } } // if numeric parsing is on and we got the numeric value already, return it if (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC) && number != null) { cal.set(Calendar.MONTH, (value - 1) * 3); return pos.getIndex(); } return newStart; } case 37: // TIME SEPARATOR (no pattern character currently defined, we should // not get here but leave support in for future definition. { // Try matching a time separator. ArrayList data = new ArrayList<>(3); data.add(formatData.getTimeSeparatorString()); // Add the default, if different from the locale. if (!formatData.getTimeSeparatorString().equals(DateFormatSymbols.DEFAULT_TIME_SEPARATOR)) { data.add(DateFormatSymbols.DEFAULT_TIME_SEPARATOR); } // If lenient, add also the alternate, if different from the locale. if (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_LITERAL_MATCH) && !formatData.getTimeSeparatorString().equals(DateFormatSymbols.ALTERNATE_TIME_SEPARATOR)) { data.add(DateFormatSymbols.ALTERNATE_TIME_SEPARATOR); } return matchString(text, start, -1 /* => nothing to set */, data.toArray(new String[0]), cal); } case 35: // 'b' -- fixed day period (am/pm/midnight/noon) { int ampmStart = subParse(text, start, 'a', count, obeyCount, allowNegative, ambiguousYear, cal, numericLeapMonthFormatter, tzTimeType, dayPeriod); if (ampmStart > 0) { return ampmStart; } else { int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if ((newStart = matchDayPeriodString( text, start, formatData.abbreviatedDayPeriods, 2, dayPeriod)) > 0) { return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchDayPeriodString( text, start, formatData.wideDayPeriods, 2, dayPeriod)) > 0) { return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchDayPeriodString( text, start, formatData.narrowDayPeriods, 2, dayPeriod)) > 0) { return newStart; } } return newStart; } } case 36: // 'B' -- flexible day period { int newStart = 0; if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 3) { if ((newStart = matchDayPeriodString( text, start, formatData.abbreviatedDayPeriods, formatData.abbreviatedDayPeriods.length, dayPeriod)) > 0) { return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchDayPeriodString( text, start, formatData.wideDayPeriods, formatData.wideDayPeriods.length, dayPeriod)) > 0) { return newStart; } } if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) { if ((newStart = matchDayPeriodString( text, start, formatData.narrowDayPeriods, formatData.narrowDayPeriods.length, dayPeriod)) > 0) { return newStart; } } return newStart; } 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 20: // 'u' - EXTENDED_YEAR // case 21: // 'g' - JULIAN_DAY // case 22: // 'A' - MILLISECONDS_IN_DAY // case 34: // // Handle "generic" fields if (obeyCount) { if ((start+count) > text.length()) return -start; number = parseInt(text, count, pos, allowNegative,currentNumberFormat); } else { number = parseInt(text, pos, allowNegative,currentNumberFormat); } if (obeyCount && !isLenient() && pos.getIndex() < start + count) { return -start; } if (number != null) { if (patternCharIndex != DateFormat.RELATED_YEAR) { cal.set(field, number.intValue()); } else { cal.setRelatedYear(number.intValue()); } return pos.getIndex(); } return ~start; } } /** * return true if the pattern specified by patternCharIndex is one that allows * numeric fallback regardless of actual pattern size. */ private boolean allowNumericFallback(int patternCharIndex) { if (patternCharIndex == 26 /*'L' STAND_ALONE_MONTH*/ || patternCharIndex == 19 /*'e' DOW_LOCAL*/ || patternCharIndex == 25 /*'c' STAND_ALONE_DAY_OF_WEEK*/ || patternCharIndex == 30 /*'U' YEAR_NAME_FIELD*/ || patternCharIndex == 27 /* 'Q' - QUARTER*/ || patternCharIndex == 28 /* 'q' - STANDALONE QUARTER*/) { return true; } return false; } /** * Parse an integer using numberFormat. This method is semantically * const, but actually may modify fNumberFormat. */ private Number parseInt(String text, ParsePosition pos, boolean allowNegative, NumberFormat fmt) { return parseInt(text, -1, pos, allowNegative, fmt); } /** * Parse an integer using numberFormat up to maxDigits. */ private Number parseInt(String text, int maxDigits, ParsePosition pos, boolean allowNegative, NumberFormat fmt) { Number number; int oldPos = pos.getIndex(); if (allowNegative) { number = fmt.parse(text, pos); } else { // Invalidate negative numbers if (fmt instanceof DecimalFormat) { String oldPrefix = ((DecimalFormat)fmt).getNegativePrefix(); ((DecimalFormat)fmt).setNegativePrefix(SUPPRESS_NEGATIVE_PREFIX); number = fmt.parse(text, pos); ((DecimalFormat)fmt).setNegativePrefix(oldPrefix); } else { boolean dateNumberFormat = (fmt instanceof DateNumberFormat); if (dateNumberFormat) { ((DateNumberFormat)fmt).setParsePositiveOnly(true); } number = fmt.parse(text, pos); if (dateNumberFormat) { ((DateNumberFormat)fmt).setParsePositiveOnly(false); } } } if (maxDigits > 0) { // adjust the result to fit into // the maxDigits and move the position back int nDigits = pos.getIndex() - oldPos; if (nDigits > maxDigits) { double val = number.doubleValue(); nDigits -= maxDigits; while (nDigits > 0) { val /= 10; nDigits--; } pos.setIndex(oldPos + maxDigits); number = (int) val; } } return number; } /** * Counts number of digit code points in the specified text. * * @param text input text * @param start start index, inclusive * @param end end index, exclusive * @return number of digits found in the text in the specified range. */ private static int countDigits(String text, int start, int end) { int numDigits = 0; int idx = start; while (idx < end) { int cp = text.codePointAt(idx); if (UCharacter.isDigit(cp)) { numDigits++; } idx += UCharacter.charCount(cp); } return numDigits; } /** * Translate a pattern, mapping each character in the from string to the * corresponding character in the to string. */ private String translatePattern(String pat, String from, String to) { StringBuilder result = new StringBuilder(); boolean inQuote = false; for (int i = 0; i < pat.length(); ++i) { char c = pat.charAt(i); if (inQuote) { if (c == '\'') inQuote = false; } else { if (c == '\'') { inQuote = true; } else if (isSyntaxChar(c)) { int ci = from.indexOf(c); if (ci != -1) { c = to.charAt(ci); } // do not worry on translatepattern if the character is not listed // we do the validity check elsewhere } } 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. *

    * Note: This implementation depends on {@link DateFormatSymbols#getLocalPatternChars()} * to get localized format pattern characters. ICU does not include * localized pattern character data, therefore, unless user sets localized * pattern characters manually, this method returns the same result as * {@link #toPattern()}. * * @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 pat) { this.pattern = pat; parsePattern(); setLocale(null, null); // reset parsed pattern items patternItems = null; // Hack to update use of Gannen year numbering for ja@calendar=japanese - // use only if format is non-numeric (includes 年) and no other fDateOverride. if (calendar != null && calendar.getType().equals("japanese") && locale != null && locale.getLanguage().equals("ja")) { if (override != null && override.equals("y=jpanyear") && !hasHanYearChar) { // Gannen numbering is set but new pattern should not use it, unset; // use procedure from setNumberFormat(NUmberFormat) to clear overrides numberFormatters = null; overrideMap = null; override = null; // record status } else if (override == null && hasHanYearChar) { // No current override (=> no Gannen numbering) but new pattern needs it; // use procedures from initNumberFormatters / setNumberFormat(String,NumberFormat) numberFormatters = new HashMap<>(); overrideMap = new HashMap<>(); overrideMap.put('y',"jpanyear"); ULocale ovrLoc = new ULocale(locale.getBaseName()+"@numbers=jpanyear"); NumberFormat nf = NumberFormat.createInstance(ovrLoc,NumberFormat.NUMBERSTYLE); nf.setGroupingUsed(false); useLocalZeroPaddingNumberFormat = false; numberFormatters.put("jpanyear",nf); override = "y=jpanyear"; // record status } } } /** * Apply the given localized pattern string to this date format. * @stable ICU 2.0 */ public void applyLocalizedPattern(String pat) { this.pattern = translatePattern(pat, 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; } /** * {@icu} Gets the time zone formatter which this date/time * formatter uses to format and parse a time zone. * * @return the time zone formatter which this date/time * formatter uses. * @stable ICU 49 */ public TimeZoneFormat getTimeZoneFormat() { return tzFormat().freeze(); } /** * {@icu} Allows you to set the time zone formatter. * * @param tzfmt the new time zone formatter * @stable ICU 49 */ public void setTimeZoneFormat(TimeZoneFormat tzfmt) { if (tzfmt.isFrozen()) { // If frozen, use it as is. tzFormat = tzfmt; } else { // If not frozen, clone and freeze. tzFormat = tzfmt.cloneAsThawed().freeze(); } } /** * Overrides Cloneable * @stable ICU 2.0 */ @Override public Object clone() { SimpleDateFormat other = (SimpleDateFormat) super.clone(); other.formatData = (DateFormatSymbols) formatData.clone(); // We must create a new copy of work buffer used by // the fast numeric field format code. if (this.decimalBuf != null) { other.decimalBuf = new char[DECIMAL_BUF_SIZE]; } return other; } /** * Override hashCode. * Generates the hash code for the SimpleDateFormat object * @stable ICU 2.0 */ @Override public int hashCode() { return pattern.hashCode(); // just enough fields for a reasonable distribution } /** * Override equals. * @stable ICU 2.0 */ @Override 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 writeObject. * See http://docs.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html */ private void writeObject(ObjectOutputStream stream) throws IOException{ if (defaultCenturyStart == null) { // if defaultCenturyStart is not yet initialized, // calculate and set value before serialization. initializeDefaultCenturyStart(defaultCenturyBase); } initializeTimeZoneFormat(false); stream.defaultWriteObject(); stream.writeInt(getContext(DisplayContext.Type.CAPITALIZATION).value()); } /** * Override readObject. * See http://docs.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); int capitalizationSettingValue = (serialVersionOnStream > 1)? stream.readInt(): -1; ///CLOVER:OFF // don't have old serial data to test with if (serialVersionOnStream < 1) { // didn't have defaultCenturyStart field defaultCenturyBase = System.currentTimeMillis(); } ///CLOVER:ON else { // fill in dependent transient field parseAmbiguousDatesAsAfter(defaultCenturyStart); } serialVersionOnStream = currentSerialVersion; locale = getLocale(ULocale.VALID_LOCALE); if (locale == null) { // ICU4J 3.6 or older versions did not have UFormat locales // in the serialized data. This is just for preventing the // worst case scenario... locale = ULocale.getDefault(Category.FORMAT); } initLocalZeroPaddingNumberFormat(); setContext(DisplayContext.CAPITALIZATION_NONE); if (capitalizationSettingValue >= 0) { for (DisplayContext context: DisplayContext.values()) { if (context.value() == capitalizationSettingValue) { setContext(context); break; } } } // if serialized pre-56 update & turned off partial match switch to new enum value if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_MATCH) == false) { setBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_LITERAL_MATCH, false); } parsePattern(); } /** * Format the object to an attributed string, and return the corresponding iterator * Overrides superclass method. * * @param obj The object to format * @return AttributedCharacterIterator describing the formatted value. * * @stable ICU 3.8 */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object obj) { Calendar cal = calendar; if (obj instanceof Calendar) { cal = (Calendar)obj; } else if (obj instanceof Date) { calendar.setTime((Date)obj); } else if (obj instanceof Number) { calendar.setTimeInMillis(((Number)obj).longValue()); } else if (obj instanceof Temporal) { cal = JavaTimeConverters.temporalToCalendar((Temporal) obj); } else { throw new IllegalArgumentException("Cannot format given Object as a Date"); } StringBuffer toAppendTo = new StringBuffer(); FieldPosition pos = new FieldPosition(0); List attributes = new ArrayList<>(); format(cal, getContext(DisplayContext.Type.CAPITALIZATION), toAppendTo, pos, attributes); AttributedString as = new AttributedString(toAppendTo.toString()); // add DateFormat field attributes to the AttributedString for (int i = 0; i < attributes.size(); i++) { FieldPosition fp = attributes.get(i); Format.Field attribute = fp.getFieldAttribute(); as.addAttribute(attribute, attribute, fp.getBeginIndex(), fp.getEndIndex()); } // return the CharacterIterator from AttributedString return as.getIterator(); } /** * Get the locale of this simple date formatter. * It is package accessible. also used in DateIntervalFormat. * * @return locale in this simple date formatter */ ULocale getLocale() { return locale; } /** * Check whether the 'field' is smaller than all the fields covered in * pattern, return true if it is. * The sequence of calendar field, * from large to small is: ERA, YEAR, MONTH, DATE, AM_PM, HOUR, MINUTE,... * @param field the calendar field need to check against * @return true if the 'field' is smaller than all the fields * covered in pattern. false otherwise. */ boolean isFieldUnitIgnored(int field) { return isFieldUnitIgnored(pattern, field); } /* * Check whether the 'field' is smaller than all the fields covered in * pattern, return true if it is. * The sequence of calendar field, * from large to small is: ERA, YEAR, MONTH, DATE, AM_PM, HOUR, MINUTE,... * @param pattern the pattern to check against * @param field the calendar field need to check against * @return true if the 'field' is smaller than all the fields * covered in pattern. false otherwise. */ static boolean isFieldUnitIgnored(String pattern, int field) { int fieldLevel = CALENDAR_FIELD_TO_LEVEL[field]; int level; char ch; boolean inQuote = false; char prevCh = 0; int count = 0; for (int i = 0; i < pattern.length(); ++i) { ch = pattern.charAt(i); if (ch != prevCh && count > 0) { level = getLevelFromChar(prevCh); if (fieldLevel <= level) { return false; } count = 0; } if (ch == '\'') { if ((i+1) < pattern.length() && pattern.charAt(i+1) == '\'') { ++i; } else { inQuote = ! inQuote; } } else if (!inQuote && isSyntaxChar(ch)) { prevCh = ch; ++count; } } if (count > 0) { // last item level = getLevelFromChar(prevCh); if (fieldLevel <= level) { return false; } } return true; } /** * Format date interval by algorithm. * It is supposed to be used only by CLDR survey tool. * * @param fromCalendar calendar set to the from date in date interval * to be formatted into date interval string * @param toCalendar calendar set to the to date in date interval * to be formatted into date interval string * @param appendTo Output parameter to receive result. * Result is appended to existing contents. * @param pos On input: an alignment field, if desired. * On output: the offsets of the alignment field. * @exception IllegalArgumentException when there is non-recognized * pattern letter * @return Reference to 'appendTo' parameter. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final StringBuffer intervalFormatByAlgorithm(Calendar fromCalendar, Calendar toCalendar, StringBuffer appendTo, FieldPosition pos) throws IllegalArgumentException { // not support different calendar types and time zones if ( !fromCalendar.isEquivalentTo(toCalendar) ) { throw new IllegalArgumentException("can not format on two different calendars"); } Object[] items = getPatternItems(); int diffBegin = -1; int diffEnd = -1; /* look for different formatting string range */ // look for start of difference try { for (int i = 0; i < items.length; i++) { if ( diffCalFieldValue(fromCalendar, toCalendar, items, i) ) { diffBegin = i; break; } } if ( diffBegin == -1 ) { // no difference, single date format return format(fromCalendar, appendTo, pos); } // look for end of difference for (int i = items.length-1; i >= diffBegin; i--) { if ( diffCalFieldValue(fromCalendar, toCalendar, items, i) ) { diffEnd = i; break; } } } catch ( IllegalArgumentException e ) { throw new IllegalArgumentException(e.toString()); } // full range is different if ( diffBegin == 0 && diffEnd == items.length-1 ) { format(fromCalendar, appendTo, pos); appendTo.append(" \u2013 "); // default separator format(toCalendar, appendTo, pos); return appendTo; } /* search for largest calendar field within the different range */ int highestLevel = 1000; for (int i = diffBegin; i <= diffEnd; i++) { if ( items[i] instanceof String) { continue; } PatternItem item = (PatternItem)items[i]; char ch = item.type; int patternCharIndex = getIndexFromChar(ch); if (patternCharIndex == -1) { throw new IllegalArgumentException("Illegal pattern character " + "'" + ch + "' in \"" + pattern + '"'); } if ( patternCharIndex < highestLevel ) { highestLevel = patternCharIndex; } } /* re-calculate diff range, including those calendar field which is in lower level than the largest calendar field covered in diff range calculated. */ try { for (int i = 0; i < diffBegin; i++) { if ( lowerLevel(items, i, highestLevel) ) { diffBegin = i; break; } } for (int i = items.length-1; i > diffEnd; i--) { if ( lowerLevel(items, i, highestLevel) ) { diffEnd = i; break; } } } catch ( IllegalArgumentException e ) { throw new IllegalArgumentException(e.toString()); } // full range is different if ( diffBegin == 0 && diffEnd == items.length-1 ) { format(fromCalendar, appendTo, pos); appendTo.append(" \u2013 "); // default separator format(toCalendar, appendTo, pos); return appendTo; } // formatting // Initialize pos.setBeginIndex(0); pos.setEndIndex(0); DisplayContext capSetting = getContext(DisplayContext.Type.CAPITALIZATION); // formatting date 1 for (int i = 0; i <= diffEnd; i++) { if (items[i] instanceof String) { appendTo.append((String)items[i]); } else { PatternItem item = (PatternItem)items[i]; if (useFastFormat) { subFormat(appendTo, item.type, item.length, appendTo.length(), i, capSetting, pos, item.type, fromCalendar); } else { appendTo.append(subFormat(item.type, item.length, appendTo.length(), i, capSetting, pos, item.type, fromCalendar)); } } } appendTo.append(" \u2013 "); // default separator // formatting date 2 for (int i = diffBegin; i < items.length; i++) { if (items[i] instanceof String) { appendTo.append((String)items[i]); } else { PatternItem item = (PatternItem)items[i]; if (useFastFormat) { subFormat(appendTo, item.type, item.length, appendTo.length(), i, capSetting, pos, item.type, toCalendar); } else { appendTo.append(subFormat(item.type, item.length, appendTo.length(), i, capSetting, pos, item.type, toCalendar)); } } } return appendTo; } /** * check whether the i-th item in 2 calendar is in different value. * * It is supposed to be used only by CLDR survey tool. * It is used by intervalFormatByAlgorithm(). * * @param fromCalendar one calendar * @param toCalendar the other calendar * @param items pattern items * @param i the i-th item in pattern items * @exception IllegalArgumentException when there is non-recognized * pattern letter * @return true is i-th item in 2 calendar is in different * value, false otherwise. */ private boolean diffCalFieldValue(Calendar fromCalendar, Calendar toCalendar, Object[] items, int i) throws IllegalArgumentException { if ( items[i] instanceof String) { return false; } PatternItem item = (PatternItem)items[i]; char ch = item.type; int patternCharIndex = getIndexFromChar(ch); if (patternCharIndex == -1) { throw new IllegalArgumentException("Illegal pattern character " + "'" + ch + "' in \"" + pattern + '"'); } final int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; if (field >= 0) { int value = fromCalendar.get(field); int value_2 = toCalendar.get(field); if ( value != value_2 ) { return true; } } return false; } /** * check whether the i-th item's level is lower than the input 'level' * * It is supposed to be used only by CLDR survey tool. * It is used by intervalFormatByAlgorithm(). * * @param items the pattern items * @param i the i-th item in pattern items * @param level the level with which the i-th pattern item compared to * @exception IllegalArgumentException when there is non-recognized * pattern letter * @return true if i-th pattern item is lower than 'level', * false otherwise */ private boolean lowerLevel(Object[] items, int i, int level) throws IllegalArgumentException { if (items[i] instanceof String) { return false; } PatternItem item = (PatternItem)items[i]; char ch = item.type; int patternCharIndex = getLevelFromChar(ch); if (patternCharIndex == -1) { throw new IllegalArgumentException("Illegal pattern character " + "'" + ch + "' in \"" + pattern + '"'); } if (patternCharIndex >= level) { return true; } return false; } /** * allow the user to set the NumberFormat for several fields * It can be a single field like: "y"(year) or "M"(month) * It can be several field combined together: "yMd"(year, month and date) * Note: * 1 symbol field is enough for multiple symbol fields (so "y" will override "yy", "yyy") * If the field is not numeric, then override has no effect (like "MMM" will use abbreviation, not numerical field) * * @param fields the fields to override * @param overrideNF the NumbeferFormat used * @exception IllegalArgumentException when the fields contain invalid field * @stable ICU 54 */ public void setNumberFormat(String fields, NumberFormat overrideNF) { overrideNF.setGroupingUsed(false); String nsName = "$" + UUID.randomUUID().toString(); // initialize mapping if not there if (numberFormatters == null) { numberFormatters = new HashMap<>(); } if (overrideMap == null) { overrideMap = new HashMap<>(); } // separate string into char and add to maps for (int i = 0; i < fields.length(); i++) { char field = fields.charAt(i); if (DateFormatSymbols.patternChars.indexOf(field) == -1) { throw new IllegalArgumentException("Illegal field character " + "'" + field + "' in setNumberFormat."); } overrideMap.put(field, nsName); numberFormatters.put(nsName, overrideNF); } // Since one or more of the override number formatters might be complex, // we can't rely on the fast numfmt where we have a partial field override. useLocalZeroPaddingNumberFormat = false; } /** * give the NumberFormat used for the field like 'y'(year) and 'M'(year) * * @param field the field the user wants * @return override NumberFormat used for the field * @stable ICU 54 */ public NumberFormat getNumberFormat(char field) { Character ovrField = field; if (overrideMap != null && overrideMap.containsKey(ovrField)) { String nsName = overrideMap.get(ovrField).toString(); NumberFormat nf = numberFormatters.get(nsName); return nf; } else { return numberFormat; } } private void initNumberFormatters(ULocale loc) { numberFormatters = new HashMap<>(); overrideMap = new HashMap<>(); processOverrideString(loc,override); } private void processOverrideString(ULocale loc, String str) { if ( str == null || str.length() == 0 ) return; int start = 0; int end; String nsName; Character ovrField; boolean moreToProcess = true; boolean fullOverride; while (moreToProcess) { int delimiterPosition = str.indexOf(";",start); if (delimiterPosition == -1) { moreToProcess = false; end = str.length(); } else { end = delimiterPosition; } String currentString = str.substring(start,end); int equalSignPosition = currentString.indexOf("="); if (equalSignPosition == -1) { // Simple override string such as "hebrew" nsName = currentString; fullOverride = true; } else { // Field specific override string such as "y=hebrew" nsName = currentString.substring(equalSignPosition+1); ovrField = currentString.charAt(0); overrideMap.put(ovrField,nsName); fullOverride = false; } ULocale ovrLoc = new ULocale(loc.getBaseName()+"@numbers="+nsName); NumberFormat nf = NumberFormat.createInstance(ovrLoc,NumberFormat.NUMBERSTYLE); nf.setGroupingUsed(false); if (fullOverride) { setNumberFormat(nf); } else { // Since one or more of the override number formatters might be complex, // we can't rely on the fast numfmt where we have a partial field override. useLocalZeroPaddingNumberFormat = false; } if (!fullOverride && !numberFormatters.containsKey(nsName)) { numberFormatters.put(nsName,nf); } start = delimiterPosition + 1; } } private void parsePattern() { hasMinute = false; hasSecond = false; hasHanYearChar = false; boolean inQuote = false; for (int i = 0; i < pattern.length(); ++i) { char ch = pattern.charAt(i); if (ch == '\'') { inQuote = !inQuote; } if (ch == '\u5E74') { // don't care whether this is inside quotes hasHanYearChar = true; } if (!inQuote) { if (ch == 'm') { hasMinute = true; } if (ch == 's') { hasSecond = true; } } } } }