DateTimePatternGenerator.java
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
********************************************************************************
* Copyright (C) 2006-2016, Google, 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.PatternTokenizer;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUCloneNotSupportedException;
import com.ibm.icu.util.Region;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import com.ibm.icu.util.UResourceBundle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* This class provides flexible generation of date format patterns, like "yy-MM-dd". The user can
* build up the generator by adding successive patterns. Once that is done, a query can be made
* using a "skeleton", which is a pattern which just includes the desired fields and lengths. The
* generator will return the "best fit" pattern corresponding to that skeleton.
*
* <p>The main method people will use is getBestPattern(String skeleton), since normally this class
* is pre-built with data from a particular locale. However, generators can be built directly from
* other data as well.
*
* @stable ICU 3.6
*/
public class DateTimePatternGenerator implements Freezable<DateTimePatternGenerator>, Cloneable {
private static final boolean DEBUG = false;
// debugging flags
// static boolean SHOW_DISTANCE = false;
// TODO add hack to fix months for CJK, as per bug ticket 1099
/**
* Create empty generator, to be constructed with addPattern(...) etc.
*
* @stable ICU 3.6
*/
public static DateTimePatternGenerator getEmptyInstance() {
DateTimePatternGenerator instance = new DateTimePatternGenerator();
instance.addCanonicalItems();
instance.fillInMissing();
return instance;
}
/**
* Only for use by subclasses
*
* @stable ICU 3.6
*/
protected DateTimePatternGenerator() {}
/**
* Construct a flexible generator according to data for the default <code>FORMAT</code> locale.
*
* @see Category#FORMAT
* @stable ICU 3.6
*/
public static DateTimePatternGenerator getInstance() {
return getInstance(ULocale.getDefault(Category.FORMAT));
}
/**
* Construct a flexible generator according to data for a given locale.
*
* @param uLocale The locale to pass.
* @stable ICU 3.6
*/
public static DateTimePatternGenerator getInstance(ULocale uLocale) {
return getFrozenInstance(uLocale).cloneAsThawed();
}
/**
* Construct a flexible generator according to data for a given locale.
*
* @param locale The {@link java.util.Locale} to pass.
* @stable ICU 54
*/
public static DateTimePatternGenerator getInstance(Locale locale) {
return getInstance(ULocale.forLocale(locale));
}
/**
* Construct a frozen instance of DateTimePatternGenerator for a given locale. This method
* returns a cached frozen instance of DateTimePatternGenerator, so less expensive than the
* regular factory method.
*
* @param uLocale The locale to pass.
* @return A frozen DateTimePatternGenerator.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static DateTimePatternGenerator getFrozenInstance(ULocale uLocale) {
String localeKey = uLocale.toString();
DateTimePatternGenerator result = DTPNG_CACHE.get(localeKey);
if (result != null) {
return result;
}
result = new DateTimePatternGenerator();
result.initData(uLocale, false);
// freeze and cache
result.freeze();
DTPNG_CACHE.put(localeKey, result);
return result;
}
/**
* Construct a non-frozen instance of DateTimePatternGenerator for a given locale that skips
* using the standard date and time patterns. Because this is different than the normal instance
* for the locale, it does not set or use the cache.
*
* @param uLocale The locale to pass.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static DateTimePatternGenerator getInstanceNoStdPat(ULocale uLocale) {
DateTimePatternGenerator result = new DateTimePatternGenerator();
result.initData(uLocale, true);
return result;
}
private void initData(ULocale uLocale, boolean skipStdPatterns) {
// This instance of PatternInfo is required for calling some functions. It is used for
// passing additional information to the caller. We won't use this extra information, but
// we still need to make a temporary instance.
PatternInfo returnInfo = new PatternInfo();
addCanonicalItems();
if (!skipStdPatterns) { // skip to prevent circular dependency when used by Calendar
addICUPatterns(returnInfo, uLocale);
}
addCLDRData(returnInfo, uLocale);
if (!skipStdPatterns) { // also skip to prevent circular dependency from Calendar
setDateTimeFromCalendar(uLocale);
} else {
// instead, since from Calendar we do not care about dateTimePattern, use a fallback
setDateTimeFormat("{1} {0}");
}
setDecimalSymbols(uLocale);
getAllowedHourFormats(uLocale);
fillInMissing();
}
private void addICUPatterns(PatternInfo returnInfo, ULocale uLocale) {
// first load with the ICU patterns
ICUResourceBundle rb =
(ICUResourceBundle)
UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, uLocale);
String calendarTypeToUse = getCalendarTypeToUse(uLocale);
// TODO: See ICU-22867
ICUResourceBundle dateTimePatterns =
rb.getWithFallback("calendar/" + calendarTypeToUse + "/DateTimePatterns");
if (dateTimePatterns.getType() != UResourceBundle.ARRAY || dateTimePatterns.getSize() < 8) {
throw new MissingResourceException(
"Resource in wrong format",
"ICUResourceBundle",
"calendar/" + calendarTypeToUse + "/DateTimePatterns");
}
for (int i = 0; i < 8; i++) { // no constants available for the resource indexes
String pattern;
UResourceBundle patternRes = dateTimePatterns.get(i);
switch (patternRes.getType()) {
case UResourceBundle.STRING:
pattern = patternRes.getString();
break;
case UResourceBundle.ARRAY:
pattern = patternRes.getString(0);
break;
default:
throw new MissingResourceException(
"Resource in wrong format",
"ICUResourceBundle",
"calendar/" + calendarTypeToUse + "/DateTimePatterns");
}
addPattern(pattern, false, returnInfo);
}
}
private String getCalendarTypeToUse(ULocale uLocale) {
// Get the correct calendar type
// TODO: C++ and Java are inconsistent (see #9952).
String calendarTypeToUse = uLocale.getKeywordValue("calendar");
if (calendarTypeToUse == null) {
String[] preferredCalendarTypes =
Calendar.getKeywordValuesForLocale("calendar", uLocale, true);
calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar
}
if (calendarTypeToUse == null) {
calendarTypeToUse = "gregorian"; // fallback
}
return calendarTypeToUse;
}
private void consumeShortTimePattern(String shortTimePattern, PatternInfo returnInfo) {
// keep this pattern to populate other time field
// combination patterns by hackTimes later in this method.
// ICU-20383 No longer set defaultHourFormatChar to the hour format character from
// this pattern; instead it is set from LOCALE_TO_ALLOWED_HOUR which now
// includes entries for both preferred and allowed formats.
// some languages didn't add mm:ss or HH:mm, so put in a hack to compute that from the short
// time.
hackTimes(returnInfo, shortTimePattern);
}
private class AppendItemFormatsSink extends UResource.Sink {
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
int field = getAppendFormatNumber(key);
if (field < 0) {
return;
}
if (getAppendItemFormat(field) == null) {
setAppendItemFormat(field, value.toString());
}
}
}
private class AppendItemNamesSink extends UResource.Sink {
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
int fieldAndWidth = getCLDRFieldAndWidthNumber(key);
if (fieldAndWidth == -1) {
return;
}
int field = fieldAndWidth / DisplayWidth.COUNT;
DisplayWidth width = CLDR_FIELD_WIDTH[fieldAndWidth % DisplayWidth.COUNT];
UResource.Table detailsTable = value.getTable();
if (detailsTable.findValue("dn", value)) {
if (getFieldDisplayName(field, width) == null) {
setFieldDisplayName(field, width, value.toString());
}
}
}
}
private void fillInMissing() {
for (int i = 0; i < TYPE_LIMIT; ++i) {
if (getAppendItemFormat(i) == null) {
setAppendItemFormat(i, "{0} \u251C{2}: {1}\u2524");
}
if (getFieldDisplayName(i, DisplayWidth.WIDE) == null) {
setFieldDisplayName(i, DisplayWidth.WIDE, "F" + i);
}
if (getFieldDisplayName(i, DisplayWidth.ABBREVIATED) == null) {
setFieldDisplayName(
i, DisplayWidth.ABBREVIATED, getFieldDisplayName(i, DisplayWidth.WIDE));
}
if (getFieldDisplayName(i, DisplayWidth.NARROW) == null) {
setFieldDisplayName(
i, DisplayWidth.NARROW, getFieldDisplayName(i, DisplayWidth.ABBREVIATED));
}
}
}
private class AvailableFormatsSink extends UResource.Sink {
PatternInfo returnInfo;
public AvailableFormatsSink(PatternInfo returnInfo) {
this.returnInfo = returnInfo;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
String formatKey = key.toString();
if (!isAvailableFormatSet(formatKey)) {
setAvailableFormat(formatKey);
// Add pattern with its associated skeleton. Override any duplicate derived from std
// patterns,
// but not a previous availableFormats entry:
String formatValue = value.toString();
addPatternWithSkeleton(formatValue, formatKey, true, returnInfo);
}
}
}
private void addCLDRData(PatternInfo returnInfo, ULocale uLocale) {
ICUResourceBundle rb =
(ICUResourceBundle)
UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, uLocale);
String calendarTypeToUse = getCalendarTypeToUse(uLocale);
// ICU4J getWithFallback does not work well when
// 1) A nested table is an alias to /LOCALE/...
// 2) getWithFallback is called multiple times for going down hierarchical resource
// path
// #9987 resolved the issue of alias table when full path is specified in
// getWithFallback,
// but there is no easy solution when the equivalent operation is done by multiple
// operations.
// This issue is addressed in #9964.
// Load append item formats.
AppendItemFormatsSink appendItemFormatsSink = new AppendItemFormatsSink();
try {
rb.getAllChildrenWithFallback(
"calendar/" + calendarTypeToUse + "/appendItems", appendItemFormatsSink);
} catch (MissingResourceException e) {
}
// Load CLDR item names.
AppendItemNamesSink appendItemNamesSink = new AppendItemNamesSink();
try {
rb.getAllChildrenWithFallback("fields", appendItemNamesSink);
} catch (MissingResourceException e) {
}
// Load the available formats from CLDR.
AvailableFormatsSink availableFormatsSink = new AvailableFormatsSink(returnInfo);
try {
rb.getAllChildrenWithFallback(
"calendar/" + calendarTypeToUse + "/availableFormats", availableFormatsSink);
} catch (MissingResourceException e) {
}
}
private void setDateTimeFromCalendar(ULocale uLocale) {
Calendar cal = Calendar.getInstance(uLocale);
for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
String dateTimeFormat = Calendar.getDateAtTimePattern(cal, uLocale, style);
setDateTimeFormat(style, dateTimeFormat);
}
}
private void setDecimalSymbols(ULocale uLocale) {
// decimal point for seconds
DecimalFormatSymbols dfs = new DecimalFormatSymbols(uLocale);
setDecimal(String.valueOf(dfs.getDecimalSeparator()));
}
private static final String[] LAST_RESORT_ALLOWED_HOUR_FORMAT = {"H"};
private String[] getAllowedHourFormatsLangCountry(String language, String country) {
String langCountry = language + "_" + country;
String[] list = LOCALE_TO_ALLOWED_HOUR.get(langCountry);
if (list == null) {
list = LOCALE_TO_ALLOWED_HOUR.get(country);
}
return list;
}
private void getAllowedHourFormats(ULocale uLocale) {
// key can be either region or locale (lang_region)
// ZW{
// allowed{
// "h",
// "H",
// }
// preferred{"h"}
// }
// af_ZA{
// allowed{
// "h",
// "H",
// "hB",
// "hb",
// }
// preferred{"h"}
// }
String language = uLocale.getLanguage();
String country = ULocale.getRegionForSupplementalData(uLocale, false);
if (language.isEmpty() || country.isEmpty()) {
// Note: addLikelySubtags is documented not to throw in Java,
// unlike in C++.
ULocale max = ULocale.addLikelySubtags(uLocale);
language = max.getLanguage();
country = max.getCountry();
}
if (language.isEmpty()) {
// Unexpected, but fail gracefully
language = "und";
}
if (country.isEmpty()) {
country = "001";
}
String[] list = getAllowedHourFormatsLangCountry(language, country);
// We need to check if there is an hour cycle on locale
Character defaultCharFromLocale = null;
String hourCycle = uLocale.getKeywordValue("hours");
if (hourCycle != null) {
switch (hourCycle) {
case "h24":
defaultCharFromLocale = 'k';
break;
case "h23":
defaultCharFromLocale = 'H';
break;
case "h12":
defaultCharFromLocale = 'h';
break;
case "h11":
defaultCharFromLocale = 'K';
break;
}
}
// Check if the region has an alias
if (list == null) {
try {
Region region = Region.getInstance(country);
country = region.toString();
list = getAllowedHourFormatsLangCountry(language, country);
} catch (IllegalArgumentException e) {
// invalid region; fall through
}
}
if (list != null) {
defaultHourFormatChar =
defaultCharFromLocale != null ? defaultCharFromLocale : list[0].charAt(0);
if (list.length > 1) {
allowedHourFormats = Arrays.copyOfRange(list, 1, list.length);
} else {
allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT;
}
} else {
allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT;
defaultHourFormatChar =
(defaultCharFromLocale != null)
? defaultCharFromLocale
: allowedHourFormats[0].charAt(0);
}
}
private static class DayPeriodAllowedHoursSink extends UResource.Sink {
HashMap<String, String[]> tempMap;
private DayPeriodAllowedHoursSink(HashMap<String, String[]> tempMap) {
this.tempMap = tempMap;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Table timeData = value.getTable();
for (int i = 0; timeData.getKeyAndValue(i, key, value); ++i) {
String regionOrLocale = key.toString();
UResource.Table formatList = value.getTable();
String[] allowed = null;
String preferred = null;
for (int j = 0; formatList.getKeyAndValue(j, key, value); ++j) {
if (key.contentEquals("allowed")) {
allowed = value.getStringArrayOrStringAsArray();
} else if (key.contentEquals("preferred")) {
preferred = value.getString();
}
}
// below we construct a list[] that has an entry for the "preferred" value at [0],
// followed by 1 or more entries for the "allowed" values.
String[] list = null;
if (allowed != null && allowed.length > 0) {
list = new String[allowed.length + 1];
list[0] = (preferred != null) ? preferred : allowed[0];
System.arraycopy(allowed, 0, list, 1, allowed.length);
} else {
// fallback handling for missing data
list = new String[2];
list[0] = (preferred != null) ? preferred : LAST_RESORT_ALLOWED_HOUR_FORMAT[0];
list[1] = list[0];
}
tempMap.put(regionOrLocale, list);
}
}
}
// Get the data for dayperiod C.
static final Map<String, String[]> LOCALE_TO_ALLOWED_HOUR;
static {
HashMap<String, String[]> temp = new HashMap<>();
ICUResourceBundle suppData =
(ICUResourceBundle)
ICUResourceBundle.getBundleInstance(
ICUData.ICU_BASE_NAME,
"supplementalData",
ICUResourceBundle.ICU_DATA_CLASS_LOADER);
DayPeriodAllowedHoursSink allowedHoursSink = new DayPeriodAllowedHoursSink(temp);
suppData.getAllItemsWithFallback("timeData", allowedHoursSink);
LOCALE_TO_ALLOWED_HOUR = Collections.unmodifiableMap(temp);
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public char getDefaultHourFormatChar() {
return defaultHourFormatChar;
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public void setDefaultHourFormatChar(char defaultHourFormatChar) {
this.defaultHourFormatChar = defaultHourFormatChar;
}
private void hackTimes(PatternInfo returnInfo, String shortTimePattern) {
fp.set(shortTimePattern);
StringBuilder mmss = new StringBuilder();
// to get mm:ss, we strip all but mm literal ss
boolean gotMm = false;
for (int i = 0; i < fp.items.size(); ++i) {
Object item = fp.items.get(i);
if (item instanceof String) {
if (gotMm) {
mmss.append(fp.quoteLiteral(item.toString()));
}
} else {
char ch = item.toString().charAt(0);
if (ch == 'm') {
gotMm = true;
mmss.append(item);
} else if (ch == 's') {
if (!gotMm) {
break; // failed
}
mmss.append(item);
addPattern(mmss.toString(), false, returnInfo);
break;
} else if (gotMm || ch == 'z' || ch == 'Z' || ch == 'v' || ch == 'V') {
break; // failed
}
}
}
// to get hh:mm, we strip (literal ss) and (literal S)
// the easiest way to do this is to mark the stuff we want to nuke, then remove it in a
// second pass.
BitSet variables = new BitSet();
BitSet nuke = new BitSet();
for (int i = 0; i < fp.items.size(); ++i) {
Object item = fp.items.get(i);
if (item instanceof VariableField) {
variables.set(i);
char ch = item.toString().charAt(0);
if (ch == 's' || ch == 'S') {
nuke.set(i);
for (int j = i - 1; j >= 0; ++j) {
if (variables.get(j)) break;
nuke.set(i);
}
}
}
}
String hhmm = getFilteredPattern(fp, nuke);
addPattern(hhmm, false, returnInfo);
}
private static String getFilteredPattern(FormatParser fp, BitSet nuke) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < fp.items.size(); ++i) {
if (nuke.get(i)) continue;
Object item = fp.items.get(i);
if (item instanceof String) {
result.append(fp.quoteLiteral(item.toString()));
} else {
result.append(item.toString());
}
}
return result.toString();
}
/*private static int getAppendNameNumber(String string) {
for (int i = 0; i < CLDR_FIELD_NAME.length; ++i) {
if (CLDR_FIELD_NAME[i].equals(string)) return i;
}
return -1;
}*/
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static int getAppendFormatNumber(UResource.Key key) {
for (int i = 0; i < CLDR_FIELD_APPEND.length; ++i) {
if (key.contentEquals(CLDR_FIELD_APPEND[i])) {
return i;
}
}
return -1;
}
/**
* @internal CLDR
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static int getAppendFormatNumber(String string) {
for (int i = 0; i < CLDR_FIELD_APPEND.length; ++i) {
if (CLDR_FIELD_APPEND[i].equals(string)) {
return i;
}
}
return -1;
}
private static int getCLDRFieldAndWidthNumber(UResource.Key key) {
for (int i = 0; i < CLDR_FIELD_NAME.length; ++i) {
for (int j = 0; j < DisplayWidth.COUNT; ++j) {
String fullKey = CLDR_FIELD_NAME[i].concat(CLDR_FIELD_WIDTH[j].cldrKey());
if (key.contentEquals(fullKey)) {
return i * DisplayWidth.COUNT + j;
}
}
}
return -1;
}
/**
* Return the best pattern matching the input skeleton. It is guaranteed to have all of the
* fields in the skeleton.
* <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:getBestPatternExample -->
*
* <p>Example code:
*
* <pre>
* import java.util.Date;
*
* import com.ibm.icu.text.DateFormat;
* import com.ibm.icu.text.DateTimePatternGenerator;
* import com.ibm.icu.text.SimpleDateFormat;
* import com.ibm.icu.util.GregorianCalendar;
* import com.ibm.icu.util.TimeZone;
* import com.ibm.icu.util.ULocale;
* ...
* final String[] skeletons = {
* "yQQQQ", // year + full name of quarter, i.e., 4th quarter 1999
* "yMMMM", // year + full name of month, i.e., October 1999
* "MMMMd", // full name of month + day of the month, i.e., October 25
* "hhmm", // 12-hour-cycle format, i.e., 1:32 PM
* "jjmm" // preferred hour format for the given locale, i.e., 24-hour-cycle format for fr_FR
* };
* final ULocale[] locales = {
* new ULocale ("en_US"),
* new ULocale ("fr_FR"),
* new ULocale ("zh_CN"),
* };
* DateTimePatternGenerator dtfg = null;
* Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
* System.out.printf("%-20s%-35s%-35s%-35s\n\n", "Skeleton", "en_US", "fr_FR","zh_CN");
* for (String skeleton:skeletons) {
* System.out.printf("%-20s", skeleton);
* for (ULocale locale:locales) {
* // create a DateTimePatternGenerator instance for given locale
* dtfg = DateTimePatternGenerator.getInstance(locale);
* // use getBestPattern method to get the best pattern for the given skeleton
* String pattern = dtfg.getBestPattern(skeleton);
* // Constructs a SimpleDateFormat with the best pattern generated above and the given locale
* SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
* // Get the format of the given date
* System.out.printf("%-35s",sdf.format(date));
* }
* System.out.println("\n");
* }
* /** output of the sample code:
* *************************************************************************************************************
* Skeleton en_US fr_FR zh_CN
* yQQQQ 4th quarter 1999 4e trimestre 1999 1999年第四季度
* yMMMM October 1999 octobre 1999 1999年10月
* MMMMd October 13 13 octobre 10月13日
* hhmm 11:58 PM 11:58 PM 下午11:58
* jjmm 11:58 PM 23:58 下午11:58
* **************************************************************************************************************<code>/</code>
* // Use DateTime.getPatternInstance to produce the same Date/Time format with predefined constant field value
* final String[] dtfskeleton = {
* DateFormat.YEAR_QUARTER, // year + full name of quarter, i.e., 4th quarter 1999
* DateFormat.YEAR_MONTH, // year + full name of month, i.e., October 1999
* DateFormat.MONTH_DAY // full name of month + day of the month, i.e., October 25
* };
* System.out.printf("%-20s%-35s%-35s%-35s\n\n", "Skeleton", "en_US", "fr_FR","zh_CN");
* for (String skeleton:dtfskeleton) {
* System.out.printf("%-20s", skeleton);
* for (ULocale locale:locales) {
* // Use DateFormat.getPatternInstance to get the date/time format for the locale,
* // and apply the format to the given date
* String df=DateFormat.getPatternInstance(skeleton,locale).format(date);
* System.out.printf("%-35s",df);
* }
* System.out.println("\n");
* }
*
* /** output of the sample code:
* ************************************************************************************************************
* Skeleton en_US fr_FR zh_CN
* yQQQQ 4th quarter 1999 4e trimestre 1999 1999年第四季度
* yMMMM October 1999 octobre 1999 1999年10月
* MMMMd October 13 13 octobre 10月13日
* ************************************************************************************************************<code>/</code>
* </pre>
*
* @param skeleton The skeleton is a pattern containing only the variable fields. For example,
* "MMMdd" and "mmhh" are skeletons.
* @return Best pattern matching the input skeleton.
* @stable ICU 3.6
*/
public String getBestPattern(String skeleton) {
return getBestPattern(skeleton, null, MATCH_NO_OPTIONS);
}
/**
* Return the best pattern matching the input skeleton. It is guaranteed to have all of the
* fields in the skeleton.
*
* @param skeleton The skeleton is a pattern containing only the variable fields. For example,
* "MMMdd" and "mmhh" are skeletons.
* @param options MATCH_xxx options for forcing the length of specified fields in the returned
* pattern to match those in the skeleton (when this would not happen otherwise). For
* default behavior, use MATCH_NO_OPTIONS.
* @return Best pattern matching the input skeleton (and options).
* @stable ICU 4.4
*/
public String getBestPattern(String skeleton, int options) {
return getBestPattern(skeleton, null, options);
}
/*
* getBestPattern which takes optional skip matcher
*/
private String getBestPattern(String skeleton, DateTimeMatcher skipMatcher, int options) {
EnumSet<DTPGflags> flags = EnumSet.noneOf(DTPGflags.class);
// Replace hour metacharacters 'j', 'C', and 'J', set flags as necessary
String skeletonMapped = mapSkeletonMetacharacters(skeleton, flags);
String datePattern, timePattern;
synchronized (this) {
current.set(skeletonMapped, fp, false);
PatternWithMatcher bestWithMatcher =
getBestRaw(current, -1, _distanceInfo, skipMatcher);
if (_distanceInfo.missingFieldMask == 0 && _distanceInfo.extraFieldMask == 0) {
// we have a good item. Adjust the field types
return adjustFieldTypes(bestWithMatcher, current, flags, options);
}
int neededFields = current.getFieldMask();
// otherwise break up by date and time.
datePattern =
getBestAppending(
current,
neededFields & DATE_MASK,
_distanceInfo,
skipMatcher,
flags,
options);
timePattern =
getBestAppending(
current,
neededFields & TIME_MASK,
_distanceInfo,
skipMatcher,
flags,
options);
}
if (datePattern == null) return timePattern == null ? "" : timePattern;
if (timePattern == null) return datePattern;
// determine which dateTimeFormat to use
String canonicalSkeleton =
current.toCanonicalString(); // month fields use M, weekday fields use E
int style = DateFormat.SHORT;
int monthFieldLen = 0;
int monthFieldOffset = canonicalSkeleton.indexOf('M');
if (monthFieldOffset >= 0) {
monthFieldLen = 1 + canonicalSkeleton.lastIndexOf('M') - monthFieldOffset;
}
if (monthFieldLen == 4) {
if (canonicalSkeleton.indexOf('E') >= 0) {
style = DateFormat.FULL;
} else {
style = DateFormat.LONG;
}
} else if (monthFieldLen == 3) {
style = DateFormat.MEDIUM;
}
// and now use it to compose date and time
return SimpleFormatterImpl.formatRawPattern(
getDateTimeFormat(style), 2, 2, timePattern, datePattern);
}
/*
* Map a skeleton that may have metacharacters jJC to one without, by replacing
* the metacharacters with locale-appropriate fields of of h/H/k/K and of a/b/B
* (depends on defaultHourFormatChar and allowedHourFormats being set, which in
* turn depends on initData having been run). This method also updates the flags
* as necessary. Returns the updated skeleton.
*/
private String mapSkeletonMetacharacters(String skeleton, EnumSet<DTPGflags> flags) {
StringBuilder skeletonCopy = new StringBuilder();
boolean inQuoted = false;
for (int patPos = 0; patPos < skeleton.length(); patPos++) {
char patChr = skeleton.charAt(patPos);
if (patChr == '\'') {
inQuoted = !inQuoted;
} else if (!inQuoted) {
// Handle special mappings for 'j' and 'C' in which fields lengths
// 1,3,5 => hour field length 1
// 2,4,6 => hour field length 2
// 1,2 => abbreviated dayPeriod (field length 1..3)
// 3,4 => long dayPeriod (field length 4)
// 5,6 => narrow dayPeriod (field length 5)
if (patChr == 'j' || patChr == 'C') {
int extraLen = 0; // 1 less than total field length
while (patPos + 1 < skeleton.length()
&& skeleton.charAt(patPos + 1) == patChr) {
extraLen++;
patPos++;
}
int hourLen = 1 + (extraLen & 1);
int dayPeriodLen = (extraLen < 2) ? 1 : 3 + (extraLen >> 1);
char hourChar = 'h';
char dayPeriodChar = 'a';
if (patChr == 'j') {
hourChar = defaultHourFormatChar;
} else { // patChr == 'C'
String bestAllowed = allowedHourFormats[0];
hourChar = bestAllowed.charAt(0);
// in #13183 just add b/B to skeleton, no longer need to set special flags
char last = bestAllowed.charAt(bestAllowed.length() - 1);
if (last == 'b' || last == 'B') {
dayPeriodChar = last;
}
}
if (hourChar == 'H' || hourChar == 'k') {
dayPeriodLen = 0;
}
while (dayPeriodLen-- > 0) {
skeletonCopy.append(dayPeriodChar);
}
while (hourLen-- > 0) {
skeletonCopy.append(hourChar);
}
} else if (patChr == 'J') {
// Get pattern for skeleton with H, then (in adjustFieldTypes)
// replace H or k with defaultHourFormatChar
skeletonCopy.append('H');
flags.add(DTPGflags.SKELETON_USES_CAP_J);
} else {
skeletonCopy.append(patChr);
}
}
}
return skeletonCopy.toString();
}
/**
* PatternInfo supplies output parameters for addPattern(...). It is used because Java doesn't
* have real output parameters. It is treated like a struct (eg Point), so all fields are
* public.
*
* @stable ICU 3.6
*/
public static final class PatternInfo { // struct for return information
/**
* @stable ICU 3.6
*/
public static final int OK = 0;
/**
* @stable ICU 3.6
*/
public static final int BASE_CONFLICT = 1;
/**
* @stable ICU 3.6
*/
public static final int CONFLICT = 2;
/**
* @stable ICU 3.6
*/
public int status;
/**
* @stable ICU 3.6
*/
public String conflictingPattern;
/**
* Simple constructor, since this is treated like a struct.
*
* @stable ICU 3.6
*/
public PatternInfo() {}
}
/**
* Adds a pattern to the generator. If the pattern has the same skeleton as an existing pattern,
* and the override parameter is set, then the previous value is overridden. Otherwise, the
* previous value is retained. In either case, the conflicting information is returned in
* PatternInfo.
*
* <p>Note that single-field patterns (like "MMM") are automatically added, and don't need to be
* added explicitly!
* <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:addPatternExample -->
*
* <p>Example code:
*
* <pre>
* Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
* ULocale locale = ULocale.FRANCE;
* // Create an DateTimePatternGenerator instance for the given locale
* DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale);
* SimpleDateFormat format = new SimpleDateFormat(gen.getBestPattern("MMMMddHmm"), locale);
* DateTimePatternGenerator.PatternInfo returnInfo = new DateTimePatternGenerator.PatternInfo();
* // Add '. von' to the existing pattern
* gen.addPattern("dd'. von' MMMM", true, returnInfo);
* // Apply the new pattern
* format.applyPattern(gen.getBestPattern("MMMMddHmm"));
* System.out.println("New Pattern for FRENCH: "+format.toPattern());
* System.out.println("Date Time in new Pattern: "+format.format(date));
*
* /** output of the sample code:
* **************************************************************************************************
* New Pattern for FRENCH: dd. 'von' MMMM HH:mm
* Date Time in new Pattern: 13. von octobre 23:58
* *************************************************************************************************<code>/</code>
* </pre>
*
* @param pattern Pattern to add.
* @param override When existing values are to be overridden use true, otherwise use false.
* @param returnInfo Returned information.
* @stable ICU 3.6
*/
public DateTimePatternGenerator addPattern(
String pattern, boolean override, PatternInfo returnInfo) {
return addPatternWithSkeleton(pattern, null, override, returnInfo);
}
/**
* addPatternWithSkeleton: If skeletonToUse is specified, then an availableFormats entry is
* being added. In this case: 1. We pass that skeleton to DateTimeMatcher().set instead of
* having it derive a skeleton from the pattern. 2. If the new entry's skeleton or basePattern
* does match an existing entry but that entry also had a skeleton specified (i.e. it was also
* from availableFormats), then the new entry does not override it regardless of the value of
* the override parameter. This prevents later availableFormats entries from a parent locale
* overriding earlier ones from the actual specified locale. However, availableFormats entries
* *should* override entries with matching skeleton whose skeleton was derived (i.e. entries
* derived from the standard date/time patters for the specified locale). 3. When adding the
* pattern (skeleton2pattern.put, basePattern_pattern.put), we set a field to indicate that the
* added entry had a specified skeleton.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public DateTimePatternGenerator addPatternWithSkeleton(
String pattern, String skeletonToUse, boolean override, PatternInfo returnInfo) {
checkFrozen();
DateTimeMatcher matcher;
if (skeletonToUse == null) {
matcher = new DateTimeMatcher().set(pattern, fp, false);
} else {
matcher = new DateTimeMatcher().set(skeletonToUse, fp, false);
}
String basePattern = matcher.getBasePattern();
// We only care about base conflicts - and replacing the pattern associated with a base -
// if:
// 1. the conflicting previous base pattern did *not* have an explicit skeleton; in that
// case the previous
// base + pattern combination was derived from either (a) a canonical item, (b) a standard
// format, or
// (c) a pattern specified programmatically with a previous call to addPattern (which would
// only happen
// if we are getting here from a subsequent call to addPattern).
// 2. a skeleton is specified for the current pattern, but override=false; in that case we
// are checking
// availableFormats items from root, which should not override any previous entry with the
// same base.
PatternWithSkeletonFlag previousPatternWithSameBase = basePattern_pattern.get(basePattern);
if (previousPatternWithSameBase != null
&& (!previousPatternWithSameBase.skeletonWasSpecified
|| (skeletonToUse != null && !override))) {
returnInfo.status = PatternInfo.BASE_CONFLICT;
returnInfo.conflictingPattern = previousPatternWithSameBase.pattern;
if (!override) {
return this;
}
}
// The only time we get here with override=true and skeletonToUse!=null is when adding
// availableFormats
// items from CLDR data. In that case, we don't want an item from a parent locale to replace
// an item with
// same skeleton from the specified locale, so skip the current item if skeletonWasSpecified
// is true for
// the previously-specified conflicting item.
PatternWithSkeletonFlag previousValue = skeleton2pattern.get(matcher);
if (previousValue != null) {
returnInfo.status = PatternInfo.CONFLICT;
returnInfo.conflictingPattern = previousValue.pattern;
if (!override || (skeletonToUse != null && previousValue.skeletonWasSpecified))
return this;
}
returnInfo.status = PatternInfo.OK;
returnInfo.conflictingPattern = "";
PatternWithSkeletonFlag patWithSkelFlag =
new PatternWithSkeletonFlag(pattern, skeletonToUse != null);
if (DEBUG) {
System.out.println(matcher + " => " + patWithSkelFlag);
}
skeleton2pattern.put(matcher, patWithSkelFlag);
basePattern_pattern.put(basePattern, patWithSkelFlag);
return this;
}
/**
* Utility to return a unique skeleton from a given pattern. For example, both "MMM-dd" and
* "dd/MMM" produce the skeleton "MMMdd".
*
* @param pattern Input pattern, such as "dd/MMM"
* @return skeleton, such as "MMMdd"
* @stable ICU 3.6
*/
public String getSkeleton(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp, false);
return current.toString();
}
}
/**
* Same as getSkeleton, but allows duplicates
*
* @param pattern Input pattern, such as "dd/MMM"
* @return skeleton, such as "MMMdd"
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String getSkeletonAllowingDuplicates(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp, true);
return current.toString();
}
}
/**
* Same as getSkeleton, but allows duplicates and returns a string using canonical pattern chars
*
* @param pattern Input pattern, such as "ccc, d LLL"
* @return skeleton, such as "MMMEd"
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String getCanonicalSkeletonAllowingDuplicates(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp, true);
return current.toCanonicalString();
}
}
/**
* Utility to return a unique base skeleton from a given pattern. This is the same as the
* skeleton, except that differences in length are minimized so as to only preserve the
* difference between string and numeric form. So for example, both "MMM-dd" and "d/MMM" produce
* the skeleton "MMMd" (notice the single d).
*
* @param pattern Input pattern, such as "dd/MMM"
* @return skeleton, such as "MMMdd"
* @stable ICU 3.6
*/
public String getBaseSkeleton(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp, false);
return current.getBasePattern();
}
}
/**
* Return a list of all the skeletons (in canonical form) from this class, and the patterns that
* they map to.
*
* @param result an output Map in which to place the mapping from skeleton to pattern. If you
* want to see the internal order being used, supply a LinkedHashMap. If the input value is
* null, then a LinkedHashMap is allocated.
* <p><i>Issue: an alternate API would be to just return a list of the skeletons, and then
* have a separate routine to get from skeleton to pattern.</i>
* @return the input Map containing the values.
* @stable ICU 3.6
*/
public Map<String, String> getSkeletons(Map<String, String> result) {
if (result == null) {
result = new LinkedHashMap<>();
}
for (DateTimeMatcher item : skeleton2pattern.keySet()) {
PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(item);
String pattern = patternWithSkelFlag.pattern;
if (CANONICAL_SET.contains(pattern)) {
continue;
}
result.put(item.toString(), pattern);
}
return result;
}
/**
* Return a list of all the base skeletons (in canonical form) from this class
*
* @stable ICU 3.6
*/
public Set<String> getBaseSkeletons(Set<String> result) {
if (result == null) {
result = new HashSet<>();
}
result.addAll(basePattern_pattern.keySet());
return result;
}
/**
* Adjusts the field types (width and subtype) of a pattern to match what is in a skeleton. That
* is, if you supply a pattern like "d-M H:m", and a skeleton of "MMMMddhhmm", then the input
* pattern is adjusted to be "dd-MMMM hh:mm". This is used internally to get the best match for
* the input skeleton, but can also be used externally.
* <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:replaceFieldTypesExample -->
*
* <p>Example code:
*
* <pre>
* Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
* TimeZone zone = TimeZone.getTimeZone("Europe/Paris");
* ULocale locale = ULocale.FRANCE;
* DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale);
* SimpleDateFormat format = new SimpleDateFormat("EEEE d MMMM y HH:mm:ss zzzz",locale);
* format.setTimeZone(zone);
* String pattern = format.toPattern();
* System.out.println("Pattern before replacement:");
* System.out.println(pattern);
* System.out.println("Date/Time format in fr_FR:");
* System.out.println(format.format(date));
* // Replace zone "zzzz" in the pattern with "vvvv"
* String newPattern = gen.replaceFieldTypes(pattern, "vvvv");
* // Apply the new pattern
* format.applyPattern(newPattern);
* System.out.println("Pattern after replacement:");
* System.out.println(newPattern);
* System.out.println("Date/Time format in fr_FR:");
* System.out.println(format.format(date));
*
* /** output of the sample code:
* ***************************************************************************************************
* Pattern before replacement:
* EEEE d MMMM y HH:mm:ss zzzz
* Date/Time format in fr_FR:
* jeudi 14 octobre 1999 05:58:59 heure avancée d’Europe centrale
* Pattern after replacement:
* EEEE d MMMM y HH:mm:ss vvvv
* Date/Time format in fr_FR:
* jeudi 14 octobre 1999 05:58:59 heure de l’Europe centrale
*
* **************************************************************************************************<code>/</code>
* </pre>
*
* @param pattern input pattern
* @param skeleton For the pattern to match to.
* @return pattern adjusted to match the skeleton fields widths and subtypes.
* @stable ICU 3.6
*/
public String replaceFieldTypes(String pattern, String skeleton) {
return replaceFieldTypes(pattern, skeleton, MATCH_NO_OPTIONS);
}
/**
* Adjusts the field types (width and subtype) of a pattern to match what is in a skeleton. That
* is, if you supply a pattern like "d-M H:m", and a skeleton of "MMMMddhhmm", then the input
* pattern is adjusted to be "dd-MMMM hh:mm". This is used internally to get the best match for
* the input skeleton, but can also be used externally.
*
* @param pattern input pattern
* @param skeleton For the pattern to match to.
* @param options MATCH_xxx options for forcing the length of specified fields in the returned
* pattern to match those in the skeleton (when this would not happen otherwise). For
* default behavior, use MATCH_NO_OPTIONS.
* @return pattern adjusted to match the skeleton fields widths and subtypes.
* @stable ICU 4.4
*/
public String replaceFieldTypes(String pattern, String skeleton, int options) {
synchronized (this) { // synchronized since a getter must be thread-safe
PatternWithMatcher patternNoMatcher = new PatternWithMatcher(pattern, null);
return adjustFieldTypes(
patternNoMatcher,
current.set(skeleton, fp, false),
EnumSet.noneOf(DTPGflags.class),
options);
}
}
/**
* The date time format is a message format pattern used to compose date and time patterns. The
* default value is "{1} {0}", where {1} will be replaced by the date pattern and {0} will be
* replaced by the time pattern.
*
* <p>This is used when the input skeleton contains both date and time fields, but there is not
* a close match among the added patterns. For example, suppose that this object was created by
* adding "dd-MMM" and "hh:mm", and its datetimeFormat is the default "{1} {0}". Then if the
* input skeleton is "MMMdhmm", there is not an exact match, so the input skeleton is broken up
* into two components "MMMd" and "hmm". There are close matches for those two skeletons, so the
* result is put together with this pattern, resulting in "d-MMM h:mm".
*
* <p>There are four DateTimeFormats in a DateTimePatternGenerator object, corresponding to date
* styles DateFormat.FULL..DateFormat.SHORT. This method sets all of them to the specified
* pattern. To set them individually, see setDateTimeFormat(int style, ...).
*
* @param dateTimeFormat message format pattern, where {1} will be replaced by the date pattern
* and {0} will be replaced by the time pattern.
* @stable ICU 3.6
*/
public void setDateTimeFormat(String dateTimeFormat) {
checkFrozen();
for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
setDateTimeFormat(style, dateTimeFormat);
}
}
/**
* Getter corresponding to setDateTimeFormat.
*
* <p>There are four DateTimeFormats in a DateTimePatternGenerator object, corresponding to date
* styles DateFormat.FULL..DateFormat.SHORT. This method gets the style for DateFormat.MEDIUM
* (the default). To get them individually, see getDateTimeFormat(int style).
*
* @return pattern
* @stable ICU 3.6
*/
public String getDateTimeFormat() {
return getDateTimeFormat(DateFormat.MEDIUM);
}
/**
* dateTimeFormats are message patterns used to compose combinations of date and time patterns.
* There are four length styles, corresponding to the inferred style of the date pattern: -
* DateFormat.FULL (for date pattern with weekday and long month), else - DateFormat.LONG (for a
* date pattern with long month), else - DateFormat.MEDIUM (for a date pattern with abbreviated
* month), else - DateFormat.SHORT (for any other date pattern). For details on dateTimeFormats,
* see https://www.unicode.org/reports/tr35/tr35-dates.html#dateTimeFormats. The default pattern
* in the root locale for all styles is "{1} {0}".
*
* @param style one of DateFormat.FULL..DateFormat.SHORT. An exception will be thrown if out of
* range.
* @param dateTimeFormat the new dateTimeFormat to set for the specified style
* @stable ICU 71
*/
public void setDateTimeFormat(int style, String dateTimeFormat) {
if (style < DateFormat.FULL || style > DateFormat.SHORT) {
throw new IllegalArgumentException("Illegal style here: " + style);
}
checkFrozen();
this.dateTimeFormats[style] = dateTimeFormat;
}
/**
* Getter corresponding to setDateTimeFormat.
*
* @param style one of DateFormat.FULL..DateFormat.SHORT. An exception will be thrown if out of
* range.
* @return the current dateTimeFormat for the specified style.
* @stable ICU 71
*/
public String getDateTimeFormat(int style) {
if (style < DateFormat.FULL || style > DateFormat.SHORT) {
throw new IllegalArgumentException("Illegal style here: " + style);
}
return dateTimeFormats[style];
}
/**
* The decimal value is used in formatting fractions of seconds. If the skeleton contains
* fractional seconds, then this is used with the fractional seconds. For example, suppose that
* the input pattern is "hhmmssSSSS", and the best matching pattern internally is "H:mm:ss", and
* the decimal string is ",". Then the resulting pattern is modified to be "H:mm:ss,SSSS"
*
* @param decimal The decimal to set to.
* @stable ICU 3.6
*/
public void setDecimal(String decimal) {
checkFrozen();
this.decimal = decimal;
}
/**
* Getter corresponding to setDecimal.
*
* @return string corresponding to the decimal point
* @stable ICU 3.6
*/
public String getDecimal() {
return decimal;
}
/**
* Redundant patterns are those which if removed, make no difference in the resulting
* getBestPattern values. This method returns a list of them, to help check the consistency of
* the patterns used to build this generator.
*
* @param output stores the redundant patterns that are removed. To get these in internal order,
* supply a LinkedHashSet. If null, a collection is allocated.
* @return the collection with added elements.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public Collection<String> getRedundants(Collection<String> output) {
synchronized (this) { // synchronized since a getter must be thread-safe
if (output == null) {
output = new LinkedHashSet<>();
}
for (DateTimeMatcher cur : skeleton2pattern.keySet()) {
PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(cur);
String pattern = patternWithSkelFlag.pattern;
if (CANONICAL_SET.contains(pattern)) {
continue;
}
String trial = getBestPattern(cur.toString(), cur, MATCH_NO_OPTIONS);
if (trial.equals(pattern)) {
output.add(pattern);
}
}
/// CLOVER:OFF
// The following would never be called since the parameter is false
// Eclipse stated the following is "dead code"
/*if (false) { // ordered
DateTimePatternGenerator results = new DateTimePatternGenerator();
PatternInfo pinfo = new PatternInfo();
for (DateTimeMatcher cur : skeleton2pattern.keySet()) {
String pattern = skeleton2pattern.get(cur);
if (CANONICAL_SET.contains(pattern)) {
continue;
}
//skipMatcher = current;
String trial = results.getBestPattern(cur.toString());
if (trial.equals(pattern)) {
output.add(pattern);
} else {
results.addPattern(pattern, false, pinfo);
}
}
}*/
/// CLOVER:ON
return output;
}
}
// Field numbers, used for AppendItem functions
/**
* @stable ICU 3.6
*/
public static final int ERA = 0;
/**
* @stable ICU 3.6
*/
public static final int YEAR = 1;
/**
* @stable ICU 3.6
*/
public static final int QUARTER = 2;
/**
* @stable ICU 3.6
*/
public static final int MONTH = 3;
/**
* @stable ICU 3.6
*/
public static final int WEEK_OF_YEAR = 4;
/**
* @stable ICU 3.6
*/
public static final int WEEK_OF_MONTH = 5;
/**
* @stable ICU 3.6
*/
public static final int WEEKDAY = 6;
/**
* @stable ICU 3.6
*/
public static final int DAY = 7;
/**
* @stable ICU 3.6
*/
public static final int DAY_OF_YEAR = 8;
/**
* @stable ICU 3.6
*/
public static final int DAY_OF_WEEK_IN_MONTH = 9;
/**
* @stable ICU 3.6
*/
public static final int DAYPERIOD = 10;
/**
* @stable ICU 3.6
*/
public static final int HOUR = 11;
/**
* @stable ICU 3.6
*/
public static final int MINUTE = 12;
/**
* @stable ICU 3.6
*/
public static final int SECOND = 13;
/**
* @stable ICU 3.6
*/
public static final int FRACTIONAL_SECOND = 14;
/**
* @stable ICU 3.6
*/
public static final int ZONE = 15;
/**
* One more than the highest normal field number.
*
* @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
*/
@Deprecated public static final int TYPE_LIMIT = 16;
/**
* Field display name width constants for getFieldDisplayName
*
* @stable ICU 61
*/
public enum DisplayWidth {
/**
* The full field name
*
* @stable ICU 61
*/
WIDE(""),
/**
* An abbreviated field name (may be the same as the wide version, if short enough)
*
* @stable ICU 61
*/
ABBREVIATED("-short"),
/**
* The shortest possible field name (may be the same as the abbreviated version)
*
* @stable ICU 61
*/
NARROW("-narrow");
/**
* The count of available widths
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated private static int COUNT = DisplayWidth.values().length;
private final String cldrKey;
DisplayWidth(String cldrKey) {
this.cldrKey = cldrKey;
}
private String cldrKey() {
return cldrKey;
}
}
/** The field name width for use in appendItems */
private static final DisplayWidth APPENDITEM_WIDTH = DisplayWidth.WIDE;
private static final int APPENDITEM_WIDTH_INT = APPENDITEM_WIDTH.ordinal();
private static final DisplayWidth[] CLDR_FIELD_WIDTH = DisplayWidth.values();
// Option masks for getBestPattern, replaceFieldTypes (individual masks may be ORed together)
/**
* Default option mask used for {@link #getBestPattern(String, int)} and {@link
* #replaceFieldTypes(String, String, int)}.
*
* @stable ICU 4.4
* @see #getBestPattern(String, int)
* @see #replaceFieldTypes(String, String, int)
*/
public static final int MATCH_NO_OPTIONS = 0;
/**
* Option mask for forcing the width of hour field.
*
* @stable ICU 4.4
* @see #getBestPattern(String, int)
* @see #replaceFieldTypes(String, String, int)
*/
public static final int MATCH_HOUR_FIELD_LENGTH = 1 << HOUR;
/**
* Option mask for forcing the width of minute field.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated public static final int MATCH_MINUTE_FIELD_LENGTH = 1 << MINUTE;
/**
* Option mask for forcing the width of second field.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated public static final int MATCH_SECOND_FIELD_LENGTH = 1 << SECOND;
/**
* Option mask for forcing the width of all date and time fields.
*
* @stable ICU 4.4
* @see #getBestPattern(String, int)
* @see #replaceFieldTypes(String, String, int)
*/
public static final int MATCH_ALL_FIELDS_LENGTH = (1 << TYPE_LIMIT) - 1;
/**
* An AppendItem format is a pattern used to append a field if there is no good match. For
* example, suppose that the input skeleton is "GyyyyMMMd", and there is no matching pattern
* internally, but there is a pattern matching "yyyyMMMd", say "d-MM-yyyy". Then that pattern is
* used, plus the G. The way these two are conjoined is by using the AppendItemFormat for G
* (era). So if that value is, say "{0}, {1}" then the final resulting pattern is "d-MM-yyyy,
* G".
*
* <p>There are actually three available variables: {0} is the pattern so far, {1} is the
* element we are adding, and {2} is the name of the element.
*
* <p>This reflects the way that the CLDR data is organized.
*
* @param field such as ERA
* @param value pattern, such as "{0}, {1}"
* @stable ICU 3.6
*/
public void setAppendItemFormat(int field, String value) {
checkFrozen();
appendItemFormats[field] = value;
}
/**
* Getter corresponding to setAppendItemFormats. Values below 0 or at or above TYPE_LIMIT are
* illegal arguments.
*
* @param field The index to retrieve the append item formats.
* @return append pattern for field
* @stable ICU 3.6
*/
public String getAppendItemFormat(int field) {
return appendItemFormats[field];
}
/**
* Sets the names of fields, eg "era" in English for ERA. These are only used if the
* corresponding AppendItemFormat is used, and if it contains a {2} variable.
*
* <p>This reflects the way that the CLDR data is organized.
*
* @param field Index of the append item names.
* @param value The value to set the item to.
* @stable ICU 3.6
*/
public void setAppendItemName(int field, String value) {
setFieldDisplayName(field, APPENDITEM_WIDTH, value);
}
/**
* Getter corresponding to setAppendItemName. Values below 0 or at or above TYPE_LIMIT are
* illegal arguments. Note: The more general method for getting date/time field display names is
* getFieldDisplayName.
*
* @param field The index to get the append item name.
* @return name for field
* @see #getFieldDisplayName(int, DisplayWidth)
* @stable ICU 3.6
*/
public String getAppendItemName(int field) {
return getFieldDisplayName(field, APPENDITEM_WIDTH);
}
/**
* Return the default hour cycle.
*
* @stable ICU 67
*/
public DateFormat.HourCycle getDefaultHourCycle() {
switch (getDefaultHourFormatChar()) {
case 'h':
return DateFormat.HourCycle.HOUR_CYCLE_12;
case 'H':
return DateFormat.HourCycle.HOUR_CYCLE_23;
case 'k':
return DateFormat.HourCycle.HOUR_CYCLE_24;
case 'K':
return DateFormat.HourCycle.HOUR_CYCLE_11;
default:
throw new AssertionError("should be unreachable");
}
}
/**
* The private interface to set a display name for a particular date/time field, in one of
* several possible display widths.
*
* @param field The field type, such as ERA.
* @param width The desired DisplayWidth, such as DisplayWidth.ABBREVIATED.
* @param value The display name to set
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
private void setFieldDisplayName(int field, DisplayWidth width, String value) {
checkFrozen();
if (field < TYPE_LIMIT && field >= 0) {
fieldDisplayNames[field][width.ordinal()] = value;
}
}
/**
* The general interface to get a display name for a particular date/time field, in one of
* several possible display widths.
*
* @param field The field type, such as ERA.
* @param width The desired DisplayWidth, such as DisplayWidth.ABBREVIATED.
* @return The display name for the field
* @stable ICU 61
*/
public String getFieldDisplayName(int field, DisplayWidth width) {
if (field >= TYPE_LIMIT || field < 0) {
return "";
}
return fieldDisplayNames[field][width.ordinal()];
}
/**
* Determines whether a skeleton contains a single field
*
* @param skeleton The skeleton to determine if it contains a single field.
* @return true or not
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static boolean isSingleField(String skeleton) {
char first = skeleton.charAt(0);
for (int i = 1; i < skeleton.length(); ++i) {
if (skeleton.charAt(i) != first) return false;
}
return true;
}
/**
* Add key to HashSet cldrAvailableFormatKeys.
*
* @param key of the availableFormats in CLDR
* @stable ICU 3.6
*/
private void setAvailableFormat(String key) {
checkFrozen();
cldrAvailableFormatKeys.add(key);
}
/**
* This function checks the corresponding slot of CLDR_AVAIL_FORMAT_KEY[] has been added to
* DateTimePatternGenerator. The function is to avoid the duplicate availableFomats added to the
* pattern map from parent locales.
*
* @param key of the availableFormatMask in CLDR
* @return true if the corresponding slot of CLDR_AVAIL_FORMAT_KEY[] has been added to
* DateTimePatternGenerator.
* @stable ICU 3.6
*/
private boolean isAvailableFormatSet(String key) {
return cldrAvailableFormatKeys.contains(key);
}
/**
* {@inheritDoc}
*
* @stable ICU 3.6
*/
@Override
public boolean isFrozen() {
return frozen;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.4
*/
@Override
public DateTimePatternGenerator freeze() {
frozen = true;
return this;
}
/**
* {@inheritDoc}
*
* @stable ICU 4.4
*/
@Override
public DateTimePatternGenerator cloneAsThawed() {
DateTimePatternGenerator result = this.clone();
frozen = false;
return result;
}
/**
* Returns a copy of this <code>DateTimePatternGenerator</code> object.
*
* @return A copy of this <code>DateTimePatternGenerator</code> object.
* @stable ICU 3.6
*/
@Override
@SuppressWarnings("unchecked")
public DateTimePatternGenerator clone() {
try {
DateTimePatternGenerator result = (DateTimePatternGenerator) super.clone();
result.skeleton2pattern =
(TreeMap<DateTimeMatcher, PatternWithSkeletonFlag>) skeleton2pattern.clone();
result.basePattern_pattern =
(TreeMap<String, PatternWithSkeletonFlag>) basePattern_pattern.clone();
result.dateTimeFormats = dateTimeFormats.clone();
result.appendItemFormats = appendItemFormats.clone();
result.fieldDisplayNames = fieldDisplayNames.clone();
result.current = new DateTimeMatcher();
result.fp = new FormatParser();
result._distanceInfo = new DistanceInfo();
result.frozen = false;
return result;
} catch (CloneNotSupportedException e) {
/// CLOVER:OFF
throw new ICUCloneNotSupportedException("Internal Error", e);
/// CLOVER:ON
}
}
/**
* Utility class for FormatParser. Immutable class that is only used to mark the difference
* between a variable field and a literal string. Each variable field must consist of 1 to n
* variable characters, representing date format fields. For example, "VVVV" is valid while "V4"
* is not, nor is "44".
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static class VariableField {
private final String string;
private final int canonicalIndex;
/**
* Create a variable field: equivalent to VariableField(string,false);
*
* @param string The string for the variable field.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public VariableField(String string) {
this(string, false);
}
/**
* Create a variable field
*
* @param string The string for the variable field
* @param strict If true, then only allows exactly those lengths specified by CLDR for
* variables. For example, "hh:mm aa" would throw an exception.
* @throws IllegalArgumentException if the variable field is not valid.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public VariableField(String string, boolean strict) {
canonicalIndex = DateTimePatternGenerator.getCanonicalIndex(string, strict);
if (canonicalIndex < 0) {
throw new IllegalArgumentException("Illegal datetime field:\t" + string);
}
this.string = string;
}
/**
* Get the main type of this variable. These types are ERA, QUARTER, MONTH, DAY,
* WEEK_OF_YEAR, WEEK_OF_MONTH, WEEKDAY, DAY, DAYPERIOD (am/pm), HOUR, MINUTE,
* SECOND,FRACTIONAL_SECOND, ZONE.
*
* @return main type.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public int getType() {
return types[canonicalIndex][1];
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static String getCanonicalCode(int type) {
try {
return CANONICAL_ITEMS[type];
} catch (Exception e) {
return String.valueOf(type);
}
}
/**
* Check if the type of this variable field is numeric.
*
* @return true if the type of this variable field is numeric.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public boolean isNumeric() {
return types[canonicalIndex][2] > 0;
}
/** Private method. */
private int getCanonicalIndex() {
return canonicalIndex;
}
/**
* Get the string represented by this variable.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Override
@Deprecated
public String toString() {
return string;
}
}
/**
* This class provides mechanisms for parsing a SimpleDateFormat pattern or generating a new
* pattern, while handling the quoting. It represents the result of the parse as a list of
* items, where each item is either a literal string or a variable field. When parsing It can be
* used to find out which variable fields are in a date format, and in what order, such as for
* presentation in a UI as separate text entry fields. It can also be used to construct new
* SimpleDateFormats.
*
* <p>Example:
*
* <pre>
* public boolean containsZone(String pattern) {
* for (Iterator it = formatParser.set(pattern).getItems().iterator(); it.hasNext();) {
* Object item = it.next();
* if (item instanceof VariableField) {
* VariableField variableField = (VariableField) item;
* if (variableField.getType() == DateTimePatternGenerator.ZONE) {
* return true;
* }
* }
* }
* return false;
* }
* </pre>
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static class FormatParser {
private static final UnicodeSet SYNTAX_CHARS = new UnicodeSet("[a-zA-Z]").freeze();
private static final UnicodeSet QUOTING_CHARS =
new UnicodeSet("[[[:script=Latn:][:script=Cyrl:]]&[[:L:][:M:]]]").freeze();
private transient PatternTokenizer tokenizer =
new PatternTokenizer()
.setSyntaxCharacters(SYNTAX_CHARS)
.setExtraQuotingCharacters(QUOTING_CHARS)
.setUsingQuote(true);
private List<Object> items = new ArrayList<>();
/**
* Construct an empty date format parser, to which strings and variables can be added with
* set(...).
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public FormatParser() {}
/**
* Parses the string into a list of items.
*
* @param string The string to parse.
* @return this, for chaining
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public final FormatParser set(String string) {
return set(string, false);
}
/**
* Parses the string into a list of items, taking into account all of the quoting that may
* be going on.
*
* @param string The string to parse.
* @param strict If true, then only allows exactly those lengths specified by CLDR for
* variables. For example, "hh:mm aa" would throw an exception.
* @return this, for chaining
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public FormatParser set(String string, boolean strict) {
items.clear();
if (string.length() == 0) return this;
tokenizer.setPattern(string);
StringBuffer buffer = new StringBuffer();
StringBuffer variable = new StringBuffer();
while (true) {
buffer.setLength(0);
int status = tokenizer.next(buffer);
if (status == PatternTokenizer.DONE) break;
if (status == PatternTokenizer.SYNTAX) {
if (variable.length() != 0 && buffer.charAt(0) != variable.charAt(0)) {
addVariable(variable, false);
}
variable.append(buffer);
} else {
addVariable(variable, false);
items.add(buffer.toString());
}
}
addVariable(variable, false);
return this;
}
private void addVariable(StringBuffer variable, boolean strict) {
if (variable.length() != 0) {
items.add(new VariableField(variable.toString(), strict));
variable.setLength(0);
}
}
// /** Private method. Return a collection of fields. These will be a mixture of
// literal Strings and VariableFields. Any "a" variable field is removed.
// * @param output List to append the items to. If null, is allocated as an
// ArrayList.
// * @return list
// */
// private List getVariableFields(List output) {
// if (output == null) output = new ArrayList();
// main:
// for (Iterator it = items.iterator(); it.hasNext();) {
// Object item = it.next();
// if (item instanceof VariableField) {
// String s = item.toString();
// switch(s.charAt(0)) {
// //case 'Q': continue main; // HACK
// case 'a': continue main; // remove
// }
// output.add(item);
// }
// }
// //System.out.println(output);
// return output;
// }
// /**
// * Produce a string which concatenates all the variables. That is, it is the
// logically the same as the input with all literals removed.
// * @return a string which is a concatenation of all the variable fields
// */
// public String getVariableFieldString() {
// List list = getVariableFields(null);
// StringBuffer result = new StringBuffer();
// for (Iterator it = list.iterator(); it.hasNext();) {
// String item = it.next().toString();
// result.append(item);
// }
// return result.toString();
// }
/**
* Returns modifiable list which is a mixture of Strings and VariableFields, in the order
* found during parsing. The strings represent literals, and have all quoting removed. Thus
* the string "dd 'de' MM" will parse into three items:
*
* <pre>
* VariableField: dd
* String: " de "
* VariableField: MM
* </pre>
*
* The list is modifiable, so you can add any strings or variables to it, or remove any
* items.
*
* @return modifiable list of items.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public List<Object> getItems() {
return items;
}
/**
* Provide display form of formatted input. Each literal string is quoted if necessary..
* That is, if the input was "hh':'mm", the result would be "hh:mm", since the ":" doesn't
* need quoting. See quoteLiteral().
*
* @return printable output string
* @internal
* @deprecated This API is ICU internal only.
*/
@Override
@Deprecated
public String toString() {
return toString(0, items.size());
}
/**
* Provide display form of a segment of the parsed input. Each literal string is minimally
* quoted. That is, if the input was "hh':'mm", the result would be "hh:mm", since the ":"
* doesn't need quoting. See quoteLiteral().
*
* @param start item to start from
* @param limit last item +1
* @return printable output string
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String toString(int start, int limit) {
StringBuilder result = new StringBuilder();
for (int i = start; i < limit; ++i) {
Object item = items.get(i);
if (item instanceof String) {
String itemString = (String) item;
result.append(tokenizer.quoteLiteral(itemString));
} else {
result.append(items.get(i).toString());
}
}
return result.toString();
}
/**
* Returns true if it has a mixture of date and time variable fields: that is, at least one
* date variable and at least one time variable.
*
* @return true or false
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public boolean hasDateAndTimeFields() {
int foundMask = 0;
for (Object item : items) {
if (item instanceof VariableField) {
int type = ((VariableField) item).getType();
foundMask |= 1 << type;
}
}
boolean isDate = (foundMask & DATE_MASK) != 0;
boolean isTime = (foundMask & TIME_MASK) != 0;
return isDate && isTime;
}
// /**
// * Internal routine
// * @param value
// * @param result
// * @return list
// */
// public List getAutoPatterns(String value, List result) {
// if (result == null) result = new ArrayList();
// int fieldCount = 0;
// int minField = Integer.MAX_VALUE;
// int maxField = Integer.MIN_VALUE;
// for (Iterator it = items.iterator(); it.hasNext();) {
// Object item = it.next();
// if (item instanceof VariableField) {
// try {
// int type = ((VariableField)item).getType();
// if (minField > type) minField = type;
// if (maxField < type) maxField = type;
// if (type == ZONE || type == DAYPERIOD || type == WEEKDAY) return
// result; // skip anything with zones
// fieldCount++;
// } catch (Exception e) {
// return result; // if there are any funny fields, return
// }
// }
// }
// if (fieldCount < 3) return result; // skip
// // trim from start
// // trim first field IF there are no letters around it
// // and it is either the min or the max field
// // first field is either 0 or 1
// for (int i = 0; i < items.size(); ++i) {
// Object item = items.get(i);
// if (item instanceof VariableField) {
// int type = ((VariableField)item).getType();
// if (type != minField && type != maxField) break;
//
// if (i > 0) {
// Object previousItem = items.get(0);
// if (alpha.containsSome(previousItem.toString())) break;
// }
// int start = i+1;
// if (start < items.size()) {
// Object nextItem = items.get(start);
// if (nextItem instanceof String) {
// if (alpha.containsSome(nextItem.toString())) break;
// start++; // otherwise skip over string
// }
// }
// result.add(toString(start, items.size()));
// break;
// }
// }
// // now trim from end
// for (int i = items.size()-1; i >= 0; --i) {
// Object item = items.get(i);
// if (item instanceof VariableField) {
// int type = ((VariableField)item).getType();
// if (type != minField && type != maxField) break;
// if (i < items.size() - 1) {
// Object previousItem = items.get(items.size() - 1);
// if (alpha.containsSome(previousItem.toString())) break;
// }
// int end = i-1;
// if (end > 0) {
// Object nextItem = items.get(end);
// if (nextItem instanceof String) {
// if (alpha.containsSome(nextItem.toString())) break;
// end--; // otherwise skip over string
// }
// }
// result.add(toString(0, end+1));
// break;
// }
// }
//
// return result;
// }
// private static UnicodeSet alpha = new UnicodeSet("[:alphabetic:]");
// private int getType(Object item) {
// String s = item.toString();
// int canonicalIndex = getCanonicalIndex(s);
// if (canonicalIndex < 0) {
// throw new IllegalArgumentException("Illegal field:\t"
// + s);
// }
// int type = types[canonicalIndex][1];
// return type;
// }
/**
* Each literal string is quoted as needed. That is, the ' quote marks will only be added if
* needed. The exact pattern of quoting is not guaranteed, thus " de la " could be quoted as
* " 'de la' " or as " 'de' 'la' ".
*
* @param string The string to check.
* @return string with quoted literals
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public Object quoteLiteral(String string) {
return tokenizer.quoteLiteral(string);
}
}
/**
* Used by CLDR tooling; not in ICU4C. Note, this will not work correctly with normal skeletons,
* since fields that should be related in the two skeletons being compared - like EEE and ccc,
* or y and U - will not be sorted in the same relative place as each other when iterating over
* both TreeSets being compare, using TreeSet's "natural" code point ordering (this could be
* addressed by initializing the TreeSet with a comparator that compares fields first by their
* index from getCanonicalIndex()). However if comparing canonical skeletons from
* getCanonicalSkeletonAllowingDuplicates it will be OK regardless, since in these skeletons all
* fields are normalized to the canonical pattern char for those fields - M or L to M, E or c to
* E, y or U to y, etc. - so corresponding fields will sort in the same way for both TreeMaps.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public boolean skeletonsAreSimilar(String id, String skeleton) {
if (id.equals(skeleton)) {
return true; // fast path
}
// must clone array, make sure items are in same order.
TreeSet<String> parser1 = getSet(id);
TreeSet<String> parser2 = getSet(skeleton);
if (parser1.size() != parser2.size()) {
return false;
}
Iterator<String> it2 = parser2.iterator();
for (String item : parser1) {
int index1 = getCanonicalIndex(item, false);
String item2 = it2.next(); // same length so safe
int index2 = getCanonicalIndex(item2, false);
if (types[index1][1] != types[index2][1]) {
return false;
}
}
return true;
}
private TreeSet<String> getSet(String id) {
final List<Object> items = fp.set(id).getItems();
TreeSet<String> result = new TreeSet<>();
for (Object obj : items) {
final String item = obj.toString();
if (item.startsWith("G") || item.startsWith("a")) {
continue;
}
result.add(item);
}
return result;
}
// ========= PRIVATES ============
private static class PatternWithMatcher {
public String pattern;
public DateTimeMatcher matcherWithSkeleton;
// Simple constructor
public PatternWithMatcher(String pat, DateTimeMatcher matcher) {
pattern = pat;
matcherWithSkeleton = matcher;
}
}
private static class PatternWithSkeletonFlag {
public String pattern;
public boolean skeletonWasSpecified;
// Simple constructor
public PatternWithSkeletonFlag(String pat, boolean skelSpecified) {
pattern = pat;
skeletonWasSpecified = skelSpecified;
}
@Override
public String toString() {
return pattern + "," + skeletonWasSpecified;
}
}
private TreeMap<DateTimeMatcher, PatternWithSkeletonFlag> skeleton2pattern =
new TreeMap<>(); // items are in priority order
private TreeMap<String, PatternWithSkeletonFlag> basePattern_pattern =
new TreeMap<>(); // items are in priority order
private String decimal = "?";
// For the following, need fallback patterns in case an empty instance
// of DateTimePatterngenerator is used for formatting.
private String[] dateTimeFormats = {"{1} {0}", "{1} {0}", "{1} {0}", "{1} {0}"};
private String[] appendItemFormats = new String[TYPE_LIMIT];
private String[][] fieldDisplayNames = new String[TYPE_LIMIT][DisplayWidth.COUNT];
private char defaultHourFormatChar = 'H';
// private boolean chineseMonthHack = false;
// private boolean isComplete = false;
private volatile boolean frozen = false;
private transient DateTimeMatcher current = new DateTimeMatcher();
private transient FormatParser fp = new FormatParser();
private transient DistanceInfo _distanceInfo = new DistanceInfo();
private String[] allowedHourFormats;
private static final int FRACTIONAL_MASK = 1 << FRACTIONAL_SECOND;
private static final int SECOND_AND_FRACTIONAL_MASK = (1 << SECOND) | (1 << FRACTIONAL_SECOND);
// Cache for DateTimePatternGenerator
private static ICUCache<String, DateTimePatternGenerator> DTPNG_CACHE = new SimpleCache<>();
private void checkFrozen() {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify frozen object");
}
}
/**
* We only get called here if we failed to find an exact skeleton. We have broken it into date +
* time, and look for the pieces. If we fail to find a complete skeleton, we compose in a loop
* until we have all the fields.
*/
private String getBestAppending(
DateTimeMatcher source,
int missingFields,
DistanceInfo distInfo,
DateTimeMatcher skipMatcher,
EnumSet<DTPGflags> flags,
int options) {
String resultPattern = null;
if (missingFields != 0) {
PatternWithMatcher resultPatternWithMatcher =
getBestRaw(source, missingFields, distInfo, skipMatcher);
resultPattern = adjustFieldTypes(resultPatternWithMatcher, source, flags, options);
while (distInfo.missingFieldMask != 0) { // precondition: EVERY single field must work!
// special hack for SSS. If we are missing SSS, and we had ss but found it, replace
// the s field according to the
// number separator
if ((distInfo.missingFieldMask & SECOND_AND_FRACTIONAL_MASK) == FRACTIONAL_MASK
&& (missingFields & SECOND_AND_FRACTIONAL_MASK)
== SECOND_AND_FRACTIONAL_MASK) {
resultPatternWithMatcher.pattern = resultPattern;
flags = EnumSet.copyOf(flags);
flags.add(DTPGflags.FIX_FRACTIONAL_SECONDS);
resultPattern =
adjustFieldTypes(resultPatternWithMatcher, source, flags, options);
distInfo.missingFieldMask &= ~FRACTIONAL_MASK; // remove bit
continue;
}
int startingMask = distInfo.missingFieldMask;
PatternWithMatcher tempWithMatcher =
getBestRaw(source, distInfo.missingFieldMask, distInfo, skipMatcher);
String temp = adjustFieldTypes(tempWithMatcher, source, flags, options);
int foundMask = startingMask & ~distInfo.missingFieldMask;
int topField = getTopBitNumber(foundMask);
resultPattern =
SimpleFormatterImpl.formatRawPattern(
getAppendFormat(topField),
2,
3,
resultPattern,
temp,
getAppendName(topField));
}
}
return resultPattern;
}
private String getAppendName(int foundMask) {
return "'" + fieldDisplayNames[foundMask][APPENDITEM_WIDTH_INT] + "'";
}
private String getAppendFormat(int foundMask) {
return appendItemFormats[foundMask];
}
// /**
// * @param current2
// * @return
// */
// private String adjustSeconds(DateTimeMatcher current2) {
// // TODO Auto-generated method stub
// return null;
// }
/**
* @param foundMask
*/
private int getTopBitNumber(int foundMask) {
int i = 0;
while (foundMask != 0) {
foundMask >>>= 1;
++i;
}
return i - 1;
}
private void addCanonicalItems() {
PatternInfo patternInfo = new PatternInfo();
// make sure that every valid field occurs once, with a "default" length
for (int i = 0; i < CANONICAL_ITEMS.length; ++i) {
addPattern(String.valueOf(CANONICAL_ITEMS[i]), false, patternInfo);
}
}
private PatternWithMatcher getBestRaw(
DateTimeMatcher source,
int includeMask,
DistanceInfo missingFields,
DateTimeMatcher skipMatcher) {
// if (SHOW_DISTANCE) System.out.println("Searching for: " + source.pattern
// + ", mask: " + showMask(includeMask));
int bestDistance = Integer.MAX_VALUE;
int bestMissingFieldMask = Integer.MIN_VALUE;
PatternWithMatcher bestPatternWithMatcher = new PatternWithMatcher("", null);
DistanceInfo tempInfo = new DistanceInfo();
for (DateTimeMatcher trial : skeleton2pattern.keySet()) {
if (trial.equals(skipMatcher)) {
continue;
}
int distance = source.getDistance(trial, includeMask, tempInfo);
// if (SHOW_DISTANCE) System.out.println("\tDistance: " + trial.pattern + ":\t"
// + distance + ",\tmissing fields: " + tempInfo);
// Because we iterate over a map the order is undefined. Can change between
// implementations,
// versions, and will very likely be different between Java and C/C++.
// So if we have patterns with the same distance we also look at the missingFieldMask,
// and we favour the smallest one. Because the field is a bitmask this technically means
// we
// favour differences in the "least significant fields". For example we prefer the one
// with differences
// in seconds field vs one with difference in the hours field.
if (distance < bestDistance
|| (distance == bestDistance
&& bestMissingFieldMask < tempInfo.missingFieldMask)) {
bestDistance = distance;
bestMissingFieldMask = tempInfo.missingFieldMask;
PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(trial);
bestPatternWithMatcher.pattern = patternWithSkelFlag.pattern;
// If the best raw match had a specified skeleton then return it too.
// This can be passed through to adjustFieldTypes to help it do a better job.
if (patternWithSkelFlag.skeletonWasSpecified) {
bestPatternWithMatcher.matcherWithSkeleton = trial;
} else {
bestPatternWithMatcher.matcherWithSkeleton = null;
}
missingFields.setTo(tempInfo);
if (distance == 0) {
break;
}
}
}
return bestPatternWithMatcher;
}
/*
* @param fixFractionalSeconds TODO
*/
// flags values
private enum DTPGflags {
FIX_FRACTIONAL_SECONDS,
SKELETON_USES_CAP_J,
// with #13183, no longer need flags for b, B
;
};
private String adjustFieldTypes(
PatternWithMatcher patternWithMatcher,
DateTimeMatcher inputRequest,
EnumSet<DTPGflags> flags,
int options) {
fp.set(patternWithMatcher.pattern);
StringBuilder newPattern = new StringBuilder();
for (Object item : fp.getItems()) {
if (item instanceof String) {
newPattern.append(fp.quoteLiteral((String) item));
} else {
final VariableField variableField = (VariableField) item;
StringBuilder fieldBuilder = new StringBuilder(variableField.toString());
// int canonicalIndex = getCanonicalIndex(field, true);
// if (canonicalIndex < 0) {
// continue; // don't adjust
// }
// int type = types[canonicalIndex][1];
int type = variableField.getType();
// handle day periods - with #13183, no longer need special handling here,
// integrated with normal types
if (flags.contains(DTPGflags.FIX_FRACTIONAL_SECONDS) && type == SECOND) {
fieldBuilder.append(decimal);
inputRequest.original.appendFieldTo(FRACTIONAL_SECOND, fieldBuilder);
} else if (inputRequest.type[type] != 0) {
// Here:
// - "reqField" is the field from the originally requested skeleton, with length
// "reqFieldLen".
// - "field" is the field from the found pattern.
//
// The adjusted field should consist of characters from the originally requested
// skeleton, except in the case of MONTH or WEEKDAY or YEAR, in which case it
// should consist of characters from the found pattern. There is some adjustment
// in some cases of HOUR to "defaultHourFormatChar". There is explanation
// how it is done below.
//
// The length of the adjusted field (adjFieldLen) should match that in the
// originally
// requested skeleton, except that in the following cases the length of the
// adjusted field
// should match that in the found pattern (i.e. the length of this pattern field
// should
// not be adjusted):
// 1. type is HOUR and the corresponding bit in options is not set (ticket
// #7180).
// Note, we may want to implement a similar change for other numeric fields
// (MM, dd,
// etc.) so the default behavior is to get locale preference for field
// length, but
// options bits can be used to override this.
// 2. There is a specified skeleton for the found pattern and one of the
// following is true:
// a) The length of the field in the skeleton (skelFieldLen) is equal to
// reqFieldLen.
// b) The pattern field is numeric and the requested field is not, or vice
// versa.
//
// Old behavior was:
// normally we just replace the field. However HOUR is special; we only change
// the length
char reqFieldChar = inputRequest.original.getFieldChar(type);
int reqFieldLen = inputRequest.original.getFieldLength(type);
if (reqFieldChar == 'E' && reqFieldLen < 3) {
reqFieldLen = 3; // 1-3 for E are equivalent to 3 for c,e
}
int adjFieldLen = reqFieldLen;
DateTimeMatcher matcherWithSkeleton = patternWithMatcher.matcherWithSkeleton;
if ((type == HOUR && (options & MATCH_HOUR_FIELD_LENGTH) == 0)
|| (type == MINUTE && (options & MATCH_MINUTE_FIELD_LENGTH) == 0)
|| (type == SECOND && (options & MATCH_SECOND_FIELD_LENGTH) == 0)) {
adjFieldLen = fieldBuilder.length();
} else if (matcherWithSkeleton != null
&& reqFieldChar != 'c'
&& reqFieldChar != 'e') {
// (we skip this section for 'c' and 'e' because unlike the other characters
// considered in this function,
// they have no minimum field length-- 'E' and 'EE' are equivalent to 'EEE',
// but 'e' and 'ee' are not
// equivalent to 'eee' -- see the entries for "week day" in
// https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table for more info)
int skelFieldLen = matcherWithSkeleton.original.getFieldLength(type);
boolean patFieldIsNumeric = variableField.isNumeric();
boolean reqFieldIsNumeric = inputRequest.fieldIsNumeric(type);
if (skelFieldLen == reqFieldLen
|| (patFieldIsNumeric && !reqFieldIsNumeric)
|| (reqFieldIsNumeric && !patFieldIsNumeric)) {
// don't adjust the field length in the found pattern
adjFieldLen = fieldBuilder.length();
}
}
char c =
(type != HOUR
&& type != MONTH
&& type != WEEKDAY
&& (type != YEAR || reqFieldChar == 'Y'))
? reqFieldChar
: fieldBuilder.charAt(0);
if (c == 'E' && adjFieldLen < 3) {
// see
// https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table:
// If we want a numeric day-of-week field, we have to use 'e'-- 'E' doesn't
// support
// numeric day-of-week abbreivations
c = 'e';
}
if (type == HOUR) {
// The adjustment here is required to match spec
// (https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour).
// It is necessary to match the hour-cycle preferred by the Locale.
// Given that, we need to do the following adjustments:
// 1. When hour-cycle is h11 it should replace 'h' by 'K'.
// 2. When hour-cycle is h23 it should replace 'H' by 'k'.
// 3. When hour-cycle is h24 it should replace 'k' by 'H'.
// 4. When hour-cycle is h12 it should replace 'K' by 'h'.
if (flags.contains(DTPGflags.SKELETON_USES_CAP_J)
|| reqFieldChar == defaultHourFormatChar) {
c = defaultHourFormatChar;
} else if (reqFieldChar == 'h' && defaultHourFormatChar == 'K') {
c = 'K';
} else if (reqFieldChar == 'H' && defaultHourFormatChar == 'k') {
c = 'k';
} else if (reqFieldChar == 'k' && defaultHourFormatChar == 'H') {
c = 'H';
} else if (reqFieldChar == 'K' && defaultHourFormatChar == 'h') {
c = 'h';
}
}
fieldBuilder = new StringBuilder();
for (int i = adjFieldLen; i > 0; --i) fieldBuilder.append(c);
}
newPattern.append(fieldBuilder);
}
}
// if (SHOW_DISTANCE) System.out.println("\tRaw: " + pattern);
return newPattern.toString();
}
// public static String repeat(String s, int count) {
// StringBuffer result = new StringBuffer();
// for (int i = 0; i < count; ++i) {
// result.append(s);
// }
// return result.toString();
// }
/**
* internal routine
*
* @param pattern The pattern that is passed.
* @return field value
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String getFields(String pattern) {
fp.set(pattern);
StringBuilder newPattern = new StringBuilder();
for (Object item : fp.getItems()) {
if (item instanceof String) {
newPattern.append(fp.quoteLiteral((String) item));
} else {
newPattern.append("{" + getName(item.toString()) + "}");
}
}
return newPattern.toString();
}
private static String showMask(int mask) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < TYPE_LIMIT; ++i) {
if ((mask & (1 << i)) == 0) continue;
if (result.length() != 0) result.append(" | ");
result.append(FIELD_NAME[i]);
result.append(" ");
}
return result.toString();
}
private static final String[] CLDR_FIELD_APPEND = {
"Era",
"Year",
"Quarter",
"Month",
"Week",
"*",
"Day-Of-Week",
"Day",
"*",
"*",
"*",
"Hour",
"Minute",
"Second",
"*",
"Timezone"
};
private static final String[] CLDR_FIELD_NAME = {
"era",
"year",
"quarter",
"month",
"week",
"weekOfMonth",
"weekday",
"day",
"dayOfYear",
"weekdayOfMonth",
"dayperiod",
"hour",
"minute",
"second",
"*",
"zone"
};
private static final String[] FIELD_NAME = {
"Era",
"Year",
"Quarter",
"Month",
"Week_in_Year",
"Week_in_Month",
"Weekday",
"Day",
"Day_Of_Year",
"Day_of_Week_in_Month",
"Dayperiod",
"Hour",
"Minute",
"Second",
"Fractional_Second",
"Zone"
};
private static final String[] CANONICAL_ITEMS = {
"G", "y", "Q", "M", "w", "W", "E", "d", "D", "F", "a", "H", "m", "s", "S", "v"
};
// canon DateTimePatternGen CLDR fields
// char field bundle key
// ---- -------------------- ----------------
// 'G', // 0 ERA "era"
// 'y', // 1 YEAR "year"
// 'Q', // 2 QUARTER "quarter"
// 'M', // 3 MONTH "month"
// 'w', // 4 WEEK_OF_YEAR, "week"
// 'W', // 5 WEEK_OF_MONTH "weekOfMonth"
// 'E', // 6 WEEKDAY "weekday"
// 'd', // 7 DAY "day"
// 'D', // 8 DAY_OF_YEAR "dayOfYear"
// 'F', // 9 DAY_OF_WEEK_IN_MONTH "weekdayOfMonth"
// 'a', // 10 DAYPERIOD "dayperiod"
// 'H', // 11 HOUR "hour"
// 'm', // 12 MINUTE "minute"
// 's', // 13 SECOND "second"
// 'S', // 14 FRACTIONAL_SECOND
// 'v', // 15 ZONE "zone"
private static final Set<String> CANONICAL_SET = new HashSet<>(Arrays.asList(CANONICAL_ITEMS));
private Set<String> cldrAvailableFormatKeys = new HashSet<>(20);
private static final int DATE_MASK = (1 << DAYPERIOD) - 1,
TIME_MASK = (1 << TYPE_LIMIT) - 1 - DATE_MASK;
private static final int // numbers are chosen to express 'distance'
DELTA = 0x10,
NUMERIC = 0x100,
NONE = 0,
NARROW = -0x101,
SHORTER = -0x102,
SHORT = -0x103,
LONG = -0x104,
EXTRA_FIELD = 0x10000,
MISSING_FIELD = 0x1000;
private static String getName(String s) {
int i = getCanonicalIndex(s, true);
String name = FIELD_NAME[types[i][1]];
if (types[i][2] < 0) {
name += ":S"; // string
} else {
name += ":N";
}
return name;
}
/**
* Get the canonical index, or return -1 if illegal.
*
* @param s
* @param strict TODO
*/
private static int getCanonicalIndex(String s, boolean strict) {
int len = s.length();
if (len == 0) {
return -1;
}
int ch = s.charAt(0);
// verify that all are the same character
for (int i = 1; i < len; ++i) {
if (s.charAt(i) != ch) {
return -1;
}
}
int bestRow = -1;
for (int i = 0; i < types.length; ++i) {
int[] row = types[i];
if (row[0] != ch) continue;
bestRow = i;
if (row[3] > len) continue;
if (row[row.length - 1] < len) continue;
return i;
}
return strict ? -1 : bestRow;
}
/** Gets the canonical character associated with the specified field (ERA, YEAR, etc). */
private static char getCanonicalChar(int field, char reference) {
// Special case: distinguish between 12-hour and 24-hour
if (reference == 'h' || reference == 'K') {
return 'h';
}
// Linear search over types (return the top entry for each field)
for (int i = 0; i < types.length; ++i) {
int[] row = types[i];
if (row[1] == field) {
return (char) row[0];
}
}
throw new IllegalArgumentException("Could not find field " + field);
}
private static final int[][] types = {
// the order here makes a difference only when searching for single field.
// format is:
// pattern character, main type, weight, min length, weight
{'G', ERA, SHORT, 1, 3},
{'G', ERA, LONG, 4},
{'G', ERA, NARROW, 5},
{'y', YEAR, NUMERIC, 1, 20},
{'Y', YEAR, NUMERIC + DELTA, 1, 20},
{'u', YEAR, NUMERIC + 2 * DELTA, 1, 20},
{'r', YEAR, NUMERIC + 3 * DELTA, 1, 20},
{'U', YEAR, SHORT, 1, 3},
{'U', YEAR, LONG, 4},
{'U', YEAR, NARROW, 5},
{'Q', QUARTER, NUMERIC, 1, 2},
{'Q', QUARTER, SHORT, 3},
{'Q', QUARTER, LONG, 4},
{'Q', QUARTER, NARROW, 5},
{'q', QUARTER, NUMERIC + DELTA, 1, 2},
{'q', QUARTER, SHORT - DELTA, 3},
{'q', QUARTER, LONG - DELTA, 4},
{'q', QUARTER, NARROW - DELTA, 5},
{'M', MONTH, NUMERIC, 1, 2},
{'M', MONTH, SHORT, 3},
{'M', MONTH, LONG, 4},
{'M', MONTH, NARROW, 5},
{'L', MONTH, NUMERIC + DELTA, 1, 2},
{'L', MONTH, SHORT - DELTA, 3},
{'L', MONTH, LONG - DELTA, 4},
{'L', MONTH, NARROW - DELTA, 5},
{'l', MONTH, NUMERIC + DELTA, 1, 1},
{'w', WEEK_OF_YEAR, NUMERIC, 1, 2},
{'W', WEEK_OF_MONTH, NUMERIC, 1},
{'E', WEEKDAY, SHORT, 1, 3},
{'E', WEEKDAY, LONG, 4},
{'E', WEEKDAY, NARROW, 5},
{'E', WEEKDAY, SHORTER, 6},
{'c', WEEKDAY, NUMERIC + 2 * DELTA, 1, 2},
{'c', WEEKDAY, SHORT - 2 * DELTA, 3},
{'c', WEEKDAY, LONG - 2 * DELTA, 4},
{'c', WEEKDAY, NARROW - 2 * DELTA, 5},
{'c', WEEKDAY, SHORTER - 2 * DELTA, 6},
{
'e', WEEKDAY, NUMERIC + DELTA, 1, 2
}, // 'e' is currently not used in CLDR data, should not be canonical
{'e', WEEKDAY, SHORT - DELTA, 3},
{'e', WEEKDAY, LONG - DELTA, 4},
{'e', WEEKDAY, NARROW - DELTA, 5},
{'e', WEEKDAY, SHORTER - DELTA, 6},
{'d', DAY, NUMERIC, 1, 2},
{'g', DAY, NUMERIC + DELTA, 1, 20}, // really internal use, so we don't care
{'D', DAY_OF_YEAR, NUMERIC, 1, 3},
{'F', DAY_OF_WEEK_IN_MONTH, NUMERIC, 1},
{'a', DAYPERIOD, SHORT, 1, 3},
{'a', DAYPERIOD, LONG, 4},
{'a', DAYPERIOD, NARROW, 5},
{'b', DAYPERIOD, SHORT - DELTA, 1, 3},
{'b', DAYPERIOD, LONG - DELTA, 4},
{'b', DAYPERIOD, NARROW - DELTA, 5},
// b needs to be closer to a than to B, so we make this 3*DELTA
{'B', DAYPERIOD, SHORT - 3 * DELTA, 1, 3},
{'B', DAYPERIOD, LONG - 3 * DELTA, 4},
{'B', DAYPERIOD, NARROW - 3 * DELTA, 5},
{'H', HOUR, NUMERIC + 10 * DELTA, 1, 2}, // 24 hour
{'k', HOUR, NUMERIC + 11 * DELTA, 1, 2},
{'h', HOUR, NUMERIC, 1, 2}, // 12 hour
{'K', HOUR, NUMERIC + DELTA, 1, 2},
{'m', MINUTE, NUMERIC, 1, 2},
{'s', SECOND, NUMERIC, 1, 2},
{'A', SECOND, NUMERIC + DELTA, 1, 1000},
{'S', FRACTIONAL_SECOND, NUMERIC, 1, 1000},
{'v', ZONE, SHORT - 2 * DELTA, 1},
{'v', ZONE, LONG - 2 * DELTA, 4},
{'z', ZONE, SHORT, 1, 3},
{'z', ZONE, LONG, 4},
{'Z', ZONE, NARROW - DELTA, 1, 3},
{'Z', ZONE, LONG - DELTA, 4},
{'Z', ZONE, SHORT - DELTA, 5},
{'O', ZONE, SHORT - DELTA, 1},
{'O', ZONE, LONG - DELTA, 4},
{'V', ZONE, SHORT - DELTA, 1},
{'V', ZONE, LONG - DELTA, 2},
{'V', ZONE, LONG - 1 - DELTA, 3},
{'V', ZONE, LONG - 2 - DELTA, 4},
{'X', ZONE, NARROW - DELTA, 1},
{'X', ZONE, SHORT - DELTA, 2},
{'X', ZONE, LONG - DELTA, 4},
{'x', ZONE, NARROW - DELTA, 1},
{'x', ZONE, SHORT - DELTA, 2},
{'x', ZONE, LONG - DELTA, 4},
};
/**
* A compact storage mechanism for skeleton field strings. Several dozen of these will be
* created for a typical DateTimePatternGenerator instance.
*
* @author sffc
*/
private static class SkeletonFields {
private byte[] chars = new byte[TYPE_LIMIT];
private byte[] lengths = new byte[TYPE_LIMIT];
private static final byte DEFAULT_CHAR = '\0';
private static final byte DEFAULT_LENGTH = 0;
public void clear() {
Arrays.fill(chars, DEFAULT_CHAR);
Arrays.fill(lengths, DEFAULT_LENGTH);
}
void copyFieldFrom(SkeletonFields other, int field) {
chars[field] = other.chars[field];
lengths[field] = other.lengths[field];
}
void clearField(int field) {
chars[field] = DEFAULT_CHAR;
lengths[field] = DEFAULT_LENGTH;
}
char getFieldChar(int field) {
return (char) chars[field];
}
int getFieldLength(int field) {
return lengths[field];
}
void populate(int field, String value) {
// Ensure no loss in character data
for (char ch : value.toCharArray()) {
assert ch == value.charAt(0);
}
populate(field, value.charAt(0), value.length());
}
void populate(int field, char ch, int length) {
assert ch <= Byte.MAX_VALUE;
assert length <= Byte.MAX_VALUE;
chars[field] = (byte) ch;
lengths[field] = (byte) length;
}
public boolean isFieldEmpty(int field) {
return lengths[field] == DEFAULT_LENGTH;
}
@Override
public String toString() {
return appendTo(new StringBuilder(), false, false).toString();
}
public String toString(boolean skipDayPeriod) {
return appendTo(new StringBuilder(), false, skipDayPeriod).toString();
}
@SuppressWarnings("unused")
public String toCanonicalString() {
return appendTo(new StringBuilder(), true, false).toString();
}
public String toCanonicalString(boolean skipDayPeriod) {
return appendTo(new StringBuilder(), true, skipDayPeriod).toString();
}
@SuppressWarnings("unused")
public StringBuilder appendTo(StringBuilder sb) {
return appendTo(sb, false, false);
}
private StringBuilder appendTo(StringBuilder sb, boolean canonical, boolean skipDayPeriod) {
for (int i = 0; i < TYPE_LIMIT; ++i) {
if (skipDayPeriod && i == DAYPERIOD) {
continue;
}
appendFieldTo(i, sb, canonical);
}
return sb;
}
public StringBuilder appendFieldTo(int field, StringBuilder sb) {
return appendFieldTo(field, sb, false);
}
private StringBuilder appendFieldTo(int field, StringBuilder sb, boolean canonical) {
char ch = (char) chars[field];
int length = lengths[field];
if (canonical) {
ch = getCanonicalChar(field, ch);
}
for (int i = 0; i < length; i++) {
sb.append(ch);
}
return sb;
}
public int compareTo(SkeletonFields other) {
for (int i = 0; i < TYPE_LIMIT; ++i) {
int charDiff = chars[i] - other.chars[i];
if (charDiff != 0) {
return charDiff;
}
int lengthDiff = lengths[i] - other.lengths[i];
if (lengthDiff != 0) {
return lengthDiff;
}
}
return 0;
}
@Override
public boolean equals(Object other) {
return this == other
|| (other != null
&& other instanceof SkeletonFields
&& compareTo((SkeletonFields) other) == 0);
}
@Override
public int hashCode() {
return Arrays.hashCode(chars) ^ Arrays.hashCode(lengths);
}
}
private static class DateTimeMatcher implements Comparable<DateTimeMatcher> {
// private String pattern = null;
private int[] type = new int[TYPE_LIMIT];
private SkeletonFields original = new SkeletonFields();
private SkeletonFields baseOriginal = new SkeletonFields();
private boolean addedDefaultDayPeriod = false;
// just for testing; fix to make multi-threaded later
// private static FormatParser fp = new FormatParser();
public boolean fieldIsNumeric(int field) {
return type[field] > 0;
}
@Override
public String toString() {
// for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
// added a single 'a' that was not in the provided skeleton, and it will be
// removed when generating the skeleton to return.
return original.toString(addedDefaultDayPeriod);
}
// returns a string like toString but using the canonical character for most types,
// e.g. M for M or L, E for E or c, y for y or U, etc. The hour field is canonicalized
// to 'H' (for 24-hour types) or 'h' (for 12-hour types)
public String toCanonicalString() {
// for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
// added a single 'a' that was not in the provided skeleton, and it will be
// removed when generating the skeleton to return.
return original.toCanonicalString(addedDefaultDayPeriod);
}
String getBasePattern() {
// for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
// added a single 'a' that was not in the provided skeleton, and it will be
// removed when generating the skeleton to return.
return baseOriginal.toString(addedDefaultDayPeriod);
}
DateTimeMatcher set(String pattern, FormatParser fp, boolean allowDuplicateFields) {
// Reset any data stored in this instance
Arrays.fill(type, NONE);
original.clear();
baseOriginal.clear();
addedDefaultDayPeriod = false;
fp.set(pattern);
for (Object obj : fp.getItems()) {
if (!(obj instanceof VariableField)) {
continue;
}
VariableField item = (VariableField) obj;
String value = item.toString();
// don't skip 'a' anymore, dayPeriod handled specially below
int canonicalIndex = item.getCanonicalIndex();
// if (canonicalIndex < 0) {
// throw new IllegalArgumentException("Illegal field:\t"
// + field + "\t in " + pattern);
// }
int[] row = types[canonicalIndex];
int field = row[1];
if (!original.isFieldEmpty(field)) {
char ch1 = original.getFieldChar(field);
char ch2 = value.charAt(0);
if (allowDuplicateFields
|| (ch1 == 'r' && (ch2 == 'U' || ch2 == 'y'))
|| ((ch1 == 'U' || ch1 == 'y') && ch2 == 'r')) {
continue;
}
throw new IllegalArgumentException(
"Conflicting fields:\t" + ch1 + ", " + value + "\t in " + pattern);
}
original.populate(field, value);
char repeatChar = (char) row[0];
int repeatCount = row[3];
if ("GEzvQ".indexOf(repeatChar) >= 0) repeatCount = 1;
baseOriginal.populate(field, repeatChar, repeatCount);
int subField = row[2];
if (subField > 0) subField += value.length();
type[field] = subField;
}
// #20739, we have a skeleton with minutes and milliseconds, but no seconds
//
// Theoretically we would need to check and fix all fields with "gaps":
// for example year-day (no month), month-hour (no day), and so on, All the possible
// field combinations.
// Plus some smartness: year + hour => should we add month, or add day-of-year?
// What about month + day-of-week, or month + am/pm indicator.
// I think beyond a certain point we should not try to fix bad developer input and try
// guessing what they mean.
// Garbage in, garbage out.
if (!original.isFieldEmpty(MINUTE)
&& !original.isFieldEmpty(FRACTIONAL_SECOND)
&& original.isFieldEmpty(SECOND)) {
// Force the use of seconds
for (int i = 0; i < types.length; ++i) {
int[] row = types[i];
if (row[1] == SECOND) {
// first entry for SECOND
original.populate(SECOND, (char) row[0], row[3]);
baseOriginal.populate(SECOND, (char) row[0], row[3]);
// We add value.length, same as above, when type is first initialized.
// The value we want to "fake" here is "s", and 1 means "s".length()
int subField = row[2];
type[SECOND] = (subField > 0) ? subField + 1 : subField;
break;
}
}
}
// #13183, handle special behavior for day period characters (a, b, B)
if (!original.isFieldEmpty(HOUR)) {
if (original.getFieldChar(HOUR) == 'h' || original.getFieldChar(HOUR) == 'K') {
// We have a skeleton with 12-hour-cycle format
if (original.isFieldEmpty(DAYPERIOD)) {
// But we do not have a day period in the skeleton; add the default
// DAYPERIOD (currently "a")
for (int i = 0; i < types.length; ++i) {
int[] row = types[i];
if (row[1] == DAYPERIOD) {
// first entry for DAYPERIOD
original.populate(DAYPERIOD, (char) row[0], row[3]);
baseOriginal.populate(DAYPERIOD, (char) row[0], row[3]);
type[DAYPERIOD] = row[2];
addedDefaultDayPeriod = true;
break;
}
}
}
} else if (!original.isFieldEmpty(DAYPERIOD)) {
// Skeleton has 24-hour-cycle hour format and has dayPeriod, delete dayPeriod
// (i.e. ignore it)
original.clearField(DAYPERIOD);
baseOriginal.clearField(DAYPERIOD);
type[DAYPERIOD] = NONE;
}
}
return this;
}
int getFieldMask() {
int result = 0;
for (int i = 0; i < type.length; ++i) {
if (type[i] != 0) result |= (1 << i);
}
return result;
}
@SuppressWarnings("unused")
void extractFrom(DateTimeMatcher source, int fieldMask) {
for (int i = 0; i < type.length; ++i) {
if ((fieldMask & (1 << i)) != 0) {
type[i] = source.type[i];
original.copyFieldFrom(source.original, i);
} else {
type[i] = NONE;
original.clearField(i);
}
}
}
int getDistance(DateTimeMatcher other, int includeMask, DistanceInfo distanceInfo) {
int result = 0;
distanceInfo.clear();
for (int i = 0; i < TYPE_LIMIT; ++i) {
int myType = (includeMask & (1 << i)) == 0 ? 0 : type[i];
int otherType = other.type[i];
if (myType == otherType) continue; // identical (maybe both zero) add 0
if (myType == 0) { // and other is not
result += EXTRA_FIELD;
distanceInfo.addExtra(i);
} else if (otherType == 0) { // and mine is not
result += MISSING_FIELD;
distanceInfo.addMissing(i);
} else {
result += Math.abs(myType - otherType); // square of mismatch
}
}
return result;
}
@Override
public int compareTo(DateTimeMatcher that) {
int result = original.compareTo(that.original);
return result > 0 ? -1 : result < 0 ? 1 : 0; // Reverse the order.
}
@Override
public boolean equals(Object other) {
return this == other
|| (other != null
&& other instanceof DateTimeMatcher
&& original.equals(((DateTimeMatcher) other).original));
}
@Override
public int hashCode() {
return original.hashCode();
}
}
private static class DistanceInfo {
int missingFieldMask;
int extraFieldMask;
void clear() {
missingFieldMask = extraFieldMask = 0;
}
void setTo(DistanceInfo other) {
missingFieldMask = other.missingFieldMask;
extraFieldMask = other.extraFieldMask;
}
void addMissing(int field) {
missingFieldMask |= (1 << field);
}
void addExtra(int field) {
extraFieldMask |= (1 << field);
}
@Override
public String toString() {
return "missingFieldMask: "
+ DateTimePatternGenerator.showMask(missingFieldMask)
+ ", extraFieldMask: "
+ DateTimePatternGenerator.showMask(extraFieldMask);
}
}
}
// eof