diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-28 17:54:57 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-28 17:54:57 +0000 |
commit | eb3451793aaf42870e44281708ccac51c010e837 (patch) | |
tree | 51c5f3646653b5153a74a2828b2af20c41cbd8d1 /icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java | |
parent | e710c4fbd23e1b7d97da0a88a8499326861ad250 (diff) | |
parent | 8144ba71b4efcfe46cd0e76e85d371bcc7d55567 (diff) | |
download | icu-eb3451793aaf42870e44281708ccac51c010e837.tar.gz |
Snap for 11641499 from 8144ba71b4efcfe46cd0e76e85d371bcc7d55567 to build-tools-release
Change-Id: Ic926bb7ff166e37b678ae2768581ebec18fcea4a
Diffstat (limited to 'icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java')
-rw-r--r-- | icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java | 2338 |
1 files changed, 2338 insertions, 0 deletions
diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java b/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java new file mode 100644 index 000000000..eb24241af --- /dev/null +++ b/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java @@ -0,0 +1,2338 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* +* Copyright (C) 2008-2016, International Business Machines +* Corporation and others. All Rights Reserved. +*/ + +package com.ibm.icu.text; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import com.ibm.icu.impl.FormattedValueFieldPositionIteratorImpl; +import com.ibm.icu.impl.ICUCache; +import com.ibm.icu.impl.ICUData; +import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.SimpleCache; +import com.ibm.icu.impl.SimpleFormatterImpl; +import com.ibm.icu.impl.Utility; +import com.ibm.icu.text.DateIntervalInfo.PatternInfo; +import com.ibm.icu.util.Calendar; +import com.ibm.icu.util.DateInterval; +import com.ibm.icu.util.Output; +import com.ibm.icu.util.TimeZone; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.ULocale.Category; +import com.ibm.icu.util.UResourceBundle; + + +/** + * DateIntervalFormat is a class for formatting and parsing date + * intervals in a language-independent manner. + * Only formatting is supported. Parsing is not supported. + * + * <P> + * Date interval means from one date to another date, + * for example, from "Jan 11, 2008" to "Jan 18, 2008". + * We introduced class DateInterval to represent it. + * DateInterval is a pair of UDate, which is + * the standard milliseconds since 24:00 GMT, Jan 1, 1970. + * + * <P> + * DateIntervalFormat formats a DateInterval into + * text as compactly as possible. + * For example, the date interval format from "Jan 11, 2008" to "Jan 18,. 2008" + * is "Jan 11-18, 2008" for English. + * And it parses text into DateInterval, + * although initially, parsing is not supported. + * + * <P> + * There is no structural information in date time patterns. + * For any punctuations and string literals inside a date time pattern, + * we do not know whether it is just a separator, or a prefix, or a suffix. + * Without such information, so, it is difficult to generate a sub-pattern + * (or super-pattern) by algorithm. + * So, formatting a DateInterval is pattern-driven. It is very + * similar to formatting in SimpleDateFormat. + * We introduce class DateIntervalInfo to save date interval + * patterns, similar to date time pattern in SimpleDateFormat. + * + * <P> + * Logically, the interval patterns are mappings + * from (skeleton, the_largest_different_calendar_field) + * to (date_interval_pattern). + * + * <P> + * A skeleton + * <ol> + * <li> + * only keeps the field pattern letter and ignores all other parts + * in a pattern, such as space, punctuations, and string literals. + * <li> + * hides the order of fields. + * <li> + * might hide a field's pattern letter length. + * + * For those non-digit calendar fields, the pattern letter length is + * important, such as MMM, MMMM, and MMMMM; EEE and EEEE, + * and the field's pattern letter length is honored. + * + * For the digit calendar fields, such as M or MM, d or dd, yy or yyyy, + * the field pattern length is ignored and the best match, which is defined + * in date time patterns, will be returned without honor the field pattern + * letter length in skeleton. + * </ol> + * + * <P> + * The calendar fields we support for interval formatting are: + * year, month, date, day-of-week, am-pm, hour, hour-of-day, minute, and + * second (though we do not currently have specific intervalFormat data for + * skeletons with seconds). + * Those calendar fields can be defined in the following order: + * year > month > date > hour (in day) > minute > second + * + * The largest different calendar fields between 2 calendars is the + * first different calendar field in above order. + * + * For example: the largest different calendar fields between "Jan 10, 2007" + * and "Feb 20, 2008" is year. + * + * <P> + * For other calendar fields, the compact interval formatting is not + * supported. And the interval format will be fall back to fall-back + * patterns, which is mostly "{date0} - {date1}". + * + * <P> + * There is a set of pre-defined static skeleton strings in DateFormat, + * There are pre-defined interval patterns for those pre-defined skeletons + * in locales' resource files. + * For example, for a skeleton YEAR_ABBR_MONTH_DAY, which is "yMMMd", + * in en_US, if the largest different calendar field between date1 and date2 + * is "year", the date interval pattern is "MMM d, yyyy - MMM d, yyyy", + * such as "Jan 10, 2007 - Jan 10, 2008". + * If the largest different calendar field between date1 and date2 is "month", + * the date interval pattern is "MMM d - MMM d, yyyy", + * such as "Jan 10 - Feb 10, 2007". + * If the largest different calendar field between date1 and date2 is "day", + * the date interval pattern is ""MMM d-d, yyyy", such as "Jan 10-20, 2007". + * + * For date skeleton, the interval patterns when year, or month, or date is + * different are defined in resource files. + * For time skeleton, the interval patterns when am/pm, or hour, or minute is + * different are defined in resource files. + * + * <P> + * If a skeleton is not found in a locale's DateIntervalInfo, which means + * the interval patterns for the skeleton is not defined in resource file, + * the interval pattern will falls back to the interval "fallback" pattern + * defined in resource file. + * If the interval "fallback" pattern is not defined, the default fall-back + * is "{date0} - {data1}". + * + * <P> + * For the combination of date and time, + * The rule to genearte interval patterns are: + * <ol> + * <li> + * when the year, month, or day differs, falls back to fall-back + * interval pattern, which mostly is the concatenate the two original + * expressions with a separator between, + * For example, interval pattern from "Jan 10, 2007 10:10 am" + * to "Jan 11, 2007 10:10am" is + * "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am" + * <li> + * otherwise, present the date followed by the range expression + * for the time. + * For example, interval pattern from "Jan 10, 2007 10:10 am" + * to "Jan 10, 2007 11:10am" is "Jan 10, 2007 10:10 am - 11:10am" + * </ol> + * + * + * <P> + * If two dates are the same, the interval pattern is the single date pattern. + * For example, interval pattern from "Jan 10, 2007" to "Jan 10, 2007" is + * "Jan 10, 2007". + * + * Or if the presenting fields between 2 dates have the exact same values, + * the interval pattern is the single date pattern. + * For example, if user only requests year and month, + * the interval pattern from "Jan 10, 2007" to "Jan 20, 2007" is "Jan 2007". + * + * <P> + * DateIntervalFormat needs the following information for correct + * formatting: time zone, calendar type, pattern, date format symbols, + * and date interval patterns. + * It can be instantiated in several ways: + * <ol> + * <li> + * create an instance using default or given locale plus given skeleton. + * Users are encouraged to created date interval formatter this way and + * to use the pre-defined skeleton macros, such as + * YEAR_NUM_MONTH, which consists the calendar fields and + * the format style. + * </li> + * <li> + * create an instance using default or given locale plus given skeleton + * plus a given DateIntervalInfo. + * This factory method is for powerful users who want to provide their own + * interval patterns. + * Locale provides the timezone, calendar, and format symbols information. + * Local plus skeleton provides full pattern information. + * DateIntervalInfo provides the date interval patterns. + * </li> + * </ol> + * + * <P> + * For the calendar field pattern letter, such as G, y, M, d, a, h, H, m, s etc. + * DateIntervalFormat uses the same syntax as that of + * DateTime format. + * + * <P> + * Code Sample: general usage + * <pre> + * + * // the date interval object which the DateIntervalFormat formats on + * // and parses into + * DateInterval dtInterval = new DateInterval(1000*3600*24L, 1000*3600*24*2L); + * DateIntervalFormat dtIntervalFmt = DateIntervalFormat.getInstance( + * DateFormat.YEAR_MONTH_DAY, new Locale("en", "GB", "")); + * StringBuffer result = new StringBuffer(""); + * FieldPosition pos = new FieldPosition(-1); + * // formatting + * dtIntervalFmt.format(dtInterval, result, pos); + * assertEquals("interval", "1–2 January 1970", result.toString()); + * + * </pre> + * + * <P> + * Code Sample: for powerful users who wants to use their own interval pattern + * <pre> + * + * import com.ibm.icu.text.DateIntervalInfo; + * import com.ibm.icu.text.DateIntervalFormat; + * .................... + * + * // Get DateIntervalFormat instance using default locale + * DateIntervalFormat dtitvfmt = DateIntervalFormat.getInstance(YEAR_MONTH_DAY); + * + * // Create an empty DateIntervalInfo object, which does not have any interval patterns inside. + * dtitvinf = new DateIntervalInfo(); + * + * // a series of set interval patterns. + * // Only ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, + * MINUTE, SECOND and MILLISECOND are supported. + * dtitvinf.setIntervalPattern("yMMMd", Calendar.YEAR, "'y ~ y'"); + * dtitvinf.setIntervalPattern("yMMMd", Calendar.MONTH, "yyyy 'diff' MMM d - MMM d"); + * dtitvinf.setIntervalPattern("yMMMd", Calendar.DATE, "yyyy MMM d ~ d"); + * dtitvinf.setIntervalPattern("yMMMd", Calendar.HOUR_OF_DAY, "yyyy MMM d HH:mm ~ HH:mm"); + * + * // Set fallback interval pattern. Fallback pattern is used when interval pattern is not found. + * // If the fall-back pattern is not set, falls back to {date0} - {date1} if interval pattern is not found. + * dtitvinf.setFallbackIntervalPattern("{0} - {1}"); + * + * // Set above DateIntervalInfo object as the interval patterns of date interval formatter + * dtitvfmt.setDateIntervalInfo(dtitvinf); + * + * // Prepare to format + * pos = new FieldPosition(0); + * str = new StringBuffer(""); + * + * // The 2 calendars should be equivalent, otherwise, IllegalArgumentException will be thrown by format() + * Calendar fromCalendar = (Calendar) dtfmt.getCalendar().clone(); + * Calendar toCalendar = (Calendar) dtfmt.getCalendar().clone(); + * fromCalendar.setTimeInMillis(....); + * toCalendar.setTimeInMillis(...); + * + * //Formatting given 2 calendars + * dtitvfmt.format(fromCalendar, toCalendar, str, pos); + * + * + * </pre> + * <h3>Synchronization</h3> + * + * The format methods of DateIntervalFormat may be used concurrently from multiple threads. + * Functions that alter the state of a DateIntervalFormat object (setters) + * may not be used concurrently with any other functions. + * + * @stable ICU 4.0 + */ + +public class DateIntervalFormat extends UFormat { + + /** + * An immutable class containing the result of a date interval formatting operation. + * + * Instances of this class are immutable and thread-safe. + * + * Not intended for public subclassing. + * + * @stable ICU 64 + */ + public static final class FormattedDateInterval implements FormattedValue { + private final String string; + private final List<FieldPosition> attributes; + + FormattedDateInterval(CharSequence cs, List<FieldPosition> attributes) { + this.string = cs.toString(); + this.attributes = Collections.unmodifiableList(attributes); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public String toString() { + return string; + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public int length() { + return string.length(); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public char charAt(int index) { + return string.charAt(index); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public CharSequence subSequence(int start, int end) { + return string.subSequence(start, end); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public <A extends Appendable> A appendTo(A appendable) { + return Utility.appendTo(string, appendable); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + return FormattedValueFieldPositionIteratorImpl.nextPosition(attributes, cfpos); + } + + /** + * {@inheritDoc} + * @stable ICU 64 + */ + @Override + public AttributedCharacterIterator toCharacterIterator() { + return FormattedValueFieldPositionIteratorImpl.toCharacterIterator(string, attributes); + } + } + + /** + * Class for span fields in FormattedDateInterval. + * + * @stable ICU 64 + */ + public static final class SpanField extends UFormat.SpanField { + private static final long serialVersionUID = -6330879259553618133L; + + /** + * The concrete field used for spans in FormattedDateInterval. + * + * Instances of DATE_INTERVAL_SPAN should have an associated value. If + * 0, the date fields within the span are for the "from" date; if 1, + * the date fields within the span are for the "to" date. + * + * @stable ICU 64 + */ + public static final SpanField DATE_INTERVAL_SPAN = new SpanField("date-interval-span"); + + private SpanField(String name) { + super(name); + } + + /** + * serialization method resolve instances to the constant + * DateIntervalFormat.SpanField values + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(DATE_INTERVAL_SPAN.getName())) + return DATE_INTERVAL_SPAN; + + throw new InvalidObjectException("An invalid object."); + } + } + + private static final long serialVersionUID = 1; + + /** + * Used to save the information for a skeleton's best match skeleton. + * It is package accessible since it is used in DateIntervalInfo too. + */ + static final class BestMatchInfo { + // the best match skeleton + final String bestMatchSkeleton; + // 0 means the best matched skeleton is the same as input skeleton + // 1 means the fields are the same, but field width are different + // 2 means the only difference between fields are v/z, + // -1 means there are other fields difference + final int bestMatchDistanceInfo; + BestMatchInfo(String bestSkeleton, int difference) { + bestMatchSkeleton = bestSkeleton; + bestMatchDistanceInfo = difference; + } + } + + + /* + * Used to save the information on a skeleton and its best match. + */ + private static final class SkeletonAndItsBestMatch { + final String skeleton; + final String bestMatchSkeleton; + SkeletonAndItsBestMatch(String skeleton, String bestMatch) { + this.skeleton = skeleton; + bestMatchSkeleton = bestMatch; + } + } + + /** Used to output information during formatting. */ + private static final class FormatOutput { + int firstIndex = -1; + + public void register(int i) { + if (firstIndex == -1) { + firstIndex = i; + } + } + } + + + // Cache for the locale interval pattern + private static ICUCache<String, Map<String, PatternInfo>> LOCAL_PATTERN_CACHE = + new SimpleCache<>(); + + /* + * The interval patterns for this locale. + */ + private DateIntervalInfo fInfo; + + /* + * The DateFormat object used to format single pattern. + * Because fDateFormat is modified during format operations, all + * access to it from logically const, thread safe functions must be synchronized. + */ + private SimpleDateFormat fDateFormat; + + /* + * The 2 calendars with the from and to date. + * could re-use the calendar in fDateFormat, + * but keeping 2 calendars make it clear and clean. + * Because these Calendars are modified during format operations, all + * access to them from logically const, thread safe functions must be synchronized. + */ + private Calendar fFromCalendar; + private Calendar fToCalendar; + + /* + * Following are transient interval information + * relevant (locale) to this formatter. + */ + private String fSkeleton = null; + + /* + * Needed for efficient deserialization. If set, it means we can use the + * cache to initialize fIntervalPatterns. + */ + private boolean isDateIntervalInfoDefault; + + /** + * Interval patterns for this instance's locale. + */ + private transient Map<String, PatternInfo> fIntervalPatterns = null; + + /* + * Patterns for fallback formatting. + */ + private String fDatePattern = null; + private String fTimePattern = null; + private String fDateTimeFormat = null; + + /* + * Capitalization context, new in ICU 68 + */ + private DisplayContext fCapitalizationSetting = DisplayContext.CAPITALIZATION_NONE; + + /* + * default constructor; private because we don't want anyone to use + */ + @SuppressWarnings("unused") + private DateIntervalFormat() { + } + + /** + * Construct a DateIntervalFormat from DateFormat, + * a DateIntervalInfo, and skeleton. + * DateFormat provides the timezone, calendar, + * full pattern, and date format symbols information. + * It should be a SimpleDateFormat object which + * has a pattern in it. + * the DateIntervalInfo provides the interval patterns. + * + * @param skeleton the skeleton of the date formatter + * @param dtItvInfo the DateIntervalInfo object to be adopted. + * @param simpleDateFormat will be used for formatting + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public DateIntervalFormat(String skeleton, DateIntervalInfo dtItvInfo, + SimpleDateFormat simpleDateFormat) + { + fDateFormat = simpleDateFormat; + // freeze date interval info + dtItvInfo.freeze(); + fSkeleton = skeleton; + fInfo = dtItvInfo; + isDateIntervalInfoDefault = false; + fFromCalendar = (Calendar) fDateFormat.getCalendar().clone(); + fToCalendar = (Calendar) fDateFormat.getCalendar().clone(); + initializePattern(null); + } + + private DateIntervalFormat(String skeleton, ULocale locale, + SimpleDateFormat simpleDateFormat) + { + fDateFormat = simpleDateFormat; + fSkeleton = skeleton; + fInfo = new DateIntervalInfo(locale).freeze(); + isDateIntervalInfoDefault = true; + fFromCalendar = (Calendar) fDateFormat.getCalendar().clone(); + fToCalendar = (Calendar) fDateFormat.getCalendar().clone(); + initializePattern(LOCAL_PATTERN_CACHE); +} + + + /** + * Construct a DateIntervalFormat from skeleton and the default <code>FORMAT</code> locale. + * + * This is a convenient override of + * getInstance(String skeleton, ULocale locale) + * with the value of locale as default <code>FORMAT</code> locale. + * + * @param skeleton the skeleton on which interval format based. + * @return a date time interval formatter. + * @see Category#FORMAT + * @stable ICU 4.0 + */ + public static final DateIntervalFormat + getInstance(String skeleton) + + { + return getInstance(skeleton, ULocale.getDefault(Category.FORMAT)); + } + + + /** + * Construct a DateIntervalFormat from skeleton and a given locale. + * + * This is a convenient override of + * getInstance(String skeleton, ULocale locale) + * + * <p>Example code:{@.jcite com.ibm.icu.samples.text.dateintervalformat.DateIntervalFormatSample:---dtitvfmtPreDefinedExample} + * @param skeleton the skeleton on which interval format based. + * @param locale the given locale + * @return a date time interval formatter. + * @stable ICU 4.0 + */ + public static final DateIntervalFormat + getInstance(String skeleton, Locale locale) + { + return getInstance(skeleton, ULocale.forLocale(locale)); + } + + + /** + * Construct a DateIntervalFormat from skeleton and a given locale. + * <P> + * In this factory method, + * the date interval pattern information is load from resource files. + * Users are encouraged to created date interval formatter this way and + * to use the pre-defined skeleton macros. + * + * <P> + * There are pre-defined skeletons in DateFormat, + * such as MONTH_DAY, YEAR_MONTH_WEEKDAY_DAY etc. + * + * Those skeletons have pre-defined interval patterns in resource files. + * Users are encouraged to use them. + * For example: + * DateIntervalFormat.getInstance(DateFormat.MONTH_DAY, false, loc); + * + * The given Locale provides the interval patterns. + * For example, for en_GB, if skeleton is YEAR_ABBR_MONTH_WEEKDAY_DAY, + * which is "yMMMEEEd", + * the interval patterns defined in resource file to above skeleton are: + * "EEE, d MMM, yyyy - EEE, d MMM, yyyy" for year differs, + * "EEE, d MMM - EEE, d MMM, yyyy" for month differs, + * "EEE, d - EEE, d MMM, yyyy" for day differs, + * @param skeleton the skeleton on which interval format based. + * @param locale the given locale + * @return a date time interval formatter. + * @stable ICU 4.0 + */ + public static final DateIntervalFormat + getInstance(String skeleton, ULocale locale) + { + DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale); + return new DateIntervalFormat(skeleton, locale, new SimpleDateFormat(generator.getBestPattern(skeleton), locale)); + } + + + + /** + * Construct a DateIntervalFormat from skeleton + * DateIntervalInfo, and the default <code>FORMAT</code> locale. + * + * This is a convenient override of + * getInstance(String skeleton, ULocale locale, DateIntervalInfo dtitvinf) + * with the locale value as default <code>FORMAT</code> locale. + * + * @param skeleton the skeleton on which interval format based. + * @param dtitvinf the DateIntervalInfo object to be adopted. + * @return a date time interval formatter. + * @see Category#FORMAT + * @stable ICU 4.0 + */ + public static final DateIntervalFormat getInstance(String skeleton, + DateIntervalInfo dtitvinf) + { + return getInstance(skeleton, ULocale.getDefault(Category.FORMAT), dtitvinf); + } + + + + /** + * Construct a DateIntervalFormat from skeleton + * a DateIntervalInfo, and the given locale. + * + * This is a convenient override of + * getInstance(String skeleton, ULocale locale, DateIntervalInfo dtitvinf) + * + * <p>Example code:{@.jcite com.ibm.icu.samples.text.dateintervalformat.DateIntervalFormatSample:---dtitvfmtCustomizedExample} + * @param skeleton the skeleton on which interval format based. + * @param locale the given locale + * @param dtitvinf the DateIntervalInfo object to be adopted. + * @return a date time interval formatter. + * @stable ICU 4.0 + */ + public static final DateIntervalFormat getInstance(String skeleton, + Locale locale, + DateIntervalInfo dtitvinf) + { + return getInstance(skeleton, ULocale.forLocale(locale), dtitvinf); + } + + + + /** + * Construct a DateIntervalFormat from skeleton + * a DateIntervalInfo, and the given locale. + * + * <P> + * In this factory method, user provides its own date interval pattern + * information, instead of using those pre-defined data in resource file. + * This factory method is for powerful users who want to provide their own + * interval patterns. + * + * <P> + * There are pre-defined skeleton in DateFormat, + * such as MONTH_DAY, YEAR_MONTH_WEEKDAY_DAY etc. + * + * Those skeletons have pre-defined interval patterns in resource files. + * Users are encouraged to use them. + * For example: + * DateIntervalFormat.getInstance(DateFormat.MONTH_DAY, false, loc,itvinf); + * + * the DateIntervalInfo provides the interval patterns. + * + * User are encouraged to set default interval pattern in DateIntervalInfo + * as well, if they want to set other interval patterns ( instead of + * reading the interval patterns from resource files). + * When the corresponding interval pattern for a largest calendar different + * field is not found ( if user not set it ), interval format fallback to + * the default interval pattern. + * If user does not provide default interval pattern, it fallback to + * "{date0} - {date1}" + * + * @param skeleton the skeleton on which interval format based. + * @param locale the given locale + * @param dtitvinf the DateIntervalInfo object to be adopted. + * @return a date time interval formatter. + * @stable ICU 4.0 + */ + public static final DateIntervalFormat getInstance(String skeleton, + ULocale locale, + DateIntervalInfo dtitvinf) + { + // clone. If it is frozen, clone returns itself, otherwise, clone + // returns a copy. + dtitvinf = (DateIntervalInfo)dtitvinf.clone(); + DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale); + return new DateIntervalFormat(skeleton, dtitvinf, new SimpleDateFormat(generator.getBestPattern(skeleton), locale)); + } + + + /** + * Clone this Format object polymorphically. + * @return A copy of the object. + * @stable ICU 4.0 + */ + @Override + public synchronized Object clone() + { + DateIntervalFormat other = (DateIntervalFormat) super.clone(); + other.fDateFormat = (SimpleDateFormat) fDateFormat.clone(); + other.fInfo = (DateIntervalInfo) fInfo.clone(); + other.fFromCalendar = (Calendar) fFromCalendar.clone(); + other.fToCalendar = (Calendar) fToCalendar.clone(); + other.fDatePattern = fDatePattern; + other.fTimePattern = fTimePattern; + other.fDateTimeFormat = fDateTimeFormat; + other.fCapitalizationSetting = fCapitalizationSetting; + return other; + } + + + /** + * Format an object to produce a string. This method handles Formattable + * objects with a DateInterval type. + * If a the Formattable object type is not a DateInterval, + * IllegalArgumentException is thrown. + * + * @param obj The object to format. + * Must be a DateInterval. + * @param appendTo Output parameter to receive result. + * Result is appended to existing contents. + * @param fieldPosition On input: an alignment field, if desired. + * On output: the offsets of the alignment field. + * There may be multiple instances of a given field type + * in an interval format; in this case the fieldPosition + * offsets refer to the first instance. + * @return Reference to 'appendTo' parameter. + * @throws IllegalArgumentException if the formatted object is not + * DateInterval object + * @stable ICU 4.0 + */ + @Override + public final StringBuffer + format(Object obj, StringBuffer appendTo, FieldPosition fieldPosition) + { + if ( obj instanceof DateInterval ) { + return format( (DateInterval)obj, appendTo, fieldPosition); + } + else { + throw new IllegalArgumentException("Cannot format given Object (" + obj.getClass().getName() + ") as a DateInterval"); + } + } + + /** + * Format a DateInterval to produce a string. + * + * @param dtInterval DateInterval to be formatted. + * @param appendTo Output parameter to receive result. + * Result is appended to existing contents. + * @param fieldPosition On input: an alignment field, if desired. + * On output: the offsets of the alignment field. + * There may be multiple instances of a given field type + * in an interval format; in this case the fieldPosition + * offsets refer to the first instance. + * @return Reference to 'appendTo' parameter. + * @stable ICU 4.0 + */ + public final StringBuffer format(DateInterval dtInterval, + StringBuffer appendTo, + FieldPosition fieldPosition) { + return formatIntervalImpl(dtInterval, appendTo, fieldPosition, null, null); + } + + /** + * Format a DateInterval to produce a FormattedDateInterval. + * + * The FormattedDateInterval exposes field information about the formatted string. + * + * @param dtInterval DateInterval to be formatted. + * @return A FormattedDateInterval containing the format result. + * @stable ICU 64 + */ + public FormattedDateInterval formatToValue(DateInterval dtInterval) { + StringBuffer sb = new StringBuffer(); + FieldPosition ignore = new FieldPosition(0); + FormatOutput output = new FormatOutput(); + List<FieldPosition> attributes = new ArrayList<>(); + formatIntervalImpl(dtInterval, sb, ignore, output, attributes); + if (output.firstIndex != -1) { + FormattedValueFieldPositionIteratorImpl.addOverlapSpans( + attributes, SpanField.DATE_INTERVAL_SPAN, output.firstIndex); + FormattedValueFieldPositionIteratorImpl.sort(attributes); + } + return new FormattedDateInterval(sb, attributes); + } + + private synchronized StringBuffer formatIntervalImpl( + DateInterval dtInterval, + StringBuffer appendTo, + FieldPosition pos, + FormatOutput output, + List<FieldPosition> attributes) { + fFromCalendar.setTimeInMillis(dtInterval.getFromDate()); + fToCalendar.setTimeInMillis(dtInterval.getToDate()); + return formatImpl(fFromCalendar, fToCalendar, appendTo, pos, output, attributes); + } + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public String getPatterns(Calendar fromCalendar, + Calendar toCalendar, + Output<String> part2) { + // First, find the largest different calendar field. + int field; + if ( fromCalendar.get(Calendar.ERA) != toCalendar.get(Calendar.ERA) ) { + field = Calendar.ERA; + } else if ( fromCalendar.get(Calendar.YEAR) != + toCalendar.get(Calendar.YEAR) ) { + field = Calendar.YEAR; + } else if ( fromCalendar.get(Calendar.MONTH) != + toCalendar.get(Calendar.MONTH) ) { + field = Calendar.MONTH; + } else if ( fromCalendar.get(Calendar.DATE) != + toCalendar.get(Calendar.DATE) ) { + field = Calendar.DATE; + } else if ( fromCalendar.get(Calendar.AM_PM) != + toCalendar.get(Calendar.AM_PM) ) { + field = Calendar.AM_PM; + } else if ( fromCalendar.get(Calendar.HOUR) != + toCalendar.get(Calendar.HOUR) ) { + field = Calendar.HOUR; + } else if ( fromCalendar.get(Calendar.MINUTE) != + toCalendar.get(Calendar.MINUTE) ) { + field = Calendar.MINUTE; + } else if ( fromCalendar.get(Calendar.SECOND) != + toCalendar.get(Calendar.SECOND) ) { + field = Calendar.SECOND; + } else if ( fromCalendar.get(Calendar.MILLISECOND) != + toCalendar.get(Calendar.MILLISECOND) ) { + field = Calendar.MILLISECOND; + } else { + return null; + } + PatternInfo intervalPattern = fIntervalPatterns.get( + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]); + part2.value = intervalPattern.getSecondPart(); + return intervalPattern.getFirstPart(); + } + + /** + * Format 2 Calendars to produce a string. + * + * @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. + * There may be multiple instances of a given field type + * in an interval format; in this case the fieldPosition + * offsets refer to the first instance. + * @return Reference to 'appendTo' parameter. + * @throws IllegalArgumentException if the two calendars are not equivalent. + * @stable ICU 4.0 + */ + public final StringBuffer format(Calendar fromCalendar, + Calendar toCalendar, + StringBuffer appendTo, + FieldPosition pos) { + return formatImpl(fromCalendar, toCalendar, appendTo, pos, null, null); + } + + /** + * Format 2 Calendars to produce a FormattedDateInterval. + * + * The FormattedDateInterval exposes field information about the formatted string. + * + * @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 + * @return A FormattedDateInterval containing the format result. + * @stable ICU 64 + */ + public FormattedDateInterval formatToValue(Calendar fromCalendar, Calendar toCalendar) { + StringBuffer sb = new StringBuffer(); + FieldPosition ignore = new FieldPosition(0); + FormatOutput output = new FormatOutput(); + List<FieldPosition> attributes = new ArrayList<>(); + formatImpl(fromCalendar, toCalendar, sb, ignore, output, attributes); + if (output.firstIndex != -1) { + FormattedValueFieldPositionIteratorImpl.addOverlapSpans( + attributes, SpanField.DATE_INTERVAL_SPAN, output.firstIndex); + FormattedValueFieldPositionIteratorImpl.sort(attributes); + } + return new FormattedDateInterval(sb, attributes); + } + + private synchronized StringBuffer formatImpl(Calendar fromCalendar, + Calendar toCalendar, + StringBuffer appendTo, + FieldPosition pos, + FormatOutput output, + List<FieldPosition> attributes) + { + // not support different calendar types and time zones + if ( !fromCalendar.isEquivalentTo(toCalendar) ) { + throw new IllegalArgumentException("can not format on two different calendars"); + } + + // Set up fDateFormat to handle the first or only part of the interval + // (override later for any second part). + fDateFormat.setContext(fCapitalizationSetting); + + // First, find the largest different calendar field. + int field = -1; //init with an invalid value. + + if ( fromCalendar.get(Calendar.ERA) != toCalendar.get(Calendar.ERA) ) { + field = Calendar.ERA; + } else if ( fromCalendar.get(Calendar.YEAR) != + toCalendar.get(Calendar.YEAR) ) { + field = Calendar.YEAR; + } else if ( fromCalendar.get(Calendar.MONTH) != + toCalendar.get(Calendar.MONTH) ) { + field = Calendar.MONTH; + } else if ( fromCalendar.get(Calendar.DATE) != + toCalendar.get(Calendar.DATE) ) { + field = Calendar.DATE; + } else if ( fromCalendar.get(Calendar.AM_PM) != + toCalendar.get(Calendar.AM_PM) ) { + field = Calendar.AM_PM; + } else if ( fromCalendar.get(Calendar.HOUR) != + toCalendar.get(Calendar.HOUR) ) { + field = Calendar.HOUR; + } else if ( fromCalendar.get(Calendar.MINUTE) != + toCalendar.get(Calendar.MINUTE) ) { + field = Calendar.MINUTE; + } else if ( fromCalendar.get(Calendar.SECOND) != + toCalendar.get(Calendar.SECOND) ) { + field = Calendar.SECOND; + } else if ( fromCalendar.get(Calendar.MILLISECOND) != + toCalendar.get(Calendar.MILLISECOND) ) { + field = Calendar.MILLISECOND; + } else { + /* ignore the millisecond etc. small fields' difference. + * use single date when all the above are the same. + */ + return fDateFormat.format(fromCalendar, appendTo, pos, attributes); + } + boolean fromToOnSameDay = (field==Calendar.AM_PM || field==Calendar.HOUR || field==Calendar.MINUTE || field==Calendar.SECOND || field==Calendar.MILLISECOND); + + // get interval pattern + PatternInfo intervalPattern = fIntervalPatterns.get( + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]); + + if ( intervalPattern == null ) { + if ( fDateFormat.isFieldUnitIgnored(field) ) { + /* the largest different calendar field is small than + * the smallest calendar field in pattern, + * return single date format. + */ + return fDateFormat.format(fromCalendar, appendTo, pos, attributes); + } + + return fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos, + output, attributes); + } + + // If the first part in interval pattern is empty, + // the 2nd part of it saves the full-pattern used in fall-back. + // For a 'real' interval pattern, the first part will never be empty. + if ( intervalPattern.getFirstPart() == null ) { + // fall back + return fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos, + output, attributes, intervalPattern.getSecondPart()); + } + Calendar firstCal; + Calendar secondCal; + if ( intervalPattern.firstDateInPtnIsLaterDate() ) { + if (output != null) { + output.register(1); + } + firstCal = toCalendar; + secondCal = fromCalendar; + } else { + if (output != null) { + output.register(0); + } + firstCal = fromCalendar; + secondCal = toCalendar; + } + // break the interval pattern into 2 parts + // first part should not be empty, + String originalPattern = fDateFormat.toPattern(); + fDateFormat.applyPattern(intervalPattern.getFirstPart()); + fDateFormat.format(firstCal, appendTo, pos, attributes); + // Only accept the first instance of the field + if (pos.getEndIndex() > 0) { + pos = new FieldPosition(0); + } + if ( intervalPattern.getSecondPart() != null ) { + fDateFormat.applyPattern(intervalPattern.getSecondPart()); + // No capitalization for second part of interval + fDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE); + fDateFormat.format(secondCal, appendTo, pos, attributes); + } + fDateFormat.applyPattern(originalPattern); + return appendTo; + } + + /** Like fallbackFormat, but specifically for ranges. */ + private final void fallbackFormatRange(Calendar fromCalendar, + Calendar toCalendar, + StringBuffer appendTo, + StringBuilder patternSB, + FieldPosition pos, + FormatOutput output, + List<FieldPosition> attributes) { + String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments( + fInfo.getFallbackIntervalPattern(), patternSB, 2, 2); + long state = 0; + while (true) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { + break; + } + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { + if (output != null) { + output.register(0); + } + fDateFormat.format(fromCalendar, appendTo, pos, attributes); + } else { + if (output != null) { + output.register(1); + } + fDateFormat.format(toCalendar, appendTo, pos, attributes); + } + // Only accept the first instance of the field + if (pos.getEndIndex() > 0) { + pos = new FieldPosition(0); + } + // No capitalization for second portion + fDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE); + } + } + + /* + * Format 2 Calendars to using fall-back interval pattern + * + * The full pattern used in this fall-back format is the + * full pattern of the date formatter. + * + * @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. + * @return Reference to 'appendTo' parameter. + */ + private final StringBuffer fallbackFormat(Calendar fromCalendar, + Calendar toCalendar, + boolean fromToOnSameDay, + StringBuffer appendTo, + FieldPosition pos, + FormatOutput output, + List<FieldPosition> attributes) { + StringBuilder patternSB = new StringBuilder(); + boolean formatDatePlusTimeRange = (fromToOnSameDay && fDatePattern != null && fTimePattern != null); + if (formatDatePlusTimeRange) { + String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments( + fDateTimeFormat, patternSB, 2, 2); + + String fullPattern; // for saving the pattern in fDateFormat + fullPattern = fDateFormat.toPattern(); // save current pattern, restore later + + // {0} is time range + // {1} is single date portion + long state = 0; + while (true) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { + break; + } + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { + fDateFormat.applyPattern(fTimePattern); + fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes); + } else { + fDateFormat.applyPattern(fDatePattern); + fDateFormat.format(fromCalendar, appendTo, pos, attributes); + } + // Only accept the first instance of the field + if (pos.getEndIndex() > 0) { + pos = new FieldPosition(0); + } + // No capitalization for second portion + fDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE); + } + + // restore full pattern + fDateFormat.applyPattern(fullPattern); + } else { + fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes); + } + return appendTo; + } + + + /* + * Format 2 Calendars to using fall-back interval pattern + * + * This fall-back pattern is generated on a given full pattern, + * not the full pattern of the date formatter. + * + * @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. + * @param fullPattern the full pattern need to apply to date formatter + * @return Reference to 'appendTo' parameter. + */ + private final StringBuffer fallbackFormat(Calendar fromCalendar, + Calendar toCalendar, + boolean fromToOnSameDay, + StringBuffer appendTo, + FieldPosition pos, + FormatOutput output, + List<FieldPosition> attributes, + String fullPattern) { + String originalPattern = fDateFormat.toPattern(); + fDateFormat.applyPattern(fullPattern); + fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos, output, attributes); + fDateFormat.applyPattern(originalPattern); + return appendTo; + } + + + /** + * Date interval parsing is not supported. + * <P> + * This method should handle parsing of + * date time interval strings into Formattable objects with + * DateInterval type, which is a pair of UDate. + * <P> + * Before calling, set parse_pos.index to the offset you want to start + * parsing at in the source. After calling, parse_pos.index is the end of + * the text you parsed. If error occurs, index is unchanged. + * <P> + * When parsing, leading whitespace is discarded (with a successful parse), + * while trailing whitespace is left as is. + * <P> + * See Format.parseObject() for more. + * + * @param source The string to be parsed into an object. + * @param parse_pos The position to start parsing at. Since no parsing + * is supported, upon return this param is unchanged. + * @return A newly created {@code Formattable} object, or NULL + * on failure. + * @internal + * @deprecated This API is ICU internal only. + */ + @Override + @Deprecated + public Object parseObject(String source, ParsePosition parse_pos) + { + throw new UnsupportedOperationException("parsing is not supported"); + } + + + /** + * Gets the date time interval patterns. + * @return a copy of the date time interval patterns associated with + * this date interval formatter. + * @stable ICU 4.0 + */ + public DateIntervalInfo getDateIntervalInfo() + { + return (DateIntervalInfo)fInfo.clone(); + } + + + /** + * Set the date time interval patterns. + * @param newItvPattern the given interval patterns to copy. + * @stable ICU 4.0 + */ + public void setDateIntervalInfo(DateIntervalInfo newItvPattern) + { + // clone it. If it is frozen, the clone returns itself. + // Otherwise, clone returns a copy + fInfo = (DateIntervalInfo)newItvPattern.clone(); + this.isDateIntervalInfoDefault = false; + fInfo.freeze(); // freeze it + if ( fDateFormat != null ) { + initializePattern(null); + } + } + + /** + * Get the TimeZone + * @return A copy of the TimeZone associated with this date interval formatter. + * @stable ICU 53 + */ + public TimeZone getTimeZone() + { + if ( fDateFormat != null ) { + // Here we clone, like other getters here, but unlike + // DateFormat.getTimeZone() and Calendar.getTimeZone() + // which return the TimeZone from the Calendar's zone variable + return (TimeZone)(fDateFormat.getTimeZone().clone()); + } + // If fDateFormat is null (unexpected), return default timezone. + return TimeZone.getDefault(); + } + + + /** + * Set the TimeZone for the calendar used by this DateIntervalFormat object. + * @param zone The new TimeZone, will be cloned for use by this DateIntervalFormat. + * @stable ICU 53 + */ + public void setTimeZone(TimeZone zone) + { + // zone is cloned once for all three usages below: + TimeZone zoneToSet = (TimeZone)zone.clone(); + if (fDateFormat != null) { + fDateFormat.setTimeZone(zoneToSet); + } + // fDateFormat has the primary calendar for the DateIntervalFormat; + // fFromCalendar and fToCalendar are internal work clones of that calendar. + if (fFromCalendar != null) { + fFromCalendar.setTimeZone(zoneToSet); + } + if (fToCalendar != null) { + fToCalendar.setTimeZone(zoneToSet); + } + } + + /** + * {@icu} Set a particular DisplayContext value in the formatter, + * such as CAPITALIZATION_FOR_STANDALONE. This causes the formatted + * result to be capitalized appropriately for the context in which + * it is intended to be used, considering both the locale and the + * type of field at the beginning of the formatted result. + * + * @param context The DisplayContext value to set. + * @stable ICU 68 + */ + public void setContext(DisplayContext context) + { + if (context.type() == DisplayContext.Type.CAPITALIZATION) { + fCapitalizationSetting = context; + } + } + + /** + * {@icu} Get the formatter's DisplayContext value for the specified DisplayContext.Type, + * such as CAPITALIZATION. + * + * @param type the DisplayContext.Type whose value to return + * @return the current DisplayContext setting for the specified type + * @stable ICU 68 + */ + public DisplayContext getContext(DisplayContext.Type type) + { + return (type == DisplayContext.Type.CAPITALIZATION && fCapitalizationSetting != null)? + fCapitalizationSetting: DisplayContext.CAPITALIZATION_NONE; + } + + /** + * Gets the date formatter + * @return a copy of the date formatter associated with + * this date interval formatter. + * @stable ICU 4.0 + */ + public synchronized DateFormat getDateFormat() + { + return (DateFormat)fDateFormat.clone(); + } + + + /* + * Below are for generating interval patterns locale to the formatter + */ + + /* + * Initialize interval patterns locale to this formatter. + */ + private void initializePattern(ICUCache<String, Map<String, PatternInfo>> cache) { + String fullPattern = fDateFormat.toPattern(); + ULocale locale = fDateFormat.getLocale(); + String key = null; + Map<String, PatternInfo> patterns = null; + if (cache != null) { + if ( fSkeleton != null ) { + key = locale.toString() + "+" + fullPattern + "+" + fSkeleton; + } else { + key = locale.toString() + "+" + fullPattern; + } + patterns = cache.get(key); + } + if (patterns == null) { + Map<String, PatternInfo> intervalPatterns = initializeIntervalPattern(fullPattern, locale); + patterns = Collections.unmodifiableMap(intervalPatterns); + if (cache != null) { + cache.put(key, patterns); + } + } + fIntervalPatterns = patterns; + } + + + + /* + * Initialize interval patterns locale to this formatter + * + * This code is a bit complicated since + * 1. the interval patterns saved in resource bundle files are interval + * patterns based on date or time only. + * It does not have interval patterns based on both date and time. + * Interval patterns on both date and time are algorithm generated. + * + * For example, it has interval patterns on skeleton "dMy" and "hm", + * but it does not have interval patterns on skeleton "dMyhm". + * + * The rule to generate interval patterns for both date and time skeleton are + * 1) when the year, month, or day differs, concatenate the two original + * expressions with a separator between, + * For example, interval pattern from "Jan 10, 2007 10:10 am" + * to "Jan 11, 2007 10:10am" is + * "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am" + * + * 2) otherwise, present the date followed by the range expression + * for the time. + * For example, interval pattern from "Jan 10, 2007 10:10 am" + * to "Jan 10, 2007 11:10am" is + * "Jan 10, 2007 10:10 am - 11:10am" + * + * 2. even a pattern does not request a certain calendar field, + * the interval pattern needs to include such field if such fields are + * different between 2 dates. + * For example, a pattern/skeleton is "hm", but the interval pattern + * includes year, month, and date when year, month, and date differs. + * + * + * @param fullPattern formatter's full pattern + * @param locale the given locale. + * @return interval patterns' hash map + */ + private Map<String, PatternInfo> initializeIntervalPattern(String fullPattern, ULocale locale) { + DateTimePatternGenerator dtpng = DateTimePatternGenerator.getInstance(locale); + if ( fSkeleton == null ) { + // fSkeleton is already set by getDateIntervalInstance() + // or by getInstance(String skeleton, .... ) + fSkeleton = dtpng.getSkeleton(fullPattern); + } + String skeleton = normalizeHourMetacharacters(fSkeleton, locale); + + HashMap<String, PatternInfo> intervalPatterns = new HashMap<>(); + + /* Check whether the skeleton is a combination of date and time. + * For the complication reason 1 explained above. + */ + StringBuilder date = new StringBuilder(skeleton.length()); + StringBuilder normalizedDate = new StringBuilder(skeleton.length()); + StringBuilder time = new StringBuilder(skeleton.length()); + StringBuilder normalizedTime = new StringBuilder(skeleton.length()); + + /* the difference between time skeleton and normalizedTimeSkeleton are: + * 1. (Formerly, normalized time skeleton folded 'H' to 'h'; no longer true) + * 2. 'a' is omitted in normalized time skeleton. + * 3. there is only one appearance for 'h', 'm','v', 'z' in normalized + * time skeleton + * + * The difference between date skeleton and normalizedDateSkeleton are: + * 1. both 'y' and 'd' appear only once in normalizeDateSkeleton + * 2. 'E' and 'EE' are normalized into 'EEE' + * 3. 'MM' is normalized into 'M' + */ + getDateTimeSkeleton(skeleton, date, normalizedDate, + time, normalizedTime); + + String dateSkeleton = date.toString(); + String timeSkeleton = time.toString(); + String normalizedDateSkeleton = normalizedDate.toString(); + String normalizedTimeSkeleton = normalizedTime.toString(); + + // move this up here since we need it for fallbacks + if (time.length() != 0 && date.length() != 0) { + // Need the Date/Time pattern for concatenating the date with + // the time interval. + // The date/time pattern ( such as {0} {1} ) is saved in + // calendar, that is why need to get the CalendarData here. + fDateTimeFormat = getConcatenationPattern(locale); + } + + boolean found = genSeparateDateTimePtn(normalizedDateSkeleton, + normalizedTimeSkeleton, + intervalPatterns, dtpng); + + // for skeletons with seconds, found is false and we enter this block + if ( found == false ) { + // use fallback + // TODO: if user asks "m", but "d" differ + //StringBuffer skeleton = new StringBuffer(skeleton); + if ( time.length() != 0 ) { + //genFallbackForNotFound(Calendar.MINUTE, skeleton); + //genFallbackForNotFound(Calendar.HOUR, skeleton); + //genFallbackForNotFound(Calendar.AM_PM, skeleton); + if ( date.length() == 0 ) { + // prefix with yMd + timeSkeleton = DateFormat.YEAR_NUM_MONTH_DAY + timeSkeleton; + String pattern =dtpng.getBestPattern(timeSkeleton); + // for fall back interval patterns, + // the first part of the pattern is empty, + // the second part of the pattern is the full-pattern + // should be used in fall-back. + PatternInfo ptn = new PatternInfo(null, pattern, + fInfo.getDefaultOrder()); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE], ptn); + // share interval pattern + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.MONTH], ptn); + // share interval pattern + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR], ptn); + + pattern =dtpng.getBestPattern(timeSkeleton + "G"); + ptn = new PatternInfo(null, pattern, fInfo.getDefaultOrder()); + // share interval pattern + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.ERA], ptn); + } else { + //genFallbackForNotFound(Calendar.DATE, skeleton); + //genFallbackForNotFound(Calendar.MONTH, skeleton); + //genFallbackForNotFound(Calendar.YEAR, skeleton); + } + } else { + //genFallbackForNotFound(Calendar.DATE, skeleton); + //genFallbackForNotFound(Calendar.MONTH, skeleton); + //genFallbackForNotFound(Calendar.YEAR, skeleton); + } + return intervalPatterns; + } // end of skeleton not found + // interval patterns for skeleton are found in resource + if ( time.length() == 0 ) { + // done + } else if ( date.length() == 0 ) { + // need to set up patterns for y/M/d differ + /* result from following looks confusing. + * for example: 10 10:10 - 11 10:10, it is not + * clear that the first 10 is the 10th day + time.insert(0, 'd'); + genFallbackPattern(Calendar.DATE, time); + time.insert(0, 'M'); + genFallbackPattern(Calendar.MONTH, time); + time.insert(0, 'y'); + genFallbackPattern(Calendar.YEAR, time); + */ + // prefix with yMd + timeSkeleton = DateFormat.YEAR_NUM_MONTH_DAY + timeSkeleton; + String pattern =dtpng.getBestPattern(timeSkeleton); + // for fall back interval patterns, + // the first part of the pattern is empty, + // the second part of the pattern is the full-pattern + // should be used in fall-back. + PatternInfo ptn = new PatternInfo( + null, pattern, fInfo.getDefaultOrder()); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE], ptn); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.MONTH], ptn); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR], ptn); + + pattern =dtpng.getBestPattern(timeSkeleton + "G"); + ptn = new PatternInfo(null, pattern, fInfo.getDefaultOrder()); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.ERA], ptn); + } else { + /* if both present, + * 1) when the year, month, or day differs, + * concatenate the two original expressions with a separator between, + * 2) otherwise, present the date followed by the + * range expression for the time. + */ + /* + * 1) when the era, year, month, or day differs, + * concatenate the two original expressions with a separator between, + */ + // if field exists, use fall back + if ( !fieldExistsInSkeleton(Calendar.DATE, dateSkeleton) ) { + // prefix skeleton with 'd' + skeleton = DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE] + skeleton; + genFallbackPattern(Calendar.DATE, skeleton, intervalPatterns, dtpng); + } + if ( !fieldExistsInSkeleton(Calendar.MONTH, dateSkeleton) ) { + // then prefix skeleton with 'M' + skeleton = DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.MONTH] + skeleton; + genFallbackPattern(Calendar.MONTH, skeleton, intervalPatterns, dtpng); + } + if ( !fieldExistsInSkeleton(Calendar.YEAR, dateSkeleton) ) { + // then prefix skeleton with 'y' + skeleton = DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR] + skeleton; + genFallbackPattern(Calendar.YEAR, skeleton, intervalPatterns, dtpng); + } + if ( !fieldExistsInSkeleton(Calendar.ERA, dateSkeleton) ) { + // then prefix skeleton with 'G' + skeleton = DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.ERA] + skeleton; + genFallbackPattern(Calendar.ERA, skeleton, intervalPatterns, dtpng); + } + + /* + * 2) otherwise, present the date followed by the + * range expression for the time. + */ + if (fDateTimeFormat == null) { + fDateTimeFormat = "{1} {0}"; + } + String datePattern =dtpng.getBestPattern(dateSkeleton); + concatSingleDate2TimeInterval(fDateTimeFormat, datePattern, Calendar.AM_PM, intervalPatterns); + concatSingleDate2TimeInterval(fDateTimeFormat, datePattern, Calendar.HOUR, intervalPatterns); + concatSingleDate2TimeInterval(fDateTimeFormat, datePattern, Calendar.MINUTE, intervalPatterns); + } + + return intervalPatterns; + } + + /** + * Retrieves the concatenation DateTime pattern from the resource bundle. + * @param locale Locale to retrieve. + * @return Concatenation DateTime pattern. + */ + private String getConcatenationPattern(ULocale locale) { + ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); + ICUResourceBundle dtPatternsRb = rb.getWithFallback("calendar/gregorian/DateTimePatterns"); + ICUResourceBundle concatenationPatternRb = (ICUResourceBundle) dtPatternsRb.get(8); + if (concatenationPatternRb.getType() == UResourceBundle.STRING) { + return concatenationPatternRb.getString(); + } else { + return concatenationPatternRb.getString(0); + } + } + + /* + * Generate fall back interval pattern given a calendar field, + * a skeleton, and a date time pattern generator + * @param field the largest different calendar field + * @param skeleton a skeleton + * @param dtpng date time pattern generator + * @param intervalPatterns interval patterns + */ + private void genFallbackPattern(int field, String skeleton, + Map<String, PatternInfo> intervalPatterns, + DateTimePatternGenerator dtpng) { + String pattern = dtpng.getBestPattern(skeleton); + // for fall back interval patterns, + // the first part of the pattern is empty, + // the second part of the pattern is the full-pattern + // should be used in fall-back. + PatternInfo ptn = new PatternInfo( + null, pattern, fInfo.getDefaultOrder()); + intervalPatterns.put( + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field], ptn); + } + + + + /* + private void genFallbackForNotFound(String field, StringBuffer skeleton) { + if ( SimpleDateFormat.isFieldUnitIgnored(skeleton.toString(), field) ) { + // single date + DateIntervalInfo.PatternInfo ptnInfo = + new DateIntervalInfo.PatternInfo(null, fDateFormat.toPattern(), + fInfo.getDefaultOrder()); + fIntervalPatterns.put(field, ptnInfo); + return; + } else if ( skeleton.indexOf(field) == -1 ) { + skeleton.insert(0,field); + genFallbackPattern(field, skeleton, dtpng); + } + } + */ + + private String normalizeHourMetacharacters(String skeleton, ULocale locale) { + StringBuilder result = new StringBuilder(skeleton); + + char hourMetachar = '\0'; + char dayPeriodChar = '\0'; + int hourFieldStart = 0; + int hourFieldLength = 0; + int dayPeriodStart = 0; + int dayPeriodLength = 0; + for (int i = 0; i < result.length(); i++) { + char c = result.charAt(i); + if (c == 'j' || c == 'J' || c == 'C' || c == 'h' || c == 'H' || c == 'k' || c == 'K') { + if (hourMetachar == '\0') { + hourMetachar = c; + hourFieldStart = i; + } + ++hourFieldLength; + } else if (c == 'a' || c == 'b' || c == 'B') { + if (dayPeriodChar == '\0') { + dayPeriodChar = c; + dayPeriodStart = i; + } + ++dayPeriodLength; + } else { + if (hourMetachar != '\0' && dayPeriodChar != '\0') { + break; + } + } + } + + if (hourMetachar != '\0') { + char hourChar = 'H'; + + DateTimePatternGenerator dtptng = DateTimePatternGenerator.getInstance(locale); + String convertedPattern = dtptng.getBestPattern(String.valueOf(hourMetachar)); + + // strip literal text from the pattern (so literal characters don't get mistaken for pattern + // characters-- such as the 'h' in 'Uhr' in German) + int firstQuotePos; + while ((firstQuotePos = convertedPattern.indexOf('\'')) != -1) { + int secondQuotePos = convertedPattern.indexOf('\'', firstQuotePos + 1); + if (secondQuotePos == -1) { + secondQuotePos = firstQuotePos; + } + convertedPattern = convertedPattern.substring(0, firstQuotePos) + convertedPattern.substring(secondQuotePos + 1); + } + + if (convertedPattern.indexOf('h') != -1) { + hourChar = 'h'; + } else if (convertedPattern.indexOf('K') != -1) { + hourChar = 'K'; + } else if (convertedPattern.indexOf('k') != -1) { + hourChar = 'k'; + } + + if (convertedPattern.indexOf('b') != -1) { + dayPeriodChar = 'b'; + } else if (convertedPattern.indexOf('B') != -1) { + dayPeriodChar = 'B'; + } else if (dayPeriodChar == '\0') { + dayPeriodChar = 'a'; + } + + StringBuilder hourAndDayPeriod = new StringBuilder(); + hourAndDayPeriod.append(hourChar); + if (hourChar != 'H' && hourChar != 'k') { + int newDayPeriodLength = 0; + if (dayPeriodLength >= 5 || hourFieldLength >= 5) { + newDayPeriodLength = 5; + } else if (dayPeriodLength >= 3 || hourFieldLength >= 3) { + newDayPeriodLength = 3; + } else { + newDayPeriodLength = 1; + } + for (int i = 0; i < newDayPeriodLength; i++) { + hourAndDayPeriod.append(dayPeriodChar); + } + } + result.replace(hourFieldStart, hourFieldStart + hourFieldLength, hourAndDayPeriod.toString()); + if (dayPeriodStart > hourFieldStart) { + dayPeriodStart += hourAndDayPeriod.length() - hourFieldLength; + } + result.delete(dayPeriodStart, dayPeriodStart + dayPeriodLength); + } + return result.toString(); + } + + /* + * get separated date and time skeleton from a combined skeleton. + * + * The difference between date skeleton and normalizedDateSkeleton are: + * 1. both 'y' and 'd' are appeared only once in normalizeDateSkeleton + * 2. 'E' and 'EE' are normalized into 'EEE' + * 3. 'MM' is normalized into 'M' + * + ** the difference between time skeleton and normalizedTimeSkeleton are: + * 1. both 'H' and 'h' are normalized as 'h' in normalized time skeleton, + * 2. 'a' is omitted in normalized time skeleton. + * 3. there is only one appearance for 'h', 'm','v', 'z' in normalized time + * skeleton + * + * + * @param skeleton given combined skeleton. + * @param date Output parameter for date only skeleton. + * @param normalizedDate Output parameter for normalized date only + * + * @param time Output parameter for time only skeleton. + * @param normalizedTime Output parameter for normalized time only + * skeleton. + */ + private static void getDateTimeSkeleton(String skeleton, + StringBuilder dateSkeleton, + StringBuilder normalizedDateSkeleton, + StringBuilder timeSkeleton, + StringBuilder normalizedTimeSkeleton) + { + // dateSkeleton follows the sequence of y*M*E*d* + // timeSkeleton follows the sequence of hm*[v|z]? + int i; + int ECount = 0; + int dCount = 0; + int MCount = 0; + int yCount = 0; + int mCount = 0; + int vCount = 0; + int zCount = 0; + char hourChar = '\0'; + + for (i = 0; i < skeleton.length(); ++i) { + char ch = skeleton.charAt(i); + switch ( ch ) { + case 'E': + dateSkeleton.append(ch); + ++ECount; + break; + case 'd': + dateSkeleton.append(ch); + ++dCount; + break; + case 'M': + dateSkeleton.append(ch); + ++MCount; + break; + case 'y': + dateSkeleton.append(ch); + ++yCount; + break; + case 'G': + case 'Y': + case 'u': + case 'Q': + case 'q': + case 'L': + case 'l': + case 'W': + case 'w': + case 'D': + case 'F': + case 'g': + case 'e': + case 'c': + case 'U': + case 'r': + normalizedDateSkeleton.append(ch); + dateSkeleton.append(ch); + break; + case 'h': + case 'H': + case 'k': + case 'K': + timeSkeleton.append(ch); + if (hourChar == '\0') { + hourChar = ch; + } + break; + case 'm': + timeSkeleton.append(ch); + ++mCount; + break; + case 'z': + ++zCount; + timeSkeleton.append(ch); + break; + case 'v': + ++vCount; + timeSkeleton.append(ch); + break; + case 'a': + case 'V': + case 'Z': + case 'j': + case 's': + case 'S': + case 'A': + case 'b': + case 'B': + timeSkeleton.append(ch); + normalizedTimeSkeleton.append(ch); + break; + } + } + + /* generate normalized form for date*/ + if ( yCount != 0 ) { + for (i = 0; i < yCount; i++) { + normalizedDateSkeleton.append('y'); + } + } + if ( MCount != 0 ) { + if ( MCount < 3 ) { + normalizedDateSkeleton.append('M'); + } else { + for ( i = 0; i < MCount && i < 5; ++i ) { + normalizedDateSkeleton.append('M'); + } + } + } + if ( ECount != 0 ) { + if ( ECount <= 3 ) { + normalizedDateSkeleton.append('E'); + } else { + for ( i = 0; i < ECount && i < 5; ++i ) { + normalizedDateSkeleton.append('E'); + } + } + } + if ( dCount != 0 ) { + normalizedDateSkeleton.append('d'); + } + + /* generate normalized form for time */ + if ( hourChar != '\0' ) { + normalizedTimeSkeleton.append(hourChar); + } + if ( mCount != 0 ) { + normalizedTimeSkeleton.append('m'); + } + if ( zCount != 0 ) { + normalizedTimeSkeleton.append('z'); + } + if ( vCount != 0 ) { + normalizedTimeSkeleton.append('v'); + } + } + + + + /* + * Generate date or time interval pattern from resource. + * + * It needs to handle the following: + * 1. need to adjust field width. + * For example, the interval patterns saved in DateIntervalInfo + * includes "dMMMy", but not "dMMMMy". + * Need to get interval patterns for dMMMMy from dMMMy. + * Another example, the interval patterns saved in DateIntervalInfo + * includes "hmv", but not "hmz". + * Need to get interval patterns for "hmz' from 'hmv' + * + * 2. there might be no pattern for 'y' differ for skeleton "Md", + * in order to get interval patterns for 'y' differ, + * need to look for it from skeleton 'yMd' + * + * @param dateSkeleton normalized date skeleton + * @param timeSkeleton normalized time skeleton + * @param intervalPatterns interval patterns + * @return whether there is interval patterns for the skeleton. + * true if there is, false otherwise + */ + private boolean genSeparateDateTimePtn(String dateSkeleton, + String timeSkeleton, + Map<String, PatternInfo> intervalPatterns, + DateTimePatternGenerator dtpng) + { + String skeleton; + // if both date and time skeleton present, + // the final interval pattern might include time interval patterns + // ( when, am_pm, hour, minute, second differ ), + // but not date interval patterns ( when year, month, day differ ). + // For year/month/day differ, it falls back to fall-back pattern. + if ( timeSkeleton.length() != 0 ) { + skeleton = timeSkeleton; + } else { + skeleton = dateSkeleton; + } + + /* interval patterns for skeleton "dMMMy" (but not "dMMMMy") + * are defined in resource, + * interval patterns for skeleton "dMMMMy" are calculated by + * 1. get the best match skeleton for "dMMMMy", which is "dMMMy" + * 2. get the interval patterns for "dMMMy", + * 3. extend "MMM" to "MMMM" in above interval patterns for "dMMMMy" + * getBestSkeleton() is step 1. + */ + // best skeleton, and the difference information + BestMatchInfo retValue = fInfo.getBestSkeleton(skeleton); + String bestSkeleton = retValue.bestMatchSkeleton; + int differenceInfo = retValue.bestMatchDistanceInfo; + + // Set patterns for fallback use, need to do this + // before returning if differenceInfo == -1 + if (dateSkeleton.length() != 0 ) { + fDatePattern = dtpng.getBestPattern(dateSkeleton); + } + if (timeSkeleton.length() != 0 ) { + fTimePattern = dtpng.getBestPattern(timeSkeleton); + } + + // difference: + // 0 means the best matched skeleton is the same as input skeleton + // 1 means the fields are the same, but field width are different + // 2 means the only difference between fields are v/z, + // -1 means there are other fields difference + // (this will happen, for instance, if the supplied skeleton has seconds, + // but no skeletons in the intervalFormats data do) + if ( differenceInfo == -1 ) { + // skeleton has different fields, not only v/z difference + return false; + } + + if ( timeSkeleton.length() == 0 ) { + // only has date skeleton + genIntervalPattern(Calendar.DATE, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + SkeletonAndItsBestMatch skeletons = genIntervalPattern( + Calendar.MONTH, skeleton, + bestSkeleton, differenceInfo, + intervalPatterns); + if ( skeletons != null ) { + bestSkeleton = skeletons.skeleton; + skeleton = skeletons.bestMatchSkeleton; + } + genIntervalPattern(Calendar.YEAR, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + genIntervalPattern(Calendar.ERA, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + } else { + genIntervalPattern(Calendar.MINUTE, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + genIntervalPattern(Calendar.HOUR, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + genIntervalPattern(Calendar.AM_PM, skeleton, bestSkeleton, differenceInfo, intervalPatterns); + } + return true; + + } + + + + /* + * Generate interval pattern from existing resource + * + * It not only save the interval patterns, + * but also return the skeleton and its best match skeleton. + * + * @param field largest different calendar field + * @param skeleton skeleton + * @param bestSkeleton the best match skeleton which has interval pattern + * defined in resource + * @param differenceInfo the difference between skeleton and best skeleton + * 0 means the best matched skeleton is the same as input skeleton + * 1 means the fields are the same, but field width are different + * 2 means the only difference between fields are v/z, + * -1 means there are other fields difference + * + * @param intervalPatterns interval patterns + * + * @return an extended skeleton or extended best skeleton if applicable. + * null otherwise. + */ + private SkeletonAndItsBestMatch genIntervalPattern( + int field, String skeleton, String bestSkeleton, + int differenceInfo, Map<String, PatternInfo> intervalPatterns) { + SkeletonAndItsBestMatch retValue = null; + PatternInfo pattern = fInfo.getIntervalPattern( + bestSkeleton, field); + if ( pattern == null ) { + // single date + if ( SimpleDateFormat.isFieldUnitIgnored(bestSkeleton, field) ) { + PatternInfo ptnInfo = + new PatternInfo(fDateFormat.toPattern(), + null, + fInfo.getDefaultOrder()); + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[field], ptnInfo); + return null; + } + + // for 24 hour system, interval patterns in resource file + // might not include pattern when am_pm differ, + // which should be the same as hour differ. + // add it here for simplicity + if ( field == Calendar.AM_PM ) { + pattern = fInfo.getIntervalPattern(bestSkeleton, + Calendar.HOUR); + if ( pattern != null ) { + boolean suppressDayPeriodField = fSkeleton.indexOf('J') != -1; + String part1 = adjustFieldWidth(skeleton, bestSkeleton, + pattern.getFirstPart(), differenceInfo, suppressDayPeriodField); + String part2 = adjustFieldWidth(skeleton, bestSkeleton, + pattern.getSecondPart(), differenceInfo, suppressDayPeriodField); + pattern = new PatternInfo(part1, part2, + pattern.firstDateInPtnIsLaterDate()); + + // share + intervalPatterns.put(DateIntervalInfo. + CALENDAR_FIELD_TO_PATTERN_LETTER[field], + pattern); + } + return null; + } + // else, looking for pattern when 'y' differ for 'dMMMM' skeleton, + // first, get best match pattern "MMMd", + // since there is no pattern for 'y' differs for skeleton 'MMMd', + // need to look for it from skeleton 'yMMMd', + // if found, adjust field width in interval pattern from + // "MMM" to "MMMM". + String fieldLetter = + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]; + bestSkeleton = fieldLetter + bestSkeleton; + skeleton = fieldLetter + skeleton; + // for example, looking for patterns when 'y' differ for + // skeleton "MMMM". + pattern = fInfo.getIntervalPattern(bestSkeleton, field); + if ( pattern == null && differenceInfo == 0 ) { + // if there is no skeleton "yMMMM" defined, + // look for the best match skeleton, for example: "yMMM" + BestMatchInfo tmpRetValue = fInfo.getBestSkeleton(skeleton); + String tmpBestSkeleton = tmpRetValue.bestMatchSkeleton; + differenceInfo = tmpRetValue.bestMatchDistanceInfo; + if ( tmpBestSkeleton.length() != 0 && differenceInfo != -1 ) { + pattern = fInfo.getIntervalPattern(tmpBestSkeleton, field); + bestSkeleton = tmpBestSkeleton; + } + } + if ( pattern != null ) { + retValue = new SkeletonAndItsBestMatch(skeleton, bestSkeleton); + } + } + if ( pattern != null ) { + if ( differenceInfo != 0 ) { + boolean suppressDayPeriodField = fSkeleton.indexOf('J') != -1; + String part1 = adjustFieldWidth(skeleton, bestSkeleton, + pattern.getFirstPart(), differenceInfo, suppressDayPeriodField); + String part2 = adjustFieldWidth(skeleton, bestSkeleton, + pattern.getSecondPart(), differenceInfo, suppressDayPeriodField); + pattern = new PatternInfo(part1, part2, + pattern.firstDateInPtnIsLaterDate()); + } else { + // pattern is immutable, no need to clone; + // pattern = (PatternInfo)pattern.clone(); + } + intervalPatterns.put( + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field], pattern); + } + return retValue; + } + + /* + * Adjust field width in best match interval pattern to match + * the field width in input skeleton. + * + * TODO (xji) make a general solution + * The adjusting rule can be: + * 1. always adjust + * 2. never adjust + * 3. default adjust, which means adjust according to the following rules + * 3.1 always adjust string, such as MMM and MMMM + * 3.2 never adjust between string and numeric, such as MM and MMM + * 3.3 always adjust year + * 3.4 do not adjust 'd', 'h', or 'm' if h presents + * 3.5 do not adjust 'M' if it is numeric(?) + * + * Since date interval format is well-formed format, + * date and time skeletons are normalized previously, + * till this stage, the adjust here is only "adjust strings, such as MMM + * and MMMM, EEE and EEEE. + * + * @param inputSkeleton the input skeleton + * @param bestMatchSkeleton the best match skeleton + * @param bestMatchIntervalpattern the best match interval pattern + * @param differenceInfo the difference between 2 skeletons + * 1 means only field width differs + * 2 means v/z exchange + * @param suppressDayPeriodField if true, remove the day period field from the result + * @return the adjusted interval pattern + */ + private static String adjustFieldWidth(String inputSkeleton, + String bestMatchSkeleton, + String bestMatchIntervalPattern, + int differenceInfo, + boolean suppressDayPeriodField ) { + + if ( bestMatchIntervalPattern == null ) { + return null; // the 2nd part could be null + } + int[] inputSkeletonFieldWidth = new int[58]; + int[] bestMatchSkeletonFieldWidth = new int[58]; + + /* initialize as following + { + // A B C D E F G H I J K L M N O + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // P Q R S T U V W X Y Z + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // a b c d e f g h i j k l m n o + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // p q r s t u v w x y z + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + */ + + + int PATTERN_CHAR_BASE = 0x41; + + DateIntervalInfo.parseSkeleton(inputSkeleton, inputSkeletonFieldWidth); + DateIntervalInfo.parseSkeleton(bestMatchSkeleton, bestMatchSkeletonFieldWidth); + if (suppressDayPeriodField) { + if (bestMatchIntervalPattern.indexOf(" a") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, " a", ""); + } else if (bestMatchIntervalPattern.indexOf("\u00A0a") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "\u00A0a", ""); + } else if (bestMatchIntervalPattern.indexOf("\u202Fa") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "\u202Fa", ""); + } else if (bestMatchIntervalPattern.indexOf("a ") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a ", ""); + } else if (bestMatchIntervalPattern.indexOf("a\u00A0") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a\u00A0", ""); + } else if (bestMatchIntervalPattern.indexOf("a\u202F") != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a\u202F", ""); + } + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a", ""); + } + if ( differenceInfo == 2 ) { + if (inputSkeleton.indexOf('z') != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "v", "z"); + } + if (inputSkeleton.indexOf('K') != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "h", "K"); + } + if (inputSkeleton.indexOf('k') != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "H", "k"); + } + if (inputSkeleton.indexOf('b') != -1) { + bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a", "b"); + } + } + if (bestMatchIntervalPattern.indexOf('a') != -1 && bestMatchSkeletonFieldWidth['a' - PATTERN_CHAR_BASE] == 0) { + bestMatchSkeletonFieldWidth['a' - PATTERN_CHAR_BASE] = 1; + } + if (bestMatchIntervalPattern.indexOf('b') != -1 && bestMatchSkeletonFieldWidth['b' - PATTERN_CHAR_BASE] == 0) { + bestMatchSkeletonFieldWidth['b' - PATTERN_CHAR_BASE] = 1; + } + + StringBuilder adjustedPtn = new StringBuilder(bestMatchIntervalPattern); + + + boolean inQuote = false; + char prevCh = 0; + int count = 0; + + // loop through the pattern string character by character + int adjustedPtnLength = adjustedPtn.length(); + for (int i = 0; i < adjustedPtnLength; ++i) { + char ch = adjustedPtn.charAt(i); + if (ch != prevCh && count > 0) { + // check the repeativeness of pattern letter + char skeletonChar = prevCh; + if ( skeletonChar == 'L' ) { + // for skeleton "M+", the pattern is "...L..." + skeletonChar = 'M'; + } + int fieldCount = bestMatchSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE]; + int inputFieldCount = inputSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE]; + if ( fieldCount == count && inputFieldCount > fieldCount ) { + count = inputFieldCount - fieldCount; + for ( int j = 0; j < count; ++j ) { + adjustedPtn.insert(i, prevCh); + } + i += count; + adjustedPtnLength += count; + } + count = 0; + } + if (ch == '\'') { + // Consecutive single quotes are a single quote literal, + // either outside of quotes or between quotes + if ((i+1) < adjustedPtn.length() && adjustedPtn.charAt(i+1) == '\'') { + ++i; + } else { + inQuote = ! inQuote; + } + } + else if ( ! inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/) + || (ch >= 0x0041 /*'A'*/ && ch <= 0x005A /*'Z'*/))) { + // ch is a date-time pattern character + prevCh = ch; + ++count; + } + } + if ( count > 0 ) { + // last item + // check the repeativeness of pattern letter + char skeletonChar = prevCh; + if ( skeletonChar == 'L' ) { + // for skeleton "M+", the pattern is "...L..." + skeletonChar = 'M'; + } + int fieldCount = bestMatchSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE]; + int inputFieldCount = inputSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE]; + if ( fieldCount == count && inputFieldCount > fieldCount ) { + count = inputFieldCount - fieldCount; + for ( int j = 0; j < count; ++j ) { + adjustedPtn.append(prevCh); + } + } + } + return adjustedPtn.toString(); + } + + /** + * Does the same thing as String.replace(), except that it won't perform the + * substitution inside quoted literal text. + * @param targetString The string to perform the find-replace operation on. + * @param strToReplace The string to search for and replace in the target string. + * @param strToReplaceWith The string to substitute in wherever {@code stringToReplace} was found. + */ + private static String findReplaceInPattern(String targetString, + String strToReplace, + String strToReplaceWith) { + int firstQuoteIndex = targetString.indexOf("\'"); + if (firstQuoteIndex < 0) { + return targetString.replace(strToReplace, strToReplaceWith); + } else { + StringBuilder result = new StringBuilder(); + String source = targetString; + + while (firstQuoteIndex >= 0) { + int secondQuoteIndex = source.indexOf("\'", firstQuoteIndex + 1); + if (secondQuoteIndex < 0) { + secondQuoteIndex = source.length() - 1; + } + + String unquotedText = source.substring(0, firstQuoteIndex); + String quotedText = source.substring(firstQuoteIndex, secondQuoteIndex + 1); + + result.append(unquotedText.replace(strToReplace, strToReplaceWith)); + result.append(quotedText); + + source = source.substring(secondQuoteIndex + 1); + firstQuoteIndex = source.indexOf("\'"); + } + result.append(source.replace(strToReplace, strToReplaceWith)); + return result.toString(); + } + } + + + /* + * Concat a single date pattern with a time interval pattern, + * set it into the intervalPatterns, while field is time field. + * This is used to handle time interval patterns on skeleton with + * both time and date. Present the date followed by + * the range expression for the time. + * @param dtfmt date and time format + * @param datePattern date pattern + * @param field time calendar field: AM_PM, HOUR, MINUTE + * @param intervalPatterns interval patterns + */ + private void concatSingleDate2TimeInterval(String dtfmt, + String datePattern, + int field, + Map<String, PatternInfo> intervalPatterns) + { + + PatternInfo timeItvPtnInfo = + intervalPatterns.get(DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]); + if ( timeItvPtnInfo != null ) { + String timeIntervalPattern = timeItvPtnInfo.getFirstPart() + + timeItvPtnInfo.getSecondPart(); + String pattern = SimpleFormatterImpl.formatRawPattern( + dtfmt, 2, 2, timeIntervalPattern, datePattern); + timeItvPtnInfo = DateIntervalInfo.genPatternInfo(pattern, + timeItvPtnInfo.firstDateInPtnIsLaterDate()); + intervalPatterns.put( + DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field], timeItvPtnInfo); + } + // else: fall back + // it should not happen if the interval format defined is valid + } + + + /* + * check whether a calendar field present in a skeleton. + * @param field calendar field need to check + * @param skeleton given skeleton on which to check the calendar field + * @return true if field present in a skeleton. + */ + private static boolean fieldExistsInSkeleton(int field, String skeleton) + { + String fieldChar = DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]; + return ( (skeleton.indexOf(fieldChar) == -1) ? false : true ) ; + } + + + /* + * readObject. + */ + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + initializePattern(isDateIntervalInfoDefault ? LOCAL_PATTERN_CACHE : null); + // if deserialized from a release that didn't have fCapitalizationSetting, set it to default + if (fCapitalizationSetting == null) { + fCapitalizationSetting = DisplayContext.CAPITALIZATION_NONE; + } + } + + /** + * Get the internal patterns for the skeleton + * @internal CLDR + * @deprecated This API is ICU internal only. + */ + @Deprecated + public Map<String, PatternInfo> getRawPatterns() { + // this is unmodifiable, so ok to return directly + return fIntervalPatterns; + } +} |