blob: 6fb7ba34e90e85b6b21bf0671b01c30cadd9b2f8 [file] [log] [blame]
//##header
//#ifndef FOUNDATION
/*
*******************************************************************************
* Copyright (C) 2006, Google, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*
* $Source: /xsrl/Nsvn/icu/icu4j/src/com/ibm/icu/text/DateTimePatternGenerator.java,v $
* $Date: 2006/09/15 18:09:24 $
* $Revision: 1.9 $
*
*******************************************************************************
*/
package com.ibm.icu.text;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
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.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
//import org.unicode.cldr.util.Utility;
import com.ibm.icu.impl.CalendarData;
import com.ibm.icu.impl.PatternTokenizer;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.Transliterator;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* 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.
* <p><i>Issue: may be useful to also have a function that returns the list of fields in a pattern, in order, since we have that internally.
* That would be useful for getting the UI order of field elements.</i>
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public class DateTimePatternGenerator implements Freezable, Cloneable {
// debugging flags
//static boolean SHOW_DISTANCE = false;
// TODO add hack to fix months for CJK, as per bug 1099
// http://dev.icu-project.org/cgi-bin/locale-bugs/incoming?findid=1099
/**
* Create empty generator, to be constructed with add(...) etc.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static DateTimePatternGenerator newInstance() {
return new DateTimePatternGenerator();
}
/**
* Only for use by subclasses
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
protected DateTimePatternGenerator() {
}
/**
* Construct a flexible generator according to data for a given locale.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static DateTimePatternGenerator getInstance() {
return getInstance(ULocale.getDefault());
}
/**
* Construct a flexible generator according to data for a given locale.
* @param uLocale
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static DateTimePatternGenerator getInstance(ULocale uLocale) {
DateTimePatternGenerator result = new DateTimePatternGenerator();
PatternInfo returnInfo = new PatternInfo();
String hackPattern = null;
// first load with the ICU patterns
for (int i = DateFormat.FULL; i <= DateFormat.SHORT; ++i) {
SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(i, uLocale);
result.add(df.toPattern(), false, returnInfo);
df = (SimpleDateFormat) DateFormat.getTimeInstance(i, uLocale);
result.add(df.toPattern(), false, returnInfo);
// HACK for hh:ss
if (i == DateFormat.MEDIUM) {
hackPattern = df.toPattern();
}
}
UResourceBundle rb = UResourceBundle.getBundleInstance("com.ibm.icu.impl.data.DateData$MyDateResources", uLocale);
//ResourceBundle rb = ResourceBundle.getBundle("com.ibm.icu.impl.data.DateData$MyDateResources", ULocale.FRENCH.toLocale());
for (Enumeration en = rb.getKeys(); en.hasMoreElements();) {
String key = (String) en.nextElement();
String value = rb.getString(key);
String [] keyParts = key.split("/");
if (keyParts[0].equals("pattern")) {
result.add(value, false, returnInfo);
} else if (keyParts[0].equals("append")) {
result.setAppendItemFormats(getAppendFormatNumber(keyParts[1]), value);
} else if (keyParts[0].equals("field")) {
result.setAppendItemNames(getAppendNameNumber(keyParts[1]), value);
}
}
// assume it is always big endian (ok for CLDR right now)
// some languages didn't add mm:ss or HH:mm, so put in a hack to compute that from the short time.
if (hackPattern != null) {
hackTimes(result, returnInfo, hackPattern);
}
// set the datetime pattern. This is ugly code -- there should be a public interface for this
Calendar cal = Calendar.getInstance(uLocale);
CalendarData calData = new CalendarData(uLocale, cal.getType());
String[] patterns = calData.get("DateTimePatterns").getStringArray();
result.setDateTimeFormat(patterns[8]);
// decimal point for seconds
DecimalFormatSymbols dfs = new DecimalFormatSymbols(uLocale);
result.setDecimal(String.valueOf(dfs.getDecimalSeparator()));
return result;
}
private static void hackTimes(DateTimePatternGenerator result, PatternInfo returnInfo, String hackPattern) {
result.fp.set(hackPattern);
String mmss = new String();
// to get mm:ss, we strip all but mm literal ss
boolean gotMm = false;
for (int i = 0; i < result.fp.items.size(); ++i) {
Object item = result.fp.items.get(i);
if (item instanceof String) {
if (gotMm) {
mmss += result.fp.quoteLiteral(item.toString());
}
} else {
char ch = item.toString().charAt(0);
if (ch == 'm') {
gotMm = true;
mmss += item;
} else if (ch == 's') {
if (!gotMm) {
break; // failed
}
mmss += item;
result.add(mmss, 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 < result.fp.items.size(); ++i) {
Object item = result.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(result.fp, nuke);
result.add(hhmm, false, returnInfo);
}
private static String getFilteredPattern(FormatParser fp, BitSet nuke) {
String result = new String();
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 += fp.quoteLiteral(item.toString());
} else {
result += item.toString();
}
}
return result;
}
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;
}
private 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;
}
/**
* 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.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getBestPattern(String skeleton) {
//if (!isComplete) complete();
current.set(skeleton, fp);
String best = getBestRaw(current, -1, _distanceInfo);
if (_distanceInfo.missingFieldMask == 0 && _distanceInfo.extraFieldMask == 0) {
// we have a good item. Adjust the field types
return adjustFieldTypes(best, current, false);
}
int neededFields = current.getFieldMask();
// otherwise break up by date and time.
String datePattern = getBestAppending(neededFields & DATE_MASK);
String timePattern = getBestAppending(neededFields & TIME_MASK);
if (datePattern == null) return timePattern == null ? "" : timePattern;
if (timePattern == null) return datePattern;
return MessageFormat.format(getDateTimeFormat(), new Object[]{datePattern, timePattern});
}
/**
* PatternInfo supplies output parameters for add(...). It is used because
* Java doesn't have real output parameters. It is treated like a struct (eg
* Point), so all fields are public.
*
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static final class PatternInfo { // struct for return information
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static final int OK = 0;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static final int BASE_CONFLICT = 1;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public static final int CONFLICT = 2;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public int status;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String conflictingPattern;
/**
* Simple constructor, since this is treated like a struct.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public PatternInfo() {
}
}
static Transliterator fromHex = Transliterator.getInstance("hex-any");
/**
* 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 overriden. 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!
*
* @param override
* when existing values are to be overridden use true, otherwise
* use false.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public DateTimePatternGenerator add(String pattern, boolean override, PatternInfo returnInfo) {
checkFrozen();
if (pattern.indexOf("\\u") >= 0) {
String oldPattern = pattern;
pattern = fromHex.transliterate(pattern);
}
DateTimeMatcher matcher = new DateTimeMatcher().set(pattern, fp);
String basePattern = matcher.getBasePattern();
String previousPatternWithSameBase = (String)basePattern_pattern.get(basePattern);
if (previousPatternWithSameBase != null) {
returnInfo.status = PatternInfo.BASE_CONFLICT;
returnInfo.conflictingPattern = previousPatternWithSameBase;
if (!override) return this;
}
String previousValue = (String)skeleton2pattern.get(matcher);
if (previousValue != null) {
returnInfo.status = PatternInfo.CONFLICT;
returnInfo.conflictingPattern = previousValue;
if (!override) return this;
}
returnInfo.status = PatternInfo.OK;
returnInfo.conflictingPattern = "";
skeleton2pattern.put(matcher, pattern);
basePattern_pattern.put(basePattern, pattern);
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"
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getSkeleton(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp);
return current.toString();
}
}
/**
* 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"
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getBaseSkeleton(String pattern) {
synchronized (this) { // synchronized since a getter must be thread-safe
current.set(pattern, fp);
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.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public Map getSkeletons(Map result) {
if (result == null) result = new LinkedHashMap();
for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
DateTimeMatcher item = (DateTimeMatcher) it.next();
String pattern = (String) skeleton2pattern.get(item);
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
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public Set getBaseSkeletons(Set 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.
*
* @param pattern
* input pattern
* @param skeleton
* @return pattern adjusted to match the skeleton fields widths and
* subtypes.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String replaceFieldTypes(String pattern, String skeleton) {
synchronized (this) { // synchronized since a getter must be thread-safe
return adjustFieldTypes(pattern, current.set(skeleton, fp), false);
}
}
/**
* The date time format is a message format pattern used to compose date and
* time patterns. The default value is "{0} {1}", where {0} will be replaced
* by the date pattern and {1} 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 "{0} {1}". 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".
*
* @param dateTimeFormat
* message format pattern, here {0} will be replaced by the date
* pattern and {1} will be replaced by the time pattern.
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public void setDateTimeFormat(String dateTimeFormat) {
checkFrozen();
this.dateTimeFormat = dateTimeFormat;
}
/**
* Getter corresponding to setDateTimeFormat.
*
* @return pattern
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getDateTimeFormat() {
return dateTimeFormat;
}
/**
* 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
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public void setDecimal(String decimal) {
checkFrozen();
this.decimal = decimal;
}
/**
* Getter corresponding to setDecimal.
* @return string corresponding to the decimal point
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
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.
* @deprecated
* @internal
*/
public Collection getRedundants(Collection output) {
synchronized (this) { // synchronized since a getter must be thread-safe
if (output == null) output = new LinkedHashSet();
for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
DateTimeMatcher current = (DateTimeMatcher) it.next();
String pattern = (String) skeleton2pattern.get(current);
if (CANONICAL_SET.contains(pattern)) continue;
skipMatcher = current;
String trial = getBestPattern(current.toString());
if (trial.equals(pattern)) {
output.add(pattern);
}
}
if (false) { // ordered
DateTimePatternGenerator results = new DateTimePatternGenerator();
PatternInfo pinfo = new PatternInfo();
for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
DateTimeMatcher current = (DateTimeMatcher) it.next();
String pattern = (String) skeleton2pattern.get(current);
if (CANONICAL_SET.contains(pattern)) continue;
//skipMatcher = current;
String trial = results.getBestPattern(current.toString());
if (trial.equals(pattern)) {
output.add(pattern);
} else {
results.add(pattern, false, pinfo);
}
}
}
return output;
}
}
// Field numbers, used for AppendItem functions
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int ERA = 0;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int YEAR = 1;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int QUARTER = 2;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int MONTH = 3;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int WEEK_OF_YEAR = 4;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int WEEK_OF_MONTH = 5;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int WEEKDAY = 6;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int DAY = 7;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int DAY_OF_YEAR = 8;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int DAY_OF_WEEK_IN_MONTH = 9;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int DAYPERIOD = 10;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int HOUR = 11;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int MINUTE = 12;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int SECOND = 13;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int FRACTIONAL_SECOND = 14;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int ZONE = 15;
/**
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
static final public int TYPE_LIMIT = 16;
/**
* 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}"
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public void setAppendItemFormats(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
* @return append pattern for field
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getAppendItemFormats(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
* @param value
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public void setAppendItemNames(int field, String value) {
checkFrozen();
appendItemNames[field] = value;
}
/**
* Getter corresponding to setAppendItemNames. Values below 0 or at or above
* TYPE_LIMIT are illegal arguments.
*
* @param field
* @return name for field
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public String getAppendItemNames(int field) {
return appendItemNames[field];
}
/**
* Determines whether a skeleton contains a single field
*
* @param skeleton
* @return true or not
* @deprecated
* @internal
*/
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;
}
/**
* Boilerplate for Freezable
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public boolean isFrozen() {
return frozen;
}
/**
* Boilerplate for Freezable
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public Object freeze() {
frozen = true;
return this;
}
/**
* Boilerplate for Freezable
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public Object cloneAsThawed() {
DateTimePatternGenerator result = (DateTimePatternGenerator) (this.clone());
frozen = false;
return result;
}
/**
* Boilerplate
* @draft ICU 3.6
* @provisional This API might change or be removed in a future release.
*/
public Object clone() {
try {
DateTimePatternGenerator result = (DateTimePatternGenerator) (super.clone());
result.skeleton2pattern = (TreeMap) skeleton2pattern.clone();
result.basePattern_pattern = (TreeMap) basePattern_pattern.clone();
result.appendItemFormats = (String[]) appendItemFormats.clone();
result.appendItemNames = (String[]) appendItemNames.clone();
result.current = new DateTimeMatcher();
result.fp = new FormatParser();
result._distanceInfo = new DistanceInfo();
result.frozen = false;
return result;
} catch (CloneNotSupportedException e) {
throw new IllegalArgumentException("Internal Error");
}
}
/**
* Utility class for FormatParser. Immutable class.
* @deprecated
* @internal
*/
public static class VariableField {
private String string;
/**
* Create a variable field
* @param string
* @deprecated
* @internal
*/
public VariableField(String string) {
this.string = string;
}
/**
* Get the internal results
* @deprecated
* @internal
*/
public String toString() {
return string;
}
}
/**
* Class providing date formatting
* @deprecated
* @internal
*/
static public class FormatParser {
private transient PatternTokenizer tokenizer = new PatternTokenizer()
.setSyntaxCharacters(new UnicodeSet("[a-zA-Z]"))
//.setEscapeCharacters(new UnicodeSet("[^\\u0020-\\u007E]")) // WARNING: DateFormat doesn't accept \\uXXXX
.setUsingQuote(true);
private List items = new ArrayList();
/**
* Set the string to parse
* @param string
* @return this, for chaining
* @deprecated
* @internal
*/
public FormatParser set(String string) {
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);
}
variable.append(buffer);
} else {
addVariable(variable);
items.add(buffer.toString());
}
}
addVariable(variable);
return this;
}
private void addVariable(StringBuffer variable) {
if (variable.length() != 0) {
items.add(new VariableField(variable.toString()));
variable.setLength(0);
}
}
/** Return a collection of fields. These will be a mixture of 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(s);
}
}
//System.out.println(output);
return output;
}
/**
* @return a string which is a concatenation of all the variable fields
* @deprecated
* @internal
*/
public String getVariableFieldString() {
List list = getVariableFields(null);
StringBuffer result = new StringBuffer();
for (Iterator it = list.iterator(); it.hasNext();) {
String item = (String) it.next();
result.append(item);
}
return result.toString();
}
/**
* Returns modifiable list which is a mixture of Strings and VariableFields, in the order found during parsing.
* @return modifiable list of items.
* @deprecated
* @internal
*/
public List getItems() {
return items;
}
/** Provide display form of formatted input
* @return printable output string
* @deprecated
* @internal
*/
public String toString() {
return toString(0, items.size());
}
/**
* Provide display form of formatted input
* @param start item to start from
* @param limit last item +1
* @return printable output string
* @deprecated
* @internal
*/
public String toString(int start, int limit) {
StringBuffer result = new StringBuffer();
for (int i = start; i < limit; ++i) {
result.append(items.get(i).toString());
}
return result.toString();
}
/**
* Internal method <p>
* Returns true if it has a mixture of date and time fields
* @return true or false
* @deprecated
* @internal
*/
public boolean hasDateAndTimeFields() {
int foundMask = 0;
for (Iterator it = items.iterator(); it.hasNext();) {
Object item = it.next();
if (item instanceof VariableField) {
int type = getType(item);
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
* @deprecated
* @internal
*/
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 = getType(item);
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 = getType(item);
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 = getType(item);
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;
}
/**
* produce a quoted literal
* @param string
* @return string with quoted literals
* @deprecated
* @internal
*/
public Object quoteLiteral(String string) {
return tokenizer.quoteLiteral(string);
}
/**
* Simple constructor, since this is treated like a struct.
* @deprecated
* @internal
*/
public FormatParser() {
super();
// TODO Auto-generated constructor stub
}
}
// ========= PRIVATES ============
private TreeMap skeleton2pattern = new TreeMap(); // items are in priority order
private TreeMap basePattern_pattern = new TreeMap(); // items are in priority order
private String decimal = "?";
private String dateTimeFormat = "{0} {1}";
private String[] appendItemFormats = new String[TYPE_LIMIT];
private String[] appendItemNames = new String[TYPE_LIMIT];
{
for (int i = 0; i < TYPE_LIMIT; ++i) {
appendItemFormats[i] = "{0} \u251C{2}: {1}\u2524";
appendItemNames[i] = "F" + i;
}
}
private transient DateTimeMatcher current = new DateTimeMatcher();
private transient FormatParser fp = new FormatParser();
private transient DistanceInfo _distanceInfo = new DistanceInfo();
private transient boolean isComplete = false;
private transient DateTimeMatcher skipMatcher = null; // only used temporarily, for internal purposes
private transient boolean frozen = false;
private static final int FRACTIONAL_MASK = 1<<FRACTIONAL_SECOND;
private static final int SECOND_AND_FRACTIONAL_MASK = (1<<SECOND) | (1<<FRACTIONAL_SECOND);
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(int missingFields) {
String resultPattern = null;
if (missingFields != 0) {
resultPattern = getBestRaw(current, missingFields, _distanceInfo);
resultPattern = adjustFieldTypes(resultPattern, current, false);
while (_distanceInfo.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 ((_distanceInfo.missingFieldMask & SECOND_AND_FRACTIONAL_MASK) == FRACTIONAL_MASK
&& (missingFields & SECOND_AND_FRACTIONAL_MASK) == SECOND_AND_FRACTIONAL_MASK) {
resultPattern = adjustFieldTypes(resultPattern, current, true);
_distanceInfo.missingFieldMask &= ~FRACTIONAL_MASK; // remove bit
continue;
}
int startingMask = _distanceInfo.missingFieldMask;
String temp = getBestRaw(current, _distanceInfo.missingFieldMask, _distanceInfo);
temp = adjustFieldTypes(temp, current, false);
int foundMask = startingMask & ~_distanceInfo.missingFieldMask;
int topField = getTopBitNumber(foundMask);
resultPattern = MessageFormat.format(getAppendFormat(topField), new Object[]{resultPattern, temp, getAppendName(topField)});
}
}
return resultPattern;
}
private String getAppendName(int foundMask) {
return "'" + appendItemNames[foundMask] + "'";
}
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
* @return
*/
private int getTopBitNumber(int foundMask) {
int i = 0;
while (foundMask != 0) {
foundMask >>>= 1;
++i;
}
return i-1;
}
/**
*
*/
private void complete() {
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) {
char c = (char)types[i][0];
add(String.valueOf(CANONICAL_ITEMS[i]), false, patternInfo);
}
isComplete = true;
}
{
complete();
}
/**
*
*/
private String getBestRaw(DateTimeMatcher source, int includeMask, DistanceInfo missingFields) {
// if (SHOW_DISTANCE) System.out.println("Searching for: " + source.pattern
// + ", mask: " + showMask(includeMask));
int bestDistance = Integer.MAX_VALUE;
String bestPattern = "";
DistanceInfo tempInfo = new DistanceInfo();
for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
DateTimeMatcher trial = (DateTimeMatcher) it.next();
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);
if (distance < bestDistance) {
bestDistance = distance;
bestPattern = (String) skeleton2pattern.get(trial);
missingFields.setTo(tempInfo);
if (distance == 0) break;
}
}
return bestPattern;
}
/**
* @param fixFractionalSeconds TODO
*
*/
private String adjustFieldTypes(String pattern, DateTimeMatcher inputRequest, boolean fixFractionalSeconds) {
fp.set(pattern);
StringBuffer newPattern = new StringBuffer();
for (Iterator it = fp.getItems().iterator(); it.hasNext();) {
Object item = it.next();
if (item instanceof String) {
newPattern.append(fp.quoteLiteral((String)item));
} else {
String field = ((VariableField) item).string;
int canonicalIndex = getCanonicalIndex(field);
if (canonicalIndex < 0) {
continue; // don't adjust
}
int type = types[canonicalIndex][1];
if (fixFractionalSeconds && type == SECOND) {
String newField = inputRequest.original[FRACTIONAL_SECOND];
field = field + decimal + newField;
} else if (inputRequest.type[type] != 0) {
String newField = inputRequest.original[type];
// normally we just replace the field. However HOUR is special; we only change the length
if (type != HOUR) {
field = newField;
} else if (field.length() != newField.length()){
char c = field.charAt(0);
field = "";
for (int i = newField.length(); i > 0; --i) field += c;
}
}
newPattern.append(field);
}
}
//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
* @return field value
* @deprecated
* @internal
*/
public String getFields(String pattern) {
fp.set(pattern);
StringBuffer newPattern = new StringBuffer();
for (Iterator it = fp.getItems().iterator(); it.hasNext();) {
Object item = it.next();
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) {
String result = "";
for (int i = 0; i < TYPE_LIMIT; ++i) {
if ((mask & (1<<i)) == 0) continue;
if (result.length() != 0) result += " | ";
result += FIELD_NAME[i] + " ";
}
return result;
}
static private String[] CLDR_FIELD_APPEND = {
"Era", "Year", "Quarter", "Month", "Week", "*", "Day-Of-Week",
"Day", "*", "*", "*",
"Hour", "Minute", "Second", "*", "Timezone"
};
static private String[] CLDR_FIELD_NAME = {
"era", "year", "quarter", "month", "week", "*", "weekday",
"day", "*", "*", "dayperiod",
"hour", "minute", "second", "*", "zone"
};
static private 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"
};
static private String[] CANONICAL_ITEMS = {
"G", "y", "Q", "M", "w", "W", "e",
"d", "D", "F",
"H", "m", "s", "S", "v"
};
static private Set CANONICAL_SET = new HashSet(Arrays.asList(CANONICAL_ITEMS));
static final private int
DATE_MASK = (1<<DAYPERIOD) - 1,
TIME_MASK = (1<<TYPE_LIMIT) - 1 - DATE_MASK;
static final private int // numbers are chosen to express 'distance'
DELTA = 0x10,
NUMERIC = 0x100,
NONE = 0,
NARROW = -0x100,
SHORT = -0x101,
LONG = -0x102,
EXTRA_FIELD = 0x10000,
MISSING_FIELD = 0x1000;
static private String getName(String s) {
int i = getCanonicalIndex(s);
String name = FIELD_NAME[types[i][1]];
int subtype = types[i][2];
boolean string = subtype < 0;
if (string) subtype = -subtype;
if (subtype < 0) name += ":S";
else name += ":N";
return name;
}
static private int getCanonicalIndex(String s) {
int len = s.length();
int ch = s.charAt(0);
for (int i = 0; i < types.length; ++i) {
int[] row = types[i];
if (row[0] != ch) continue;
if (row[3] > len) continue;
if (row[row.length-1] < len) continue;
return i;
}
return -1;
}
static private 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},
{'y', YEAR, NUMERIC, 1, 20},
{'Y', YEAR, NUMERIC + DELTA, 1, 20},
{'u', YEAR, NUMERIC + 2*DELTA, 1, 20},
{'Q', QUARTER, NUMERIC, 1, 2},
{'Q', QUARTER, SHORT, 3},
{'Q', QUARTER, LONG, 4},
{'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},
{'w', WEEK_OF_YEAR, NUMERIC, 1, 2},
{'W', WEEK_OF_MONTH, NUMERIC + DELTA, 1},
{'e', WEEKDAY, NUMERIC + DELTA, 1, 2},
{'e', WEEKDAY, SHORT - DELTA, 3},
{'e', WEEKDAY, LONG - DELTA, 4},
{'e', WEEKDAY, NARROW - DELTA, 5},
{'E', WEEKDAY, SHORT, 1, 3},
{'E', WEEKDAY, LONG, 4},
{'E', WEEKDAY, NARROW, 5},
{'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},
{'d', DAY, NUMERIC, 1, 2},
{'D', DAY_OF_YEAR, NUMERIC + DELTA, 1, 3},
{'F', DAY_OF_WEEK_IN_MONTH, NUMERIC + 2*DELTA, 1},
{'g', DAY, NUMERIC + 3*DELTA, 1, 20}, // really internal use, so we don't care
{'a', DAYPERIOD, SHORT, 1},
{'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},
{'S', FRACTIONAL_SECOND, NUMERIC + DELTA, 1, 1000},
{'A', SECOND, NUMERIC + 2*DELTA, 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, SHORT - DELTA, 1, 3},
{'Z', ZONE, LONG - DELTA, 4},
};
private static class DateTimeMatcher implements Comparable {
//private String pattern = null;
private int[] type = new int[TYPE_LIMIT];
private String[] original = new String[TYPE_LIMIT];
private String[] baseOriginal = new String[TYPE_LIMIT];
// just for testing; fix to make multi-threaded later
// private static FormatParser fp = new FormatParser();
public String toString() {
StringBuffer result = new StringBuffer();
for (int i = 0; i < TYPE_LIMIT; ++i) {
if (original[i].length() != 0) result.append(original[i]);
}
return result.toString();
}
String getBasePattern() {
StringBuffer result = new StringBuffer();
for (int i = 0; i < TYPE_LIMIT; ++i) {
if (baseOriginal[i].length() != 0) result.append(baseOriginal[i]);
}
return result.toString();
}
DateTimeMatcher set(String pattern, FormatParser fp) {
if (pattern.indexOf("\\u") >= 0) {
String oldPattern = pattern;
pattern = fromHex.transliterate(pattern);
}
for (int i = 0; i < TYPE_LIMIT; ++i) {
type[i] = NONE;
original[i] = "";
baseOriginal[i] = "";
}
fp.set(pattern);
for (Iterator it = fp.getVariableFields(new ArrayList()).iterator(); it.hasNext();) {
String field = (String) it.next();
if (field.charAt(0) == 'a') continue; // skip day period, special cass
int canonicalIndex = getCanonicalIndex(field);
if (canonicalIndex < 0) {
throw new IllegalArgumentException("Illegal field:\t"
+ field + "\t in " + pattern);
}
int[] row = types[canonicalIndex];
int typeValue = row[1];
if (original[typeValue].length() != 0) {
throw new IllegalArgumentException("Conflicting fields:\t"
+ original[typeValue] + ", " + field + "\t in " + pattern);
}
original[typeValue] = field;
char repeatChar = (char)row[0];
int repeatCount = row[3];
if (repeatCount > 3) repeatCount = 3; // hack to discard differences
if ("GEzvQ".indexOf(repeatChar) >= 0) repeatCount = 1;
baseOriginal[typeValue] = Utility.repeat(String.valueOf(repeatChar),repeatCount);
int subTypeValue = row[2];
if (subTypeValue > 0) subTypeValue += field.length();
type[typeValue] = (byte) subTypeValue;
}
return this;
}
/**
*
*/
int getFieldMask() {
int result = 0;
for (int i = 0; i < type.length; ++i) {
if (type[i] != 0) result |= (1<<i);
}
return result;
}
/**
*
*/
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[i] = source.original[i];
} else {
type[i] = NONE;
original[i] = "";
}
}
}
int getDistance(DateTimeMatcher other, int includeMask, DistanceInfo distanceInfo) {
int result = 0;
distanceInfo.clear();
for (int i = 0; i < type.length; ++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;
}
public int compareTo(Object o) {
DateTimeMatcher that = (DateTimeMatcher) o;
for (int i = 0; i < original.length; ++i) {
int comp = original[i].compareTo(that.original[i]);
if (comp != 0) return -comp;
}
return 0;
}
public boolean equals(Object other) {
if (other == null) return false;
DateTimeMatcher that = (DateTimeMatcher) other;
for (int i = 0; i < original.length; ++i) {
if (!original[i].equals(that.original[i])) return false;
}
return true;
}
public int hashCode() {
int result = 0;
for (int i = 0; i < original.length; ++i) {
result ^= original[i].hashCode();
}
return result;
}
}
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);
}
public String toString() {
return "missingFieldMask: " + DateTimePatternGenerator.showMask(missingFieldMask)
+ ", extraFieldMask: " + DateTimePatternGenerator.showMask(extraFieldMask);
}
}
}
//#endif
//eof