DateIntervalInfo.java
// © 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 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.UResource;
import com.ibm.icu.impl.UResource.Key;
import com.ibm.icu.impl.UResource.Value;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUCloneNotSupportedException;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Set;
/**
* DateIntervalInfo is a public class for encapsulating localizable date time interval patterns. It
* is used by DateIntervalFormat.
*
* <p>For most users, ordinary use of DateIntervalFormat does not need to create DateIntervalInfo
* object directly. DateIntervalFormat will take care of it when creating a date interval formatter
* when user pass in skeleton and locale.
*
* <p>For power users, who want to create their own date interval patterns, or want to re-set date
* interval patterns, they could do so by directly creating DateIntervalInfo and manipulating it.
*
* <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.
* <p>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.
* <p>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 > am-pm > hour > minute > second
*
* <p>The largest different calendar fields between 2 calendars is the first different calendar
* field in above order.
*
* <p>For example: the largest different calendar fields between "Jan 10, 2007" and "Feb 20, 2008"
* is year.
*
* <p>There is a set of pre-defined static skeleton strings. 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".
*
* <p>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>There are 2 dates in interval pattern. For most locales, the first date in an interval pattern
* is the earlier date. There might be a locale in which the first date in an interval pattern is
* the later date. We use fallback format for the default order for the locale. For example, if the
* fallback format is "{0} - {1}", it means the first date in the interval pattern for this locale
* is earlier date. If the fallback format is "{1} - {0}", it means the first date is the later
* date. For a particular interval pattern, the default order can be overridden by prefixing
* "latestFirst:" or "earliestFirst:" to the interval pattern. For example, if the fallback format
* is "{0}-{1}", but for skeleton "yMMMd", the interval pattern when day is different is
* "latestFirst:d-d MMM yy", it means by default, the first date in interval pattern is the earlier
* date. But for skeleton "yMMMd", when day is different, the first date in "d-d MMM yy" is the
* later date.
*
* <p>The recommended way to create a DateIntervalFormat object is to pass in the locale. By using a
* Locale parameter, the DateIntervalFormat object is initialized with the pre-defined interval
* patterns for a given or default locale.
*
* <p>Users can also create DateIntervalFormat object by supplying their own interval patterns. It
* provides flexibility for power usage.
*
* <p>After a DateIntervalInfo object is created, clients may modify the interval patterns using
* setIntervalPattern function as so desired. Currently, users can only set interval patterns when
* the following calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, DAY_OF_WEEK,
* AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, and MILLISECOND. Interval patterns when other calendar
* fields are different is not supported.
*
* <p>DateIntervalInfo objects are cloneable. When clients obtain a DateIntervalInfo object, they
* can feel free to modify it as necessary.
*
* <p>DateIntervalInfo are not expected to be subclassed. Data for a calendar is loaded out of
* resource bundles. Through ICU 4.4, date interval patterns are only supported in the Gregorian
* calendar; non-Gregorian calendars are supported from ICU 4.4.1.
*
* @stable ICU 4.0
*/
public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, Serializable {
/* Save the interval pattern information.
* Interval pattern consists of 2 single date patterns and the separator.
* For example, interval pattern "MMM d - MMM d, yyyy" consists
* a single date pattern "MMM d", another single date pattern "MMM d, yyyy",
* and a separator "-".
* Also, the first date appears in an interval pattern could be
* the earlier date or the later date.
* And such information is saved in the interval pattern as well.
*/
static final int currentSerialVersion = 1;
/**
* PatternInfo class saves the first and second part of interval pattern, and whether the
* interval pattern is earlier date first.
*
* @stable ICU 4.0
*/
public static final class PatternInfo implements Cloneable, Serializable {
static final int currentSerialVersion = 1;
private static final long serialVersionUID = 1;
private final String fIntervalPatternFirstPart;
private final String fIntervalPatternSecondPart;
/*
* Whether the first date in interval pattern is later date or not.
* Fallback format set the default ordering.
* And for a particular interval pattern, the order can be
* overridden by prefixing the interval pattern with "latestFirst:" or
* "earliestFirst:"
* For example, given 2 date, Jan 10, 2007 to Feb 10, 2007.
* if the fallback format is "{0} - {1}",
* and the pattern is "d MMM - d MMM yyyy", the interval format is
* "10 Jan - 10 Feb, 2007".
* If the pattern is "latestFirst:d MMM - d MMM yyyy",
* the interval format is "10 Feb - 10 Jan, 2007"
*/
private final boolean fFirstDateInPtnIsLaterDate;
/**
* Constructs a <code>PatternInfo</code> object.
*
* @param firstPart The first part of interval pattern.
* @param secondPart The second part of interval pattern.
* @param firstDateInPtnIsLaterDate Whether the first date in interval patter is later date
* or not.
* @stable ICU 4.0
*/
public PatternInfo(String firstPart, String secondPart, boolean firstDateInPtnIsLaterDate) {
fIntervalPatternFirstPart = firstPart;
fIntervalPatternSecondPart = secondPart;
fFirstDateInPtnIsLaterDate = firstDateInPtnIsLaterDate;
}
/**
* Returns the first part of interval pattern.
*
* @return The first part of interval pattern.
* @stable ICU 4.0
*/
public String getFirstPart() {
return fIntervalPatternFirstPart;
}
/**
* Returns the second part of interval pattern.
*
* @return The second part of interval pattern.
* @stable ICU 4.0
*/
public String getSecondPart() {
return fIntervalPatternSecondPart;
}
/**
* Returns whether the first date in interval patter is later date or not.
*
* @return Whether the first date in interval patter is later date or not.
* @stable ICU 4.0
*/
public boolean firstDateInPtnIsLaterDate() {
return fFirstDateInPtnIsLaterDate;
}
/**
* Compares the specified object with this <code>PatternInfo</code> for equality.
*
* @param a The object to be compared.
* @return <code>true</code> if the specified object is equal to this <code>PatternInfo
* </code>.
* @stable ICU 4.0
*/
@Override
public boolean equals(Object a) {
if (a instanceof PatternInfo) {
PatternInfo patternInfo = (PatternInfo) a;
return Objects.equals(
fIntervalPatternFirstPart, patternInfo.fIntervalPatternFirstPart)
&& Objects.equals(
fIntervalPatternSecondPart, patternInfo.fIntervalPatternSecondPart)
&& fFirstDateInPtnIsLaterDate == patternInfo.fFirstDateInPtnIsLaterDate;
}
return false;
}
/**
* Returns the hash code of this <code>PatternInfo</code>.
*
* @return A hash code value for this object.
* @stable ICU 4.0
*/
@Override
public int hashCode() {
int hash = fIntervalPatternFirstPart != null ? fIntervalPatternFirstPart.hashCode() : 0;
if (fIntervalPatternSecondPart != null) {
hash ^= fIntervalPatternSecondPart.hashCode();
}
if (fFirstDateInPtnIsLaterDate) {
hash ^= -1;
}
return hash;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.0
*/
@Override
public String toString() {
return "{first=«"
+ fIntervalPatternFirstPart
+ "», second=«"
+ fIntervalPatternSecondPart
+ "», reversed:"
+ fFirstDateInPtnIsLaterDate
+ "}";
}
}
// Following is package protected since
// it is shared with DateIntervalFormat.
static final String[] CALENDAR_FIELD_TO_PATTERN_LETTER = {
"G", "y", "M",
"w", "W", "d",
"D", "E", "F",
"a", "h", "H",
"m", "s", "S", // MINUTE, SECOND, MILLISECOND
"z", " ", "Y", // ZONE_OFFSET, DST_OFFSET, YEAR_WOY
"e", "u", "g", // DOW_LOCAL, EXTENDED_YEAR, JULIAN_DAY
"A", " ", " ", // MILLISECONDS_IN_DAY, IS_LEAP_MONTH.
};
private static final long serialVersionUID = 1;
private static final int MINIMUM_SUPPORTED_CALENDAR_FIELD = Calendar.MILLISECOND;
// private static boolean DEBUG = true;
private static String CALENDAR_KEY = "calendar";
private static String INTERVAL_FORMATS_KEY = "intervalFormats";
private static String FALLBACK_STRING = "fallback";
private static String LATEST_FIRST_PREFIX = "latestFirst:";
private static String EARLIEST_FIRST_PREFIX = "earliestFirst:";
// DateIntervalInfo cache
private static final ICUCache<String, DateIntervalInfo> DIICACHE = new SimpleCache<>();
// default interval pattern on the skeleton, {0} - {1}
private String fFallbackIntervalPattern;
// default order
private boolean fFirstDateInPtnIsLaterDate = false;
// HashMap( skeleton, HashMap(largest_different_field, pattern) )
private Map<String, Map<String, PatternInfo>> fIntervalPatterns = null;
private transient volatile boolean frozen = false;
// If true, fIntervalPatterns should not be modified in-place because it
// is shared with other objects. Unlike frozen which is always true once
// set to true, this field can go from true to false as long as frozen is
// false.
private transient boolean fIntervalPatternsReadOnly = false;
/**
* Create empty instance. It does not initialize any interval patterns except that it initialize
* default fall-back pattern as "{0} - {1}", which can be reset by setFallbackIntervalPattern().
*
* <p>It should be followed by setFallbackIntervalPattern() and setIntervalPattern(), and is
* recommended to be used only for power users who wants to create their own interval patterns
* and use them to create date interval formatter.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public DateIntervalInfo() {
fIntervalPatterns = new HashMap<>();
fFallbackIntervalPattern = "{0} \u2013 {1}";
}
/**
* Construct DateIntervalInfo for the given locale,
*
* @param locale the interval patterns are loaded from the appropriate calendar data (specified
* calendar or default calendar) in this locale.
* @stable ICU 4.0
*/
public DateIntervalInfo(ULocale locale) {
initializeData(locale);
}
/**
* Construct DateIntervalInfo for the given {@link java.util.Locale}.
*
* @param locale the interval patterns are loaded from the appropriate calendar data (specified
* calendar or default calendar) in this locale.
* @stable ICU 54
*/
public DateIntervalInfo(Locale locale) {
this(ULocale.forLocale(locale));
}
/*
* Initialize the DateIntervalInfo from locale
* @param locale the given locale.
*/
private void initializeData(ULocale locale) {
String key = locale.toString();
DateIntervalInfo dii = DIICACHE.get(key);
if (dii == null) {
// initialize data from scratch
setup(locale);
// Marking fIntervalPatterns read-only makes cloning cheaper.
fIntervalPatternsReadOnly = true;
// We freeze what goes in the cache without freezing this object.
DIICACHE.put(key, clone().freeze());
} else {
initializeFromReadOnlyPatterns(dii);
}
}
/**
* Initialize this object
*
* @param dii must have read-only fIntervalPatterns.
*/
private void initializeFromReadOnlyPatterns(DateIntervalInfo dii) {
fFallbackIntervalPattern = dii.fFallbackIntervalPattern;
fFirstDateInPtnIsLaterDate = dii.fFirstDateInPtnIsLaterDate;
fIntervalPatterns = dii.fIntervalPatterns;
fIntervalPatternsReadOnly = true;
}
/** Sink for enumerating all of the date interval skeletons. */
private static final class DateIntervalSink extends UResource.Sink {
/**
* Accepted pattern letters: Calendar.YEAR Calendar.MONTH Calendar.DATE Calendar.AM_PM
* Calendar.HOUR Calendar.HOUR_OF_DAY Calendar.MINUTE Calendar.SECOND Calendar.MILLISECOND
*/
private static final String ACCEPTED_PATTERN_LETTERS = "GyMdahHmsS";
// Output data
DateIntervalInfo dateIntervalInfo;
// Alias handling
String nextCalendarType;
// Constructor
public DateIntervalSink(DateIntervalInfo dateIntervalInfo) {
this.dateIntervalInfo = dateIntervalInfo;
}
@Override
public void put(Key key, Value value, boolean noFallback) {
// Iterate over all the calendar entries and only pick the 'intervalFormats' table.
UResource.Table dateIntervalData = value.getTable();
for (int i = 0; dateIntervalData.getKeyAndValue(i, key, value); i++) {
if (!key.contentEquals(INTERVAL_FORMATS_KEY)) {
continue;
}
// Handle aliases and tables. Ignore the rest.
if (value.getType() == ICUResourceBundle.ALIAS) {
// Get the calendar type from the alias path.
nextCalendarType = getCalendarTypeFromPath(value.getAliasString());
break;
} else if (value.getType() == ICUResourceBundle.TABLE) {
// Iterate over all the skeletons in the 'intervalFormat' table.
UResource.Table skeletonData = value.getTable();
for (int j = 0; skeletonData.getKeyAndValue(j, key, value); j++) {
if (value.getType() == ICUResourceBundle.TABLE) {
// Process the skeleton
processSkeletonTable(key, value);
}
}
break;
}
}
}
/** Processes the patterns for a skeleton table. */
public void processSkeletonTable(Key key, Value value) {
// Iterate over all the patterns in the current skeleton table
String currentSkeleton = key.toString();
UResource.Table patternData = value.getTable();
for (int k = 0; patternData.getKeyAndValue(k, key, value); k++) {
if (value.getType() == ICUResourceBundle.STRING) {
// Process the key
CharSequence patternLetter = validateAndProcessPatternLetter(key);
// If the calendar field has a valid value
if (patternLetter != null) {
// Get the largest different calendar unit
String lrgDiffCalUnit = patternLetter.toString();
// Set the interval pattern
setIntervalPatternIfAbsent(currentSkeleton, lrgDiffCalUnit, value);
}
}
}
}
/**
* Returns and resets the next calendar type.
*
* @return Next calendar type
*/
public String getAndResetNextCalendarType() {
String tmpCalendarType = nextCalendarType;
nextCalendarType = null;
return tmpCalendarType;
}
// Alias' path prefix and suffix.
private static final String DATE_INTERVAL_PATH_PREFIX = "/LOCALE/" + CALENDAR_KEY + "/";
private static final String DATE_INTERVAL_PATH_SUFFIX = "/" + INTERVAL_FORMATS_KEY;
/**
* Extracts the calendar type from the path
*
* @param path
* @return Calendar Type
*/
private String getCalendarTypeFromPath(String path) {
if (path.startsWith(DATE_INTERVAL_PATH_PREFIX)
&& path.endsWith(DATE_INTERVAL_PATH_SUFFIX)) {
return path.substring(
DATE_INTERVAL_PATH_PREFIX.length(),
path.length() - DATE_INTERVAL_PATH_SUFFIX.length());
}
throw new ICUException("Malformed 'intervalFormat' alias path: " + path);
}
/**
* Processes the pattern letter
*
* @param patternLetter
* @return Pattern letter
*/
private CharSequence validateAndProcessPatternLetter(CharSequence patternLetter) {
// Check that patternLetter is just one letter
if (patternLetter.length() != 1) {
return null;
}
// Check that the pattern letter is accepted
char letter = patternLetter.charAt(0);
if (ACCEPTED_PATTERN_LETTERS.indexOf(letter) < 0 && letter != 'B') {
return null;
}
// Replace 'h' for 'H'
if (letter == CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR_OF_DAY].charAt(0)) {
patternLetter = CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR];
}
// Replace 'a' for 'B'
// TODO: Using AM/PM as a proxy for flexible day period isn’t really correct, but it’s
// close
if (letter == 'B') {
patternLetter = CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM];
}
return patternLetter;
}
/**
* Stores the interval pattern for the current skeleton in the internal data structure if
* it's not present.
*
* @param lrgDiffCalUnit
* @param intervalPattern
*/
private void setIntervalPatternIfAbsent(
String currentSkeleton, String lrgDiffCalUnit, Value intervalPattern) {
// Check if the pattern has already been stored on the data structure.
Map<String, PatternInfo> patternsOfOneSkeleton =
dateIntervalInfo.fIntervalPatterns.get(currentSkeleton);
if (patternsOfOneSkeleton == null
|| !patternsOfOneSkeleton.containsKey(lrgDiffCalUnit)) {
// Store the pattern
dateIntervalInfo.setIntervalPatternInternally(
currentSkeleton, lrgDiffCalUnit, intervalPattern.toString());
}
}
}
/*
* Initialize DateIntervalInfo from calendar data
* @param calData calendar data
*/
private void setup(ULocale locale) {
int DEFAULT_HASH_SIZE = 19;
fIntervalPatterns = new HashMap<>(DEFAULT_HASH_SIZE);
// initialize to guard if there is no interval date format defined in
// resource files
fFallbackIntervalPattern = "{0} \u2013 {1}";
try {
// Get the correct calendar type
String calendarTypeToUse = locale.getKeywordValue("calendar");
if (calendarTypeToUse == null) {
String[] preferredCalendarTypes =
Calendar.getKeywordValuesForLocale("calendar", locale, true);
calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar
}
if (calendarTypeToUse == null) {
calendarTypeToUse = "gregorian"; // fallback
}
// Instantiate the sink to process the data and the resource bundle
DateIntervalSink sink = new DateIntervalSink(this);
ICUResourceBundle resource =
(ICUResourceBundle)
UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
// Get the fallback pattern
String fallbackPattern =
resource.getStringWithFallback(
CALENDAR_KEY
+ "/"
+ calendarTypeToUse
+ "/"
+ INTERVAL_FORMATS_KEY
+ "/"
+ FALLBACK_STRING);
setFallbackIntervalPattern(fallbackPattern);
// Already loaded calendar types
Set<String> loadedCalendarTypes = new HashSet<>();
while (calendarTypeToUse != null) {
// Throw an exception when a loop is detected
if (loadedCalendarTypes.contains(calendarTypeToUse)) {
throw new ICUException("Loop in calendar type fallback: " + calendarTypeToUse);
}
// Register the calendar type to avoid loops
loadedCalendarTypes.add(calendarTypeToUse);
// Get all resources for this calendar type
String pathToIntervalFormats = CALENDAR_KEY + "/" + calendarTypeToUse;
resource.getAllItemsWithFallback(pathToIntervalFormats, sink);
// Get next calendar type to load if there was an alias pointing at it
calendarTypeToUse = sink.getAndResetNextCalendarType();
}
} catch (MissingResourceException e) {
// Will fallback to {data0} - {date1}
}
}
/*
* Split interval patterns into 2 part.
* @param intervalPattern interval pattern
* @return the index in interval pattern which split the pattern into 2 part
*/
private static int splitPatternInto2Part(String intervalPattern) {
boolean inQuote = false;
char prevCh = 0;
int count = 0;
/* repeatedPattern used to record whether a pattern has already seen.
It is a pattern applies to first calendar if it is first time seen,
otherwise, it is a pattern applies to the second calendar
*/
int[] patternRepeated = new int[58];
int PATTERN_CHAR_BASE = 0x41;
/* loop through the pattern string character by character looking for
* the first repeated pattern letter, which breaks the interval pattern
* into 2 parts.
*/
int i;
boolean foundRepetition = false;
for (i = 0; i < intervalPattern.length(); ++i) {
char ch = intervalPattern.charAt(i);
if (ch != prevCh && count > 0) {
// check the repeativeness of pattern letter
int repeated = patternRepeated[prevCh - PATTERN_CHAR_BASE];
if (repeated == 0) {
patternRepeated[prevCh - PATTERN_CHAR_BASE] = 1;
} else {
foundRepetition = true;
break;
}
count = 0;
}
if (ch == '\'') {
// Consecutive single quotes are a single quote literal,
// either outside of quotes or between quotes
if ((i + 1) < intervalPattern.length() && intervalPattern.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;
}
}
// check last pattern char, distinguish
// "dd MM" ( no repetition ),
// "d-d"(last char repeated ), and
// "d-d MM" ( repetition found )
if (count > 0 && foundRepetition == false) {
if (patternRepeated[prevCh - PATTERN_CHAR_BASE] == 0) {
count = 0;
}
}
return (i - count);
}
/**
* Provides a way for client to build interval patterns. User could construct DateIntervalInfo
* by providing a list of skeletons and their patterns.
*
* <p>For example:
*
* <pre>
* DateIntervalInfo dIntervalInfo = new DateIntervalInfo();
* dIntervalInfo.setIntervalPattern("yMd", Calendar.YEAR, "'from' yyyy-M-d 'to' yyyy-M-d");
* dIntervalInfo.setIntervalPattern("yMMMd", Calendar.MONTH, "'from' yyyy MMM d 'to' MMM d");
* dIntervalInfo.setIntervalPattern("yMMMd", Calendar.DAY, "yyyy MMM d-d");
* dIntervalInfo.setFallbackIntervalPattern("{0} ~ {1}");
* </pre>
*
* Restriction: Currently, users can only set interval patterns when the following calendar
* fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, DAY_OF_WEEK, AM_PM, HOUR,
* HOUR_OF_DAY, MINUTE, SECOND, and MILLISECOND. Interval patterns when other calendar fields
* are different are not supported.
*
* @param skeleton the skeleton on which interval pattern based
* @param lrgDiffCalUnit the largest different calendar unit.
* @param intervalPattern the interval pattern on the largest different calendar unit. For
* example, if lrgDiffCalUnit is "year", the interval pattern for en_US when year is
* different could be "'from' yyyy 'to' yyyy".
* @throws IllegalArgumentException if setting interval pattern on a calendar field that is
* smaller than the MINIMUM_SUPPORTED_CALENDAR_FIELD
* @throws UnsupportedOperationException if the object is frozen
* @stable ICU 4.0
*/
public void setIntervalPattern(String skeleton, int lrgDiffCalUnit, String intervalPattern) {
if (frozen) {
throw new UnsupportedOperationException(
"no modification is allowed after DII is frozen");
}
if (lrgDiffCalUnit > MINIMUM_SUPPORTED_CALENDAR_FIELD) {
throw new IllegalArgumentException(
"calendar field is larger than MINIMUM_SUPPORTED_CALENDAR_FIELD");
}
if (fIntervalPatternsReadOnly) {
fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
fIntervalPatternsReadOnly = false;
}
PatternInfo ptnInfo =
setIntervalPatternInternally(
skeleton,
CALENDAR_FIELD_TO_PATTERN_LETTER[lrgDiffCalUnit],
intervalPattern);
if (lrgDiffCalUnit == Calendar.HOUR_OF_DAY) {
setIntervalPattern(skeleton, CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM], ptnInfo);
setIntervalPattern(skeleton, CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR], ptnInfo);
} else if (lrgDiffCalUnit == Calendar.DAY_OF_MONTH
|| lrgDiffCalUnit == Calendar.DAY_OF_WEEK) {
setIntervalPattern(skeleton, CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE], ptnInfo);
}
}
/* Set Interval pattern.
*
* It generates the interval pattern info,
* afer which, not only sets the interval pattern info into the hash map,
* but also returns the interval pattern info to the caller
* so that caller can re-use it.
*
* @param skeleton skeleton on which the interval pattern based
* @param lrgDiffCalUnit the largest different calendar unit.
* @param intervalPattern the interval pattern on the largest different
* calendar unit.
* @return the interval pattern pattern information
*/
private PatternInfo setIntervalPatternInternally(
String skeleton, String lrgDiffCalUnit, String intervalPattern) {
Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
boolean emptyHash = false;
if (patternsOfOneSkeleton == null) {
patternsOfOneSkeleton = new HashMap<>();
emptyHash = true;
}
boolean order = fFirstDateInPtnIsLaterDate;
// check for "latestFirst:" or "earliestFirst:" prefix
if (intervalPattern.startsWith(LATEST_FIRST_PREFIX)) {
order = true;
int prefixLength = LATEST_FIRST_PREFIX.length();
intervalPattern = intervalPattern.substring(prefixLength, intervalPattern.length());
} else if (intervalPattern.startsWith(EARLIEST_FIRST_PREFIX)) {
order = false;
int earliestFirstLength = EARLIEST_FIRST_PREFIX.length();
intervalPattern =
intervalPattern.substring(earliestFirstLength, intervalPattern.length());
}
PatternInfo itvPtnInfo = genPatternInfo(intervalPattern, order);
patternsOfOneSkeleton.put(lrgDiffCalUnit, itvPtnInfo);
if (emptyHash == true) {
fIntervalPatterns.put(skeleton, patternsOfOneSkeleton);
}
return itvPtnInfo;
}
/* Set Interval pattern.
*
* @param skeleton skeleton on which the interval pattern based
* @param lrgDiffCalUnit the largest different calendar unit.
* @param ptnInfo interval pattern infomration
*/
private void setIntervalPattern(String skeleton, String lrgDiffCalUnit, PatternInfo ptnInfo) {
Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
patternsOfOneSkeleton.put(lrgDiffCalUnit, ptnInfo);
}
/**
* Break interval patterns as 2 part and save them into pattern info.
*
* @param intervalPattern interval pattern
* @param laterDateFirst whether the first date in intervalPattern is earlier date or later date
* @return pattern info object
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static PatternInfo genPatternInfo(String intervalPattern, boolean laterDateFirst) {
int splitPoint = splitPatternInto2Part(intervalPattern);
String firstPart = intervalPattern.substring(0, splitPoint);
String secondPart = null;
if (splitPoint < intervalPattern.length()) {
secondPart = intervalPattern.substring(splitPoint, intervalPattern.length());
}
return new PatternInfo(firstPart, secondPart, laterDateFirst);
}
/**
* Get the interval pattern given the largest different calendar field.
*
* @param skeleton the skeleton
* @param field the largest different calendar field
* @return interval pattern return null if interval pattern is not found.
* @throws IllegalArgumentException if getting interval pattern on a calendar field that is
* smaller than the MINIMUM_SUPPORTED_CALENDAR_FIELD
* @stable ICU 4.0
*/
public PatternInfo getIntervalPattern(String skeleton, int field) {
if (field > MINIMUM_SUPPORTED_CALENDAR_FIELD) {
throw new IllegalArgumentException("no support for field less than MILLISECOND");
}
Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
if (patternsOfOneSkeleton != null) {
PatternInfo intervalPattern =
patternsOfOneSkeleton.get(CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
if (intervalPattern != null) {
return intervalPattern;
}
}
return null;
}
/**
* Get the fallback interval pattern.
*
* @return fallback interval pattern
* @stable ICU 4.0
*/
public String getFallbackIntervalPattern() {
return fFallbackIntervalPattern;
}
/**
* Re-set the fallback interval pattern.
*
* <p>In construction, default fallback pattern is set as "{0} - {1}". And constructor taking
* locale as parameter will set the fallback pattern as what defined in the locale resource
* file.
*
* <p>This method provides a way for user to replace the fallback pattern.
*
* @param fallbackPattern fall-back interval pattern.
* @throws UnsupportedOperationException if the object is frozen
* @throws IllegalArgumentException if there is no pattern {0} or pattern {1} in
* fallbakckPattern
* @stable ICU 4.0
*/
public void setFallbackIntervalPattern(String fallbackPattern) {
if (frozen) {
throw new UnsupportedOperationException(
"no modification is allowed after DII is frozen");
}
int firstPatternIndex = fallbackPattern.indexOf("{0}");
int secondPatternIndex = fallbackPattern.indexOf("{1}");
if (firstPatternIndex == -1 || secondPatternIndex == -1) {
throw new IllegalArgumentException("no pattern {0} or pattern {1} in fallbackPattern");
}
if (firstPatternIndex > secondPatternIndex) {
fFirstDateInPtnIsLaterDate = true;
}
fFallbackIntervalPattern = fallbackPattern;
}
/**
* Get default order -- whether the first date in pattern is later date or not.
*
* <p>return default date ordering in interval pattern. true if the first date in pattern is
* later date, false otherwise.
*
* @stable ICU 4.0
*/
public boolean getDefaultOrder() {
return fFirstDateInPtnIsLaterDate;
}
/**
* Clone this object.
*
* @return a copy of the object
* @stable ICU4.0
*/
@Override
public DateIntervalInfo clone() {
if (frozen) {
return this;
}
return cloneUnfrozenDII();
}
/*
* Clone an unfrozen DateIntervalInfo object.
* @return a copy of the object
*/
private DateIntervalInfo cloneUnfrozenDII() // throws IllegalStateException
{
try {
DateIntervalInfo other = (DateIntervalInfo) super.clone();
other.fFallbackIntervalPattern = fFallbackIntervalPattern;
other.fFirstDateInPtnIsLaterDate = fFirstDateInPtnIsLaterDate;
if (fIntervalPatternsReadOnly) {
other.fIntervalPatterns = fIntervalPatterns;
other.fIntervalPatternsReadOnly = true;
} else {
other.fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
other.fIntervalPatternsReadOnly = false;
}
other.frozen = false;
return other;
} catch (CloneNotSupportedException e) {
/// CLOVER:OFF
throw new ICUCloneNotSupportedException("clone is not supported", e);
/// CLOVER:ON
}
}
private static Map<String, Map<String, PatternInfo>> cloneIntervalPatterns(
Map<String, Map<String, PatternInfo>> patterns) {
Map<String, Map<String, PatternInfo>> result = new HashMap<>();
for (Entry<String, Map<String, PatternInfo>> skeletonEntry : patterns.entrySet()) {
String skeleton = skeletonEntry.getKey();
Map<String, PatternInfo> patternsOfOneSkeleton = skeletonEntry.getValue();
Map<String, PatternInfo> oneSetPtn = new HashMap<>();
for (Entry<String, PatternInfo> calEntry : patternsOfOneSkeleton.entrySet()) {
String calField = calEntry.getKey();
PatternInfo value = calEntry.getValue();
oneSetPtn.put(calField, value);
}
result.put(skeleton, oneSetPtn);
}
return result;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.0
*/
@Override
public boolean isFrozen() {
return frozen;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.4
*/
@Override
public DateIntervalInfo freeze() {
fIntervalPatternsReadOnly = true;
frozen = true;
return this;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.4
*/
@Override
public DateIntervalInfo cloneAsThawed() {
DateIntervalInfo result = (DateIntervalInfo) (this.cloneUnfrozenDII());
return result;
}
/**
* Parse skeleton, save each field's width. It is used for looking for best match skeleton, and
* adjust pattern field width.
*
* @param skeleton skeleton to be parsed
* @param skeletonFieldWidth parsed skeleton field width
*/
static void parseSkeleton(String skeleton, int[] skeletonFieldWidth) {
int PATTERN_CHAR_BASE = 0x41;
for (int i = 0; i < skeleton.length(); ++i) {
++skeletonFieldWidth[skeleton.charAt(i) - PATTERN_CHAR_BASE];
}
}
/*
* Check whether one field width is numeric while the other is string.
*
* TODO (xji): make it general
*
* @param fieldWidth one field width
* @param anotherFieldWidth another field width
* @param patternLetter pattern letter char
* @return true if one field width is numeric and the other is string,
* false otherwise.
*/
private static boolean stringNumeric(
int fieldWidth, int anotherFieldWidth, char patternLetter) {
if (patternLetter == 'M') {
if (fieldWidth <= 2 && anotherFieldWidth > 2
|| fieldWidth > 2 && anotherFieldWidth <= 2) {
return true;
}
}
return false;
}
/*
* given an input skeleton, get the best match skeleton
* which has pre-defined interval pattern in resource file.
*
* TODO (xji): set field weight or
* isolate the functionality in DateTimePatternGenerator
* @param inputSkeleton input skeleton
* @return 0, if there is exact match for input skeleton
* 1, if there is only field width difference between
* the best match and the input skeleton
* 2, the only field difference is 'v' and 'z'
* -1, if there is calendar field difference between
* the best match and the input skeleton
*/
DateIntervalFormat.BestMatchInfo getBestSkeleton(String inputSkeleton) {
String bestSkeleton = inputSkeleton;
int[] inputSkeletonFieldWidth = new int[58];
int[] skeletonFieldWidth = new int[58];
final int DIFFERENT_FIELD = 0x1000;
final int STRING_NUMERIC_DIFFERENCE = 0x100;
final int BASE = 0x41;
// hack for certain alternate characters
// resource bundles only have time skeletons containing 'v', 'h', and 'H'
// but not time skeletons containing 'z', 'K', or 'k'
// the skeleton may also include 'a' or 'b', which never occur in the resource bundles, so
// strip them out too
boolean replacedAlternateChars = false;
if (inputSkeleton.indexOf('z') != -1
|| inputSkeleton.indexOf('k') != -1
|| inputSkeleton.indexOf('K') != -1
|| inputSkeleton.indexOf('a') != -1
|| inputSkeleton.indexOf('b') != -1) {
inputSkeleton = inputSkeleton.replace('z', 'v');
inputSkeleton = inputSkeleton.replace('k', 'H');
inputSkeleton = inputSkeleton.replace('K', 'h');
inputSkeleton = inputSkeleton.replace("a", "");
inputSkeleton = inputSkeleton.replace("b", "");
replacedAlternateChars = true;
}
parseSkeleton(inputSkeleton, inputSkeletonFieldWidth);
int bestDistance = Integer.MAX_VALUE;
// 0 means exact the same skeletons;
// 1 means having the same field, but with different length,
// 2 means only z/v, h/K, or H/k differs
// -1 means having different field.
int bestFieldDifference = 0;
for (String skeleton : fIntervalPatterns.keySet()) {
// clear skeleton field width
for (int i = 0; i < skeletonFieldWidth.length; ++i) {
skeletonFieldWidth[i] = 0;
}
parseSkeleton(skeleton, skeletonFieldWidth);
// calculate distance
int distance = 0;
int fieldDifference = 1;
for (int i = 0; i < inputSkeletonFieldWidth.length; ++i) {
int inputFieldWidth = inputSkeletonFieldWidth[i];
int fieldWidth = skeletonFieldWidth[i];
if (inputFieldWidth == fieldWidth) {
continue;
}
if (inputFieldWidth == 0) {
fieldDifference = -1;
distance += DIFFERENT_FIELD;
} else if (fieldWidth == 0) {
fieldDifference = -1;
distance += DIFFERENT_FIELD;
} else if (stringNumeric(inputFieldWidth, fieldWidth, (char) (i + BASE))) {
distance += STRING_NUMERIC_DIFFERENCE;
} else {
distance += Math.abs(inputFieldWidth - fieldWidth);
}
}
if (distance < bestDistance) {
bestSkeleton = skeleton;
bestDistance = distance;
bestFieldDifference = fieldDifference;
}
if (distance == 0) {
bestFieldDifference = 0;
break;
}
}
if (replacedAlternateChars && bestFieldDifference != -1) {
bestFieldDifference = 2;
}
return new DateIntervalFormat.BestMatchInfo(bestSkeleton, bestFieldDifference);
}
/**
* Override equals
*
* @stable ICU 4.0
*/
@Override
public boolean equals(Object a) {
if (a instanceof DateIntervalInfo) {
DateIntervalInfo dtInfo = (DateIntervalInfo) a;
return fIntervalPatterns.equals(dtInfo.fIntervalPatterns);
}
return false;
}
/**
* Override hashcode
*
* @stable ICU 4.0
*/
@Override
public int hashCode() {
return fIntervalPatterns.hashCode();
}
/**
* @internal CLDR
* @deprecated This API is ICU internal only.
*/
@Deprecated
public Map<String, Set<String>> getPatterns() {
LinkedHashMap<String, Set<String>> result = new LinkedHashMap<>();
for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) {
result.put(entry.getKey(), new LinkedHashSet<>(entry.getValue().keySet()));
}
return result;
}
/**
* Get the internal patterns, with a deep clone for safety.
*
* @internal CLDR
* @deprecated This API is ICU internal only.
*/
@Deprecated
public Map<String, Map<String, PatternInfo>> getRawPatterns() {
LinkedHashMap<String, Map<String, PatternInfo>> result = new LinkedHashMap<>();
for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) {
result.put(entry.getKey(), new LinkedHashMap<>(entry.getValue()));
}
return result;
}
} // end class DateIntervalInfo