/* * Copyright (C) 2008-2010, International Business Machines * Corporation and others. All Rights Reserved. */ package com.ibm.icu.text; import java.io.IOException; import java.io.ObjectInputStream; import java.text.FieldPosition; import java.text.ParsePosition; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import com.ibm.icu.impl.CalendarData; import com.ibm.icu.impl.ICUCache; import com.ibm.icu.impl.SimpleCache; import com.ibm.icu.text.DateIntervalInfo.PatternInfo; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.DateInterval; import com.ibm.icu.util.ULocale; /** * DateIntervalFormat is a class for formatting and parsing date * intervals in a language-independent manner. * Only formatting is supported. Parsing is not supported. * *
* 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. * *
* 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. * *
* 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. * *
* Logically, the interval patterns are mappings * from (skeleton, the_largest_different_calendar_field) * to (date_interval_pattern). * *
* A skeleton *
* The calendar fields we support for interval formatting are: * year, month, date, day-of-week, am-pm, hour, hour-of-day, and minute. * Those calendar fields can be defined in the following order: * year > month > date > hour (in day) > minute * * 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. * *
* 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}". * *
* 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. * *
* 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}". * *
* For the combination of date and time, * The rule to genearte interval patterns are: *
* 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". * *
* 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: *
* 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. * *
* Code Sample: general usage *
* * // 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( * YEAR_MONTH_DAY, Locale("en", "GB", "")); * StringBuffer str = new StringBuffer(""); * FieldPosition pos = new FieldPosition(0); * // formatting * dtIntervalFmt.format(dtInterval, dateIntervalString, pos); * ** *
* Code Sample: for powerful users who wants to use their own interval pattern *
* * 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, and MINUTE 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); * * ** @stable ICU 4.0 */ public class DateIntervalFormat extends UFormat { 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; } } // Cache for the locale interval pattern private static ICUCache
* 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. * *
* 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) { DateIntervalInfo dtitvinf = new DateIntervalInfo(locale); return new DateIntervalFormat(locale, dtitvinf, skeleton); } /** * Construct a DateIntervalFormat from skeleton * DateIntervalInfo, and default locale. * * This is a convenient override of * getInstance(String skeleton, ULocale locale, DateIntervalInfo dtitvinf) * with the locale value as default locale. * * @param skeleton the skeleton on which interval format based. * @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, DateIntervalInfo dtitvinf) { return getInstance(skeleton, ULocale.getDefault(), 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) * * @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. * *
* 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. * *
* 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) { LOCAL_PATTERN_CACHE.clear(); // clone. If it is frozen, clone returns itself, otherwise, clone // returns a copy. dtitvinf = (DateIntervalInfo)dtitvinf.clone(); return new DateIntervalFormat(locale, dtitvinf, skeleton); } /** * Clone this Format object polymorphically. * @return A copy of the object. * @stable ICU 4.0 */ public 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.fSkeleton = fSkeleton; other.fIntervalPatterns = fIntervalPatterns; 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. * @return Reference to 'appendTo' parameter. * @throws IllegalArgumentException if the formatted object is not * DateInterval object * @stable ICU 4.0 */ 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. * @return Reference to 'appendTo' parameter. * @stable ICU 4.0 */ public final StringBuffer format(DateInterval dtInterval, StringBuffer appendTo, FieldPosition fieldPosition) { fFromCalendar.setTimeInMillis(dtInterval.getFromDate()); fToCalendar.setTimeInMillis(dtInterval.getToDate()); return format(fFromCalendar, fToCalendar, appendTo, fieldPosition); } /** * 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. * @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) { // not support different calendar types and time zones if ( !fromCalendar.isEquivalentTo(toCalendar) ) { throw new IllegalArgumentException("can not format on two different calendars"); } // 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 { /* ignore the second/millisecond etc. small fields' difference. * use single date when all the above are the same. */ return fDateFormat.format(fromCalendar, appendTo, pos); } // 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); } return fallbackFormat(fromCalendar, toCalendar, appendTo, pos); } // 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, appendTo, pos, intervalPattern.getSecondPart()); } Calendar firstCal; Calendar secondCal; if ( intervalPattern.firstDateInPtnIsLaterDate() ) { firstCal = toCalendar; secondCal = fromCalendar; } else { 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); if ( intervalPattern.getSecondPart() != null ) { fDateFormat.applyPattern(intervalPattern.getSecondPart()); fDateFormat.format(secondCal, appendTo, pos); } fDateFormat.applyPattern(originalPattern); return appendTo; } /* * 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, StringBuffer appendTo, FieldPosition pos) { // the fall back StringBuffer earlierDate = new StringBuffer(64); earlierDate = fDateFormat.format(fromCalendar, earlierDate, pos); StringBuffer laterDate = new StringBuffer(64); laterDate = fDateFormat.format(toCalendar, laterDate, pos); String fallbackPattern = fInfo.getFallbackIntervalPattern(); String fallback = MessageFormat.format(fallbackPattern, new Object[] {earlierDate.toString(), laterDate.toString()}); appendTo.append(fallback); 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, StringBuffer appendTo, FieldPosition pos, String fullPattern) { String originalPattern = fDateFormat.toPattern(); fDateFormat.applyPattern(fullPattern); fallbackFormat(fromCalendar, toCalendar, appendTo, pos); fDateFormat.applyPattern(originalPattern); return appendTo; } /** * Date interval parsing is not supported. *
* This method should handle parsing of * date time interval strings into Formattable objects with * DateInterval type, which is a pair of UDate. *
*
* 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. *
* When parsing, leading whitespace is discarded (with a successful parse), * while trailing whitespace is left as is. *
* 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 Formattable* object, or NULL
* on failure.
* @internal
* @deprecated This API is ICU internal only.
*/
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();
fInfo.freeze(); // freeze it
LOCAL_PATTERN_CACHE.clear();
if ( fDateFormat != null ) {
initializePattern();
}
}
/**
* Gets the date formatter
* @return a copy of the date formatter associated with
* this date interval formatter.
* @stable ICU 4.0
*/
public 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() {
String fullPattern = fDateFormat.toPattern();
ULocale locale = fDateFormat.getLocale();
String key;
if ( fSkeleton != null ) {
key = locale.toString() + "+" + fullPattern + "+" + fSkeleton;
} else {
key = locale.toString() + "+" + fullPattern;
}
Map