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

There is a newer version: 76.1
Show 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.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.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. * *

* SimpleDateFormat allows you to start by choosing * any user-defined patterns for date-time formatting. However, you * are encouraged to create a date-time formatter with either * getTimeInstance, getDateInstance, or * getDateTimeInstance in DateFormat. Each * of these class methods can return a date/time formatter initialized * with a default format pattern. You may modify the format pattern * using the applyPattern methods as desired. * 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. * Within date and time pattern strings, all unquoted ASCII letters [A-Za-z] are reserved * as pattern letters representing calendar fields. SimpleDateFormat supports * the date and time formatting algorithm and pattern letters defined by UTS#35 * Unicode Locale Data Markup Language (LDML). The following pattern letters are * currently available (note that the actual values depend on CLDR and may change from the * examples shown here):

*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
FieldSym.No.ExampleDescription
eraG1..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.
4Anno Domini
5A
yeary1..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
*
*
Y1..n1997Year (in "Week of Year" based calendars). Normally the length specifies the padding, * but for two letters it also specifies the maximum length. This year designation is used in ISO * year-week calendar as defined by ISO 8601, but can be used in non-Gregorian based calendar systems * where week date processing is desired. May not always be the same value as calendar year.
u1..n4601Extended year. This is a single number designating the year of this calendar system, encompassing * all supra-year fields. For example, for the Julian calendar system, year numbers are positive, with an * era of BCE or CE. An extended year value for the Julian calendar system assigns positive values to CE * years and negative values to BCE years, with 1 BCE being year 0.
U1..3甲子Cyclic year name. Calendars such as the Chinese lunar calendar (and related calendars) * and the Hindu calendars use 60-year cycles of year names. Use one through three letters for the abbreviated * name, four for the full (wide) name, or five for the narrow name (currently the data only provides abbreviated names, * which will be used for all requested name widths). If the calendar does not provide cyclic year name data, * or if the year value to be formatted is out of the range of years for which cyclic name data is provided, * then numeric formatting is used (behaves like 'y').
4(currently also 甲子)
5(currently also 甲子)
quarterQ1..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
q1..202Stand-Alone Quarter - Use one or two for the numerical quarter, three for the abbreviation, * or four for the full name (five for the narrow name is not yet supported).
3Q2
42nd quarter
monthM1..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
L1..209Stand-Alone Month - Use one or two for the numerical month, three for the abbreviation, * four for the full (wide) name, or 5 for the narrow name. With two ("LL"), the month number is zero-padded if * necessary (e.g. "08").
3Sep
4September
5S
weekw1..227Week of Year. Use "w" to show the minimum number of digits, or "ww" to always show two digits * (zero-padding if necessary, e.g. "08").
W13Week of Month
dayd1..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").
D1..3345Day of year
F12Day of Week in Month. The example is for the 2nd Wed in July
g1..n2451334Modified Julian day. This is different from the conventional Julian day number in two regards. * First, it demarcates days at local zone midnight, rather than noon GMT. Second, it is a local number; * that is, it depends on the local time zone. It can be thought of as a single number that encompasses * all the date-related fields.
week
* day
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
e1..22Local day of week. Same as E except adds a numeric value that will depend on the local * starting day of the week, using one or two letters. For this example, Monday is the first day of the week.
3Tue
4Tuesday
5T
6Tu
c12Stand-Alone local day of week - Use one letter for the local numeric value (same * as 'e'), three for the short day, four for the full (wide) name, five for the narrow name, or six for * the short name.
3Tue
4Tuesday
5T
6Tu
perioda1AMAM or PM
hourh1..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.
K1..20Hour [0-11]. When used in a skeleton, only matches K or h, see above. Use KK for zero padding.
k1..224Hour [1-24]. When used in a skeleton, only matches k or H, see above. Use kk for zero padding.
minutem1..259Minute. Use "m" to show the minimum number of digits, or "mm" to always show two digits * (zero-padding if necessary, e.g. "08")..
seconds1..212Second. Use "s" to show the minimum number of digits, or "ss" to always show two digits * (zero-padding if necessary, e.g. "08").
S1..n3450Fractional Second - truncates (like other time fields) to the count of letters when formatting. Appends zeros if more than 3 letters specified. Truncates at three significant digits when parsing. * (example shows display using pattern SSSS for seconds value 12.34567)
A1..n69540000Milliseconds in day. This field behaves exactly like a composite of all time-related fields, * not including the zone fields. As such, it also reflects discontinuities of those fields on DST transition * days. On a day of DST onset, it will jump forward. On a day of DST cessation, it will jump backward. This * reflects the fact that is must be combined with the offset field to obtain a unique local time value.
zonez1..3PDTThe 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").
Z1..3-0800The ISO8601 basic format with hours, minutes and optional seconds fields. * The format is equivalent to RFC 822 zone format (when optional seconds field is absent). * This is equivalent to the "xxxx" specifier.
4GMT-8:00The long localized GMT format. * This is equivalent to the "OOOO" specifier.
5-08:00
* -07:52:58
The ISO8601 extended format with hours, minutes and optional seconds fields. * The ISO8601 UTC indicator "Z" is used when local time offset is 0. * This is equivalent to the "XXXXX" specifier.
O1GMT-8The short localized GMT format.
4GMT-08:00The long localized GMT format.
v1PTThe 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"). *
V1uslaxThe short time zone ID. * Where that is unavailable, the special short time zone ID unk (Unknown Zone) is used.
* Note: This specifier was originally used for a variant of the short specific non-location format, * but it was deprecated in the later version of the LDML specification. In CLDR 23/ICU 51, the definition of * the specifier was changed to designate a short time zone ID.
2America/Los_AngelesThe long time zone ID.
3Los AngelesThe exemplar city (location) for the time zone. * Where that is unavailable, the localized exemplar city name for the special zone Etc/Unknown is used * as the fallback (for example, "Unknown City").
4Los Angeles TimeThe generic location format. * Where that is unavailable, falls back to the long localized GMT format ("OOOO"; * Note: Fallback is only necessary with a GMT-style Time Zone ID, like Etc/GMT-830.)
* This is especially useful when presenting possible timezone choices for user selection, * since the naming is more uniform than the "v" format.
X1-08
* +0530
* Z
The ISO8601 basic format with hours field and optional minutes field. * The ISO8601 UTC indicator "Z" is used when local time offset is 0.
2-0800
* Z
The ISO8601 basic format with hours and minutes fields. * The ISO8601 UTC indicator "Z" is used when local time offset is 0.
3-08:00
* Z
The ISO8601 extended format with hours and minutes fields. * The ISO8601 UTC indicator "Z" is used when local time offset is 0.
4-0800
* -075258
* Z
The ISO8601 basic format with hours, minutes and optional seconds fields. * (Note: The seconds field is not supported by the ISO8601 specification.) * The ISO8601 UTC indicator "Z" is used when local time offset is 0.
5-08:00
* -07:52:58
* Z
The ISO8601 extended format with hours, minutes and optional seconds fields. * (Note: The seconds field is not supported by the ISO8601 specification.) * The ISO8601 UTC indicator "Z" is used when local time offset is 0.
x1-08
* +0530
The ISO8601 basic format with hours field and optional minutes field.
2-0800The ISO8601 basic format with hours and minutes fields.
3-08:00The ISO8601 extended format with hours and minutes fields.
4-0800
* -075258
The ISO8601 basic format with hours, minutes and optional seconds fields. * (Note: The seconds field is not supported by the ISO8601 specification.)
5-08:00
* -07:52:58
The ISO8601 extended format with hours, minutes and optional seconds fields. * (Note: The seconds field is not supported by the ISO8601 specification.)
* *
*

* 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 = Integer.valueOf((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 { 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; ovrField = Character.valueOf(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 = Character.valueOf(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; } } } } }