3 ********************************************************************************
4 * Copyright (C) 2006-2009, Google, International Business Machines Corporation *
5 * and others. All Rights Reserved. *
6 ********************************************************************************
8 package com.ibm.icu.text;
10 import com.ibm.icu.impl.ICUCache;
11 import com.ibm.icu.impl.ICUResourceBundle;
12 import com.ibm.icu.impl.PatternTokenizer;
13 import com.ibm.icu.impl.SimpleCache;
14 import com.ibm.icu.impl.Utility;
15 import com.ibm.icu.util.Calendar;
16 import com.ibm.icu.util.Freezable;
17 import com.ibm.icu.util.ULocale;
18 import com.ibm.icu.util.UResourceBundle;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.BitSet;
23 import java.util.Collection;
24 //#if defined(FOUNDATION10) || defined(J2SE13)
25 //##import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 //#if defined(FOUNDATION10) || defined(J2SE13)
31 //import java.util.LinkedHashMap;
32 //import java.util.LinkedHashSet;
34 import java.util.List;
37 import java.util.TreeMap;
40 * This class provides flexible generation of date format patterns, like
41 * "yy-MM-dd". The user can build up the generator by adding successive
42 * patterns. Once that is done, a query can be made using a "skeleton", which is
43 * a pattern which just includes the desired fields and lengths. The generator
44 * will return the "best fit" pattern corresponding to that skeleton.
46 * The main method people will use is getBestPattern(String skeleton), since
47 * normally this class is pre-built with data from a particular locale. However,
48 * generators can be built directly from other data as well.
50 * // some simple use cases
51 * Date sampleDate = new Date(99, 9, 13, 23, 58, 59);
52 * ULocale locale = ULocale.GERMANY;
53 * TimeZone zone = TimeZone.getTimeZone("Europe/Paris");
57 * DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale);
58 * SimpleDateFormat format = new SimpleDateFormat(gen.getBestPattern("MMMddHmm"),
60 * format.setTimeZone(zone);
61 * assertEquals("simple format: MMMddHmm",
62 * "8:58 14. Okt",
63 * format.format(sampleDate));
64 * // (a generator can be built from scratch, but that is not a typical use case)
66 * // modify the generator by adding patterns
67 * DateTimePatternGenerator.PatternInfo returnInfo = new DateTimePatternGenerator.PatternInfo();
68 * gen.add("d'. von' MMMM", true, returnInfo);
69 * // the returnInfo is mostly useful for debugging problem cases
70 * format.applyPattern(gen.getBestPattern("MMMMddHmm"));
71 * assertEquals("modified format: MMMddHmm",
72 * "8:58 14. von Oktober",
73 * format.format(sampleDate));
75 * // get a pattern and modify it
76 * format = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.FULL,
77 * DateFormat.FULL, locale);
78 * format.setTimeZone(zone);
79 * String pattern = format.toPattern();
80 * assertEquals("full-date",
81 * "Donnerstag, 14. Oktober 1999 8:58 Uhr GMT+02:00",
82 * format.format(sampleDate));
84 * // modify it to change the zone.
85 * String newPattern = gen.replaceFieldTypes(pattern, "vvvv");
86 * format.applyPattern(newPattern);
87 * assertEquals("full-date, modified zone",
88 * "Donnerstag, 14. Oktober 1999 8:58 Uhr Frankreich",
89 * format.format(sampleDate));
93 public class DateTimePatternGenerator implements Freezable, Cloneable {
95 //static boolean SHOW_DISTANCE = false;
96 // TODO add hack to fix months for CJK, as per bug ticket 1099
99 * Create empty generator, to be constructed with add(...) etc.
102 public static DateTimePatternGenerator getEmptyInstance() {
103 return new DateTimePatternGenerator();
107 * Only for use by subclasses
110 protected DateTimePatternGenerator() {
114 * Construct a flexible generator according to data for a given locale.
117 public static DateTimePatternGenerator getInstance() {
118 return getInstance(ULocale.getDefault());
122 * Construct a flexible generator according to data for a given locale.
126 public static DateTimePatternGenerator getInstance(ULocale uLocale) {
127 String localeKey = uLocale.toString();
128 DateTimePatternGenerator result = (DateTimePatternGenerator)DTPNG_CACHE.get(localeKey);
129 if (result != null) {
132 result = new DateTimePatternGenerator();
133 String lang = uLocale.getLanguage();
134 if (lang.equals("zh") || lang.equals("ko") || lang.equals("ja")) {
135 result.chineseMonthHack = true;
137 PatternInfo returnInfo = new PatternInfo();
138 String shortTimePattern = null;
139 // first load with the ICU patterns
140 for (int i = DateFormat.FULL; i <= DateFormat.SHORT; ++i) {
141 SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(i, uLocale);
142 result.addPattern(df.toPattern(), false, returnInfo);
143 df = (SimpleDateFormat) DateFormat.getTimeInstance(i, uLocale);
144 result.addPattern(df.toPattern(), false, returnInfo);
145 if (i == DateFormat.SHORT) {
146 // keep this pattern to populate other time field
147 // combination patterns by hackTimes later in this method.
148 shortTimePattern = df.toPattern();
150 // use hour style in SHORT time pattern as the default
151 // hour style for the locale
152 FormatParser fp = new FormatParser();
153 fp.set(shortTimePattern);
154 List items = fp.getItems();
155 for (int idx = 0; idx < items.size(); idx++) {
156 Object item = items.get(idx);
157 if (item instanceof VariableField) {
158 VariableField fld = (VariableField)item;
159 if (fld.getType() == HOUR) {
160 result.defaultHourFormatChar = fld.toString().charAt(0);
168 ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, uLocale);
169 rb = rb.getWithFallback("calendar");
170 ICUResourceBundle gregorianBundle = rb.getWithFallback("gregorian");
173 ICUResourceBundle itemBundle = gregorianBundle.getWithFallback("appendItems");
174 for (int i=0; i<itemBundle.getSize(); ++i) {
175 ICUResourceBundle formatBundle = (ICUResourceBundle)itemBundle.get(i);
176 String formatName = itemBundle.get(i).getKey();
177 String value = formatBundle.getString();
178 result.setAppendItemFormat(getAppendFormatNumber(formatName), value);
182 itemBundle = gregorianBundle.getWithFallback("fields");
183 ICUResourceBundle fieldBundle, dnBundle;
184 for (int i=0; i<TYPE_LIMIT; ++i) {
185 if ( isCLDRFieldName(i) ) {
186 fieldBundle = itemBundle.getWithFallback(CLDR_FIELD_NAME[i]);
187 dnBundle = fieldBundle.getWithFallback("dn");
188 String value = dnBundle.getString();
189 //System.out.println("Field name:"+value);
190 result.setAppendItemName(i, value);
194 // set the AvailableFormat in CLDR
196 ICUResourceBundle formatBundle = gregorianBundle.getWithFallback("availableFormats");
197 //System.out.println("available format from current locale:"+uLocale.getName());
198 for (int i=0; i<formatBundle.getSize(); ++i) {
199 String formatKey = formatBundle.get(i).getKey();
200 String formatValue = formatBundle.get(i).getString();
201 //System.out.println(" availableFormat:"+formatValue);
202 result.setAvailableFormat(formatKey);
203 result.addPattern(formatValue, false, returnInfo);
205 }catch(Exception e) {
208 ULocale parentLocale=uLocale;
209 while ( (parentLocale=parentLocale.getFallback()) != null) {
210 ICUResourceBundle prb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, parentLocale);
211 prb = prb.getWithFallback("calendar");
212 ICUResourceBundle pGregorianBundle = prb.getWithFallback("gregorian");
214 ICUResourceBundle formatBundle = pGregorianBundle.getWithFallback("availableFormats");
215 //System.out.println("available format from parent locale:"+parentLocale.getName());
216 for (int i=0; i<formatBundle.getSize(); ++i) {
217 String formatKey = formatBundle.get(i).getKey();
218 String formatValue = formatBundle.get(i).getString();
219 //System.out.println(" availableFormat:"+formatValue);
220 if (!result.isAvailableFormatSet(formatKey)) {
221 result.setAvailableFormat(formatKey);
222 result.addPattern(formatValue, false, returnInfo);
223 //System.out.println(" availableFormat:"+formatValue);
227 }catch(Exception e) {
232 // assume it is always big endian (ok for CLDR right now)
233 // some languages didn't add mm:ss or HH:mm, so put in a hack to compute that from the short time.
234 if (shortTimePattern != null) {
235 hackTimes(result, returnInfo, shortTimePattern);
238 result.setDateTimeFormat(Calendar.getDateTimePattern(Calendar.getInstance(uLocale), uLocale, DateFormat.MEDIUM));
240 // decimal point for seconds
241 DecimalFormatSymbols dfs = new DecimalFormatSymbols(uLocale);
242 result.setDecimal(String.valueOf(dfs.getDecimalSeparator()));
243 DTPNG_CACHE.put(localeKey, result);
247 private static void hackTimes(DateTimePatternGenerator result, PatternInfo returnInfo, String hackPattern) {
248 result.fp.set(hackPattern);
249 String mmss = new String();
250 // to get mm:ss, we strip all but mm literal ss
251 boolean gotMm = false;
252 for (int i = 0; i < result.fp.items.size(); ++i) {
253 Object item = result.fp.items.get(i);
254 if (item instanceof String) {
256 mmss += result.fp.quoteLiteral(item.toString());
259 char ch = item.toString().charAt(0);
263 } else if (ch == 's') {
268 result.addPattern(mmss, false, returnInfo);
270 } else if (gotMm || ch == 'z' || ch == 'Z' || ch == 'v' || ch == 'V') {
275 // to get hh:mm, we strip (literal ss) and (literal S)
276 // the easiest way to do this is to mark the stuff we want to nuke, then remove it in a second pass.
277 BitSet variables = new BitSet();
278 BitSet nuke = new BitSet();
279 for (int i = 0; i < result.fp.items.size(); ++i) {
280 Object item = result.fp.items.get(i);
281 if (item instanceof VariableField) {
283 char ch = item.toString().charAt(0);
284 if (ch == 's' || ch == 'S') {
286 for (int j = i-1; j >= 0; ++j) {
287 if (variables.get(j)) break;
293 String hhmm = getFilteredPattern(result.fp, nuke);
294 result.addPattern(hhmm, false, returnInfo);
297 private static String getFilteredPattern(FormatParser fp, BitSet nuke) {
298 String result = new String();
299 for (int i = 0; i < fp.items.size(); ++i) {
300 if (nuke.get(i)) continue;
301 Object item = fp.items.get(i);
302 if (item instanceof String) {
303 result += fp.quoteLiteral(item.toString());
305 result += item.toString();
311 /*private static int getAppendNameNumber(String string) {
312 for (int i = 0; i < CLDR_FIELD_NAME.length; ++i) {
313 if (CLDR_FIELD_NAME[i].equals(string)) return i;
318 private static int getAppendFormatNumber(String string) {
319 for (int i = 0; i < CLDR_FIELD_APPEND.length; ++i) {
320 if (CLDR_FIELD_APPEND[i].equals(string)) return i;
326 private static boolean isCLDRFieldName(int index) {
327 if ((index<0) && (index>=TYPE_LIMIT)) {
330 if (CLDR_FIELD_NAME[index].charAt(0) == '*') {
340 * Return the best pattern matching the input skeleton. It is guaranteed to
341 * have all of the fields in the skeleton.
344 * The skeleton is a pattern containing only the variable fields.
345 * For example, "MMMdd" and "mmhh" are skeletons.
348 public String getBestPattern(String skeleton) {
349 //if (!isComplete) complete();
350 if (chineseMonthHack) {
351 //#if defined(FOUNDATION10) || defined(J2SE13)
352 //## int monidx = skeleton.indexOf("MMM");
353 //## if (monidx >= 0) {
354 //## StringBuffer tmp = new StringBuffer(skeleton.substring(0, monidx));
355 //## tmp.append("MM");
357 //## while (monidx < skeleton.length()) {
358 //## if (skeleton.charAt(monidx) != 'M') {
363 //## tmp.append(skeleton.substring(monidx));
364 //## skeleton = tmp.toString();
367 skeleton = skeleton.replaceAll("MMM+", "MM");
370 // if skeleton contains meta hour field 'j', then
371 // replace it with the default hour format char
372 skeleton = Utility.replaceAll(skeleton, "j", String.valueOf(defaultHourFormatChar));
374 current.set(skeleton, fp);
375 String best = getBestRaw(current, -1, _distanceInfo);
376 if (_distanceInfo.missingFieldMask == 0 && _distanceInfo.extraFieldMask == 0) {
377 // we have a good item. Adjust the field types
378 return adjustFieldTypes(best, current, false);
380 int neededFields = current.getFieldMask();
381 // otherwise break up by date and time.
382 String datePattern = getBestAppending(neededFields & DATE_MASK);
383 String timePattern = getBestAppending(neededFields & TIME_MASK);
385 if (datePattern == null) return timePattern == null ? "" : timePattern;
386 if (timePattern == null) return datePattern;
387 return MessageFormat.format(getDateTimeFormat(), new Object[]{timePattern, datePattern});
391 * PatternInfo supplies output parameters for add(...). It is used because
392 * Java doesn't have real output parameters. It is treated like a struct (eg
393 * Point), so all fields are public.
397 public static final class PatternInfo { // struct for return information
401 public static final int OK = 0;
406 public static final int BASE_CONFLICT = 1;
411 public static final int CONFLICT = 2;
421 public String conflictingPattern;
424 * Simple constructor, since this is treated like a struct.
427 public PatternInfo() {
432 * Adds a pattern to the generator. If the pattern has the same skeleton as
433 * an existing pattern, and the override parameter is set, then the previous
434 * value is overriden. Otherwise, the previous value is retained. In either
435 * case, the conflicting information is returned in PatternInfo.
437 * Note that single-field patterns (like "MMM") are automatically added, and
438 * don't need to be added explicitly!
441 * when existing values are to be overridden use true, otherwise
445 public DateTimePatternGenerator addPattern(String pattern, boolean override, PatternInfo returnInfo) {
447 DateTimeMatcher matcher = new DateTimeMatcher().set(pattern, fp);
448 String basePattern = matcher.getBasePattern();
449 String previousPatternWithSameBase = (String)basePattern_pattern.get(basePattern);
450 if (previousPatternWithSameBase != null) {
451 returnInfo.status = PatternInfo.BASE_CONFLICT;
452 returnInfo.conflictingPattern = previousPatternWithSameBase;
453 if (!override) return this;
455 String previousValue = (String)skeleton2pattern.get(matcher);
456 if (previousValue != null) {
457 returnInfo.status = PatternInfo.CONFLICT;
458 returnInfo.conflictingPattern = previousValue;
459 if (!override) return this;
461 returnInfo.status = PatternInfo.OK;
462 returnInfo.conflictingPattern = "";
463 skeleton2pattern.put(matcher, pattern);
464 basePattern_pattern.put(basePattern, pattern);
469 * Utility to return a unique skeleton from a given pattern. For example,
470 * both "MMM-dd" and "dd/MMM" produce the skeleton "MMMdd".
473 * Input pattern, such as "dd/MMM"
474 * @return skeleton, such as "MMMdd"
477 public String getSkeleton(String pattern) {
478 synchronized (this) { // synchronized since a getter must be thread-safe
479 current.set(pattern, fp);
480 return current.toString();
485 * Utility to return a unique base skeleton from a given pattern. This is
486 * the same as the skeleton, except that differences in length are minimized
487 * so as to only preserve the difference between string and numeric form. So
488 * for example, both "MMM-dd" and "d/MMM" produce the skeleton "MMMd"
489 * (notice the single d).
492 * Input pattern, such as "dd/MMM"
493 * @return skeleton, such as "MMMdd"
496 public String getBaseSkeleton(String pattern) {
497 synchronized (this) { // synchronized since a getter must be thread-safe
498 current.set(pattern, fp);
499 return current.getBasePattern();
504 * Return a list of all the skeletons (in canonical form) from this class,
505 * and the patterns that they map to.
508 * an output Map in which to place the mapping from skeleton to
509 * pattern. If you want to see the internal order being used,
510 * supply a LinkedHashMap. If the input value is null, then a
511 * LinkedHashMap is allocated.
513 * <i>Issue: an alternate API would be to just return a list of
514 * the skeletons, and then have a separate routine to get from
515 * skeleton to pattern.</i>
516 * @return the input Map containing the values.
519 public Map getSkeletons(Map result) {
520 if (result == null) {
521 //#if defined(FOUNDATION10) || defined(J2SE13)
522 //## result = new HashMap();
524 // result = new LinkedHashMap();
527 for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
528 DateTimeMatcher item = (DateTimeMatcher) it.next();
529 String pattern = (String) skeleton2pattern.get(item);
530 if (CANONICAL_SET.contains(pattern)) continue;
531 result.put(item.toString(), pattern);
537 * Return a list of all the base skeletons (in canonical form) from this class
540 public Set getBaseSkeletons(Set result) {
541 if (result == null) result = new HashSet();
542 result.addAll(basePattern_pattern.keySet());
547 * Adjusts the field types (width and subtype) of a pattern to match what is
548 * in a skeleton. That is, if you supply a pattern like "d-M H:m", and a
549 * skeleton of "MMMMddhhmm", then the input pattern is adjusted to be
550 * "dd-MMMM hh:mm". This is used internally to get the best match for the
551 * input skeleton, but can also be used externally.
556 * @return pattern adjusted to match the skeleton fields widths and
560 public String replaceFieldTypes(String pattern, String skeleton) {
561 synchronized (this) { // synchronized since a getter must be thread-safe
562 return adjustFieldTypes(pattern, current.set(skeleton, fp), false);
567 * The date time format is a message format pattern used to compose date and
568 * time patterns. The default value is "{1} {0}", where {1} will be replaced
569 * by the date pattern and {0} will be replaced by the time pattern.
571 * This is used when the input skeleton contains both date and time fields,
572 * but there is not a close match among the added patterns. For example,
573 * suppose that this object was created by adding "dd-MMM" and "hh:mm", and
574 * its datetimeFormat is the default "{1} {0}". Then if the input skeleton
575 * is "MMMdhmm", there is not an exact match, so the input skeleton is
576 * broken up into two components "MMMd" and "hmm". There are close matches
577 * for those two skeletons, so the result is put together with this pattern,
578 * resulting in "d-MMM h:mm".
580 * @param dateTimeFormat
581 * message format pattern, where {1} will be replaced by the date
582 * pattern and {0} will be replaced by the time pattern.
585 public void setDateTimeFormat(String dateTimeFormat) {
587 this.dateTimeFormat = dateTimeFormat;
591 * Getter corresponding to setDateTimeFormat.
596 public String getDateTimeFormat() {
597 return dateTimeFormat;
601 * The decimal value is used in formatting fractions of seconds. If the
602 * skeleton contains fractional seconds, then this is used with the
603 * fractional seconds. For example, suppose that the input pattern is
604 * "hhmmssSSSS", and the best matching pattern internally is "H:mm:ss", and
605 * the decimal string is ",". Then the resulting pattern is modified to be
611 public void setDecimal(String decimal) {
613 this.decimal = decimal;
617 * Getter corresponding to setDecimal.
618 * @return string corresponding to the decimal point
621 public String getDecimal() {
626 * Redundant patterns are those which if removed, make no difference in the
627 * resulting getBestPattern values. This method returns a list of them, to
628 * help check the consistency of the patterns used to build this generator.
631 * stores the redundant patterns that are removed. To get these
632 * in internal order, supply a LinkedHashSet. If null, a
633 * collection is allocated.
634 * @return the collection with added elements.
636 * @deprecated This API is ICU internal only.
638 public Collection getRedundants(Collection output) {
639 synchronized (this) { // synchronized since a getter must be thread-safe
640 if (output == null) {
641 //#if defined(FOUNDATION10) || defined(J2SE13)
642 //## output = new HashSet();
644 // output = new LinkedHashSet();
647 for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
648 DateTimeMatcher cur = (DateTimeMatcher) it.next();
649 String pattern = (String) skeleton2pattern.get(cur);
650 if (CANONICAL_SET.contains(pattern)) continue;
652 String trial = getBestPattern(cur.toString());
653 if (trial.equals(pattern)) {
657 if (false) { // ordered
658 DateTimePatternGenerator results = new DateTimePatternGenerator();
659 PatternInfo pinfo = new PatternInfo();
660 for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
661 DateTimeMatcher cur = (DateTimeMatcher) it.next();
662 String pattern = (String) skeleton2pattern.get(cur);
663 if (CANONICAL_SET.contains(pattern)) continue;
664 //skipMatcher = current;
665 String trial = results.getBestPattern(cur.toString());
666 if (trial.equals(pattern)) {
669 results.addPattern(pattern, false, pinfo);
677 // Field numbers, used for AppendItem functions
682 static final public int ERA = 0;
687 static final public int YEAR = 1;
692 static final public int QUARTER = 2;
697 static final public int MONTH = 3;
702 static final public int WEEK_OF_YEAR = 4;
707 static final public int WEEK_OF_MONTH = 5;
712 static final public int WEEKDAY = 6;
717 static final public int DAY = 7;
722 static final public int DAY_OF_YEAR = 8;
727 static final public int DAY_OF_WEEK_IN_MONTH = 9;
732 static final public int DAYPERIOD = 10;
737 static final public int HOUR = 11;
742 static final public int MINUTE = 12;
747 static final public int SECOND = 13;
752 static final public int FRACTIONAL_SECOND = 14;
757 static final public int ZONE = 15;
762 static final public int TYPE_LIMIT = 16;
765 * An AppendItem format is a pattern used to append a field if there is no
766 * good match. For example, suppose that the input skeleton is "GyyyyMMMd",
767 * and there is no matching pattern internally, but there is a pattern
768 * matching "yyyyMMMd", say "d-MM-yyyy". Then that pattern is used, plus the
769 * G. The way these two are conjoined is by using the AppendItemFormat for G
770 * (era). So if that value is, say "{0}, {1}" then the final resulting
771 * pattern is "d-MM-yyyy, G".
773 * There are actually three available variables: {0} is the pattern so far,
774 * {1} is the element we are adding, and {2} is the name of the element.
776 * This reflects the way that the CLDR data is organized.
781 * pattern, such as "{0}, {1}"
784 public void setAppendItemFormat(int field, String value) {
786 appendItemFormats[field] = value;
790 * Getter corresponding to setAppendItemFormats. Values below 0 or at or
791 * above TYPE_LIMIT are illegal arguments.
794 * @return append pattern for field
797 public String getAppendItemFormat(int field) {
798 return appendItemFormats[field];
802 * Sets the names of fields, eg "era" in English for ERA. These are only
803 * used if the corresponding AppendItemFormat is used, and if it contains a
806 * This reflects the way that the CLDR data is organized.
812 public void setAppendItemName(int field, String value) {
814 appendItemNames[field] = value;
818 * Getter corresponding to setAppendItemNames. Values below 0 or at or above
819 * TYPE_LIMIT are illegal arguments.
822 * @return name for field
825 public String getAppendItemName(int field) {
826 return appendItemNames[field];
830 * Determines whether a skeleton contains a single field
833 * @return true or not
835 * @deprecated This API is ICU internal only.
837 public static boolean isSingleField(String skeleton) {
838 char first = skeleton.charAt(0);
839 for (int i = 1; i < skeleton.length(); ++i) {
840 if (skeleton.charAt(i) != first) return false;
846 * Add key to HashSet cldrAvailableFormatKeys.
848 * @param key of the availableFormats in CLDR
851 private void setAvailableFormat(String key) {
853 cldrAvailableFormatKeys.add(key);
857 * This function checks the corresponding slot of CLDR_AVAIL_FORMAT_KEY[]
858 * has been added to DateTimePatternGenerator.
859 * The function is to avoid the duplicate availableFomats added to
860 * the pattern map from parent locales.
862 * @param key of the availableFormatMask in CLDR
863 * @return TRUE if the corresponding slot of CLDR_AVAIL_FORMAT_KEY[]
864 * has been added to DateTimePatternGenerator.
867 private boolean isAvailableFormatSet(String key) {
868 return cldrAvailableFormatKeys.contains(key);
872 * Boilerplate for Freezable
875 public boolean isFrozen() {
880 * Boilerplate for Freezable
883 public Object freeze() {
889 * Boilerplate for Freezable
892 public Object cloneAsThawed() {
893 DateTimePatternGenerator result = (DateTimePatternGenerator) (this.clone());
902 public Object clone() {
904 DateTimePatternGenerator result = (DateTimePatternGenerator) (super.clone());
905 result.skeleton2pattern = (TreeMap) skeleton2pattern.clone();
906 result.basePattern_pattern = (TreeMap) basePattern_pattern.clone();
907 result.appendItemFormats = (String[]) appendItemFormats.clone();
908 result.appendItemNames = (String[]) appendItemNames.clone();
909 result.current = new DateTimeMatcher();
910 result.fp = new FormatParser();
911 result._distanceInfo = new DistanceInfo();
913 result.frozen = false;
915 } catch (CloneNotSupportedException e) {
916 throw new IllegalArgumentException("Internal Error");
921 * Utility class for FormatParser. Immutable class that is only used to mark
922 * the difference between a variable field and a literal string. Each
923 * variable field must consist of 1 to n variable characters, representing
924 * date format fields. For example, "VVVV" is valid while "V4" is not, nor
928 * @deprecated This API is ICU internal only.
930 public static class VariableField {
931 private final String string;
932 private final int canonicalIndex;
935 * Create a variable field: equivalent to VariableField(string,false);
938 * @deprecated This API is ICU internal only.
940 public VariableField(String string) {
944 * Create a variable field
947 * @throws IllegalArgumentException if the variable field is not valid.
949 * @deprecated This API is ICU internal only.
951 public VariableField(String string, boolean strict) {
952 canonicalIndex = DateTimePatternGenerator.getCanonicalIndex(string, strict);
953 if (canonicalIndex < 0) {
954 throw new IllegalArgumentException("Illegal datetime field:\t"
957 this.string = string;
961 * Get the main type of this variable. These types are ERA, QUARTER,
962 * MONTH, DAY, WEEK_OF_YEAR, WEEK_OF_MONTH, WEEKDAY, DAY, DAYPERIOD
963 * (am/pm), HOUR, MINUTE, SECOND,FRACTIONAL_SECOND, ZONE.
966 * @deprecated This API is ICU internal only.
968 public int getType() {
969 return types[canonicalIndex][1];
975 private int getCanonicalIndex() {
976 return canonicalIndex;
980 * Get the string represented by this variable.
982 * @deprecated This API is ICU internal only.
984 public String toString() {
990 * This class provides mechanisms for parsing a SimpleDateFormat pattern
991 * or generating a new pattern, while handling the quoting. It represents
992 * the result of the parse as a list of items, where each item is either a
993 * literal string or a variable field. When parsing It can be used to find
994 * out which variable fields are in a date format, and in what order, such
995 * as for presentation in a UI as separate text entry fields. It can also be
996 * used to construct new SimpleDateFormats.
999 public boolean containsZone(String pattern) {
1000 for (Iterator it = formatParser.set(pattern).getItems().iterator(); it.hasNext();) {
1001 Object item = it.next();
1002 if (item instanceof VariableField) {
1003 VariableField variableField = (VariableField) item;
1004 if (variableField.getType() == DateTimePatternGenerator.ZONE) {
1013 * @deprecated This API is ICU internal only.
1015 static public class FormatParser {
1016 private transient PatternTokenizer tokenizer = new PatternTokenizer()
1017 .setSyntaxCharacters(new UnicodeSet("[a-zA-Z]"))
1018 .setExtraQuotingCharacters(new UnicodeSet("[[[:script=Latn:][:script=Cyrl:]]&[[:L:][:M:]]]"))
1019 //.setEscapeCharacters(new UnicodeSet("[^\\u0020-\\u007E]")) // WARNING: DateFormat doesn't accept \\uXXXX
1020 .setUsingQuote(true);
1021 private List items = new ArrayList();
1024 * Construct an empty date format parser, to which strings and variables can be added with set(...).
1026 * @deprecated This API is ICU internal only.
1028 public FormatParser() {
1032 * Parses the string into a list of items.
1034 * @return this, for chaining
1036 * @deprecated This API is ICU internal only.
1038 final public FormatParser set(String string) {
1039 return set(string, false);
1043 * Parses the string into a list of items, taking into account all of the quoting that may be going on.
1045 * @param strict If true, then only allows exactly those lengths specified by CLDR for variables. For example, "hh:mm aa" would throw an exception.
1046 * @return this, for chaining
1048 * @deprecated This API is ICU internal only.
1050 public FormatParser set(String string, boolean strict) {
1052 if (string.length() == 0) return this;
1053 tokenizer.setPattern(string);
1054 StringBuffer buffer = new StringBuffer();
1055 StringBuffer variable = new StringBuffer();
1057 buffer.setLength(0);
1058 int status = tokenizer.next(buffer);
1059 if (status == PatternTokenizer.DONE) break;
1060 if (status == PatternTokenizer.SYNTAX) {
1061 if (variable.length() != 0 && buffer.charAt(0) != variable.charAt(0)) {
1062 addVariable(variable, false);
1064 variable.append(buffer);
1066 addVariable(variable, false);
1067 items.add(buffer.toString());
1070 addVariable(variable, false);
1074 private void addVariable(StringBuffer variable, boolean strict) {
1075 if (variable.length() != 0) {
1076 items.add(new VariableField(variable.toString(), strict));
1077 variable.setLength(0);
1081 // /** Private method. Return a collection of fields. These will be a mixture of literal Strings and VariableFields. Any "a" variable field is removed.
1082 // * @param output List to append the items to. If null, is allocated as an ArrayList.
1085 // private List getVariableFields(List output) {
1086 // if (output == null) output = new ArrayList();
1088 // for (Iterator it = items.iterator(); it.hasNext();) {
1089 // Object item = it.next();
1090 // if (item instanceof VariableField) {
1091 // String s = item.toString();
1092 // switch(s.charAt(0)) {
1093 // //case 'Q': continue main; // HACK
1094 // case 'a': continue main; // remove
1096 // output.add(item);
1099 // //System.out.println(output);
1104 // * Produce a string which concatenates all the variables. That is, it is the logically the same as the input with all literals removed.
1105 // * @return a string which is a concatenation of all the variable fields
1107 // * @deprecated This API is ICU internal only.
1109 // public String getVariableFieldString() {
1110 // List list = getVariableFields(null);
1111 // StringBuffer result = new StringBuffer();
1112 // for (Iterator it = list.iterator(); it.hasNext();) {
1113 // String item = it.next().toString();
1114 // result.append(item);
1116 // return result.toString();
1120 * 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:
1126 * The list is modifiable, so you can add any strings or variables to it, or remove any items.
1127 * @return modifiable list of items.
1129 * @deprecated This API is ICU internal only.
1131 public List getItems() {
1135 /** 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().
1136 * @return printable output string
1138 * @deprecated This API is ICU internal only.
1140 public String toString() {
1141 return toString(0, items.size());
1145 * 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().
1146 * @param start item to start from
1147 * @param limit last item +1
1148 * @return printable output string
1150 * @deprecated This API is ICU internal only.
1152 public String toString(int start, int limit) {
1153 StringBuffer result = new StringBuffer();
1154 for (int i = start; i < limit; ++i) {
1155 Object item = items.get(i);
1156 if (item instanceof String) {
1157 String itemString = (String) item;
1158 result.append(tokenizer.quoteLiteral(itemString));
1160 result.append(items.get(i).toString());
1163 return result.toString();
1167 * 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.
1168 * @return true or false
1170 * @deprecated This API is ICU internal only.
1172 public boolean hasDateAndTimeFields() {
1174 for (Iterator it = items.iterator(); it.hasNext();) {
1175 Object item = it.next();
1176 if (item instanceof VariableField) {
1177 int type = ((VariableField)item).getType();
1178 foundMask |= 1 << type;
1181 boolean isDate = (foundMask & DATE_MASK) != 0;
1182 boolean isTime = (foundMask & TIME_MASK) != 0;
1183 return isDate && isTime;
1187 // * Internal routine
1192 // * @deprecated This API is ICU internal only.
1194 // public List getAutoPatterns(String value, List result) {
1195 // if (result == null) result = new ArrayList();
1196 // int fieldCount = 0;
1197 // int minField = Integer.MAX_VALUE;
1198 // int maxField = Integer.MIN_VALUE;
1199 // for (Iterator it = items.iterator(); it.hasNext();) {
1200 // Object item = it.next();
1201 // if (item instanceof VariableField) {
1203 // int type = ((VariableField)item).getType();
1204 // if (minField > type) minField = type;
1205 // if (maxField < type) maxField = type;
1206 // if (type == ZONE || type == DAYPERIOD || type == WEEKDAY) return result; // skip anything with zones
1208 // } catch (Exception e) {
1209 // return result; // if there are any funny fields, return
1213 // if (fieldCount < 3) return result; // skip
1214 // // trim from start
1215 // // trim first field IF there are no letters around it
1216 // // and it is either the min or the max field
1217 // // first field is either 0 or 1
1218 // for (int i = 0; i < items.size(); ++i) {
1219 // Object item = items.get(i);
1220 // if (item instanceof VariableField) {
1221 // int type = ((VariableField)item).getType();
1222 // if (type != minField && type != maxField) break;
1225 // Object previousItem = items.get(0);
1226 // if (alpha.containsSome(previousItem.toString())) break;
1229 // if (start < items.size()) {
1230 // Object nextItem = items.get(start);
1231 // if (nextItem instanceof String) {
1232 // if (alpha.containsSome(nextItem.toString())) break;
1233 // start++; // otherwise skip over string
1236 // result.add(toString(start, items.size()));
1240 // // now trim from end
1241 // for (int i = items.size()-1; i >= 0; --i) {
1242 // Object item = items.get(i);
1243 // if (item instanceof VariableField) {
1244 // int type = ((VariableField)item).getType();
1245 // if (type != minField && type != maxField) break;
1246 // if (i < items.size() - 1) {
1247 // Object previousItem = items.get(items.size() - 1);
1248 // if (alpha.containsSome(previousItem.toString())) break;
1252 // Object nextItem = items.get(end);
1253 // if (nextItem instanceof String) {
1254 // if (alpha.containsSome(nextItem.toString())) break;
1255 // end--; // otherwise skip over string
1258 // result.add(toString(0, end+1));
1266 // private static UnicodeSet alpha = new UnicodeSet("[:alphabetic:]");
1268 // private int getType(Object item) {
1269 // String s = item.toString();
1270 // int canonicalIndex = getCanonicalIndex(s);
1271 // if (canonicalIndex < 0) {
1272 // throw new IllegalArgumentException("Illegal field:\t"
1275 // int type = types[canonicalIndex][1];
1280 * 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' ".
1282 * @return string with quoted literals
1284 * @deprecated This API is ICU internal only.
1286 public Object quoteLiteral(String string) {
1287 return tokenizer.quoteLiteral(string);
1291 // ========= PRIVATES ============
1293 private TreeMap skeleton2pattern = new TreeMap(); // items are in priority order
1294 private TreeMap basePattern_pattern = new TreeMap(); // items are in priority order
1295 private String decimal = "?";
1296 private String dateTimeFormat = "{1} {0}";
1297 private String[] appendItemFormats = new String[TYPE_LIMIT];
1298 private String[] appendItemNames = new String[TYPE_LIMIT];
1300 for (int i = 0; i < TYPE_LIMIT; ++i) {
1301 appendItemFormats[i] = "{0} \u251C{2}: {1}\u2524";
1302 appendItemNames[i] = "F" + i;
1305 private char defaultHourFormatChar = 'H';
1306 private boolean chineseMonthHack = false;
1307 //private boolean isComplete = false;
1308 private boolean frozen = false;
1310 private transient DateTimeMatcher current = new DateTimeMatcher();
1311 private transient FormatParser fp = new FormatParser();
1312 private transient DistanceInfo _distanceInfo = new DistanceInfo();
1313 private transient DateTimeMatcher skipMatcher = null; // only used temporarily, for internal purposes
1316 private static final int FRACTIONAL_MASK = 1<<FRACTIONAL_SECOND;
1317 private static final int SECOND_AND_FRACTIONAL_MASK = (1<<SECOND) | (1<<FRACTIONAL_SECOND);
1319 // Cache for DateTimePatternGenerator
1320 private static ICUCache DTPNG_CACHE = new SimpleCache();
1322 private void checkFrozen() {
1324 throw new UnsupportedOperationException("Attempt to modify frozen object");
1329 * 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.
1330 * If we fail to find a complete skeleton, we compose in a loop until we have all the fields.
1332 private String getBestAppending(int missingFields) {
1333 String resultPattern = null;
1334 if (missingFields != 0) {
1335 resultPattern = getBestRaw(current, missingFields, _distanceInfo);
1336 resultPattern = adjustFieldTypes(resultPattern, current, false);
1338 while (_distanceInfo.missingFieldMask != 0) { // precondition: EVERY single field must work!
1340 // special hack for SSS. If we are missing SSS, and we had ss but found it, replace the s field according to the
1342 if ((_distanceInfo.missingFieldMask & SECOND_AND_FRACTIONAL_MASK) == FRACTIONAL_MASK
1343 && (missingFields & SECOND_AND_FRACTIONAL_MASK) == SECOND_AND_FRACTIONAL_MASK) {
1344 resultPattern = adjustFieldTypes(resultPattern, current, true);
1345 _distanceInfo.missingFieldMask &= ~FRACTIONAL_MASK; // remove bit
1349 int startingMask = _distanceInfo.missingFieldMask;
1350 String temp = getBestRaw(current, _distanceInfo.missingFieldMask, _distanceInfo);
1351 temp = adjustFieldTypes(temp, current, false);
1352 int foundMask = startingMask & ~_distanceInfo.missingFieldMask;
1353 int topField = getTopBitNumber(foundMask);
1354 resultPattern = MessageFormat.format(getAppendFormat(topField), new Object[]{resultPattern, temp, getAppendName(topField)});
1357 return resultPattern;
1360 private String getAppendName(int foundMask) {
1361 return "'" + appendItemNames[foundMask] + "'";
1363 private String getAppendFormat(int foundMask) {
1364 return appendItemFormats[foundMask];
1368 // * @param current2
1371 // private String adjustSeconds(DateTimeMatcher current2) {
1372 // // TODO Auto-generated method stub
1380 private int getTopBitNumber(int foundMask) {
1382 while (foundMask != 0) {
1392 private void complete() {
1393 PatternInfo patternInfo = new PatternInfo();
1394 // make sure that every valid field occurs once, with a "default" length
1395 for (int i = 0; i < CANONICAL_ITEMS.length; ++i) {
1396 //char c = (char)types[i][0];
1397 addPattern(String.valueOf(CANONICAL_ITEMS[i]), false, patternInfo);
1399 //isComplete = true;
1408 private String getBestRaw(DateTimeMatcher source, int includeMask, DistanceInfo missingFields) {
1409 // if (SHOW_DISTANCE) System.out.println("Searching for: " + source.pattern
1410 // + ", mask: " + showMask(includeMask));
1411 int bestDistance = Integer.MAX_VALUE;
1412 String bestPattern = "";
1413 DistanceInfo tempInfo = new DistanceInfo();
1414 for (Iterator it = skeleton2pattern.keySet().iterator(); it.hasNext();) {
1415 DateTimeMatcher trial = (DateTimeMatcher) it.next();
1416 if (trial.equals(skipMatcher)) continue;
1417 int distance = source.getDistance(trial, includeMask, tempInfo);
1418 // if (SHOW_DISTANCE) System.out.println("\tDistance: " + trial.pattern + ":\t"
1419 // + distance + ",\tmissing fields: " + tempInfo);
1420 if (distance < bestDistance) {
1421 bestDistance = distance;
1422 bestPattern = (String) skeleton2pattern.get(trial);
1423 missingFields.setTo(tempInfo);
1424 if (distance == 0) break;
1431 * @param fixFractionalSeconds TODO
1434 private String adjustFieldTypes(String pattern, DateTimeMatcher inputRequest, boolean fixFractionalSeconds) {
1436 StringBuffer newPattern = new StringBuffer();
1437 for (Iterator it = fp.getItems().iterator(); it.hasNext();) {
1438 Object item = it.next();
1439 if (item instanceof String) {
1440 newPattern.append(fp.quoteLiteral((String)item));
1442 final VariableField variableField = (VariableField) item;
1443 String field = variableField.toString();
1444 // int canonicalIndex = getCanonicalIndex(field, true);
1445 // if (canonicalIndex < 0) {
1446 // continue; // don't adjust
1448 // int type = types[canonicalIndex][1];
1449 int type = variableField.getType();
1451 if (fixFractionalSeconds && type == SECOND) {
1452 String newField = inputRequest.original[FRACTIONAL_SECOND];
1453 field = field + decimal + newField;
1454 } else if (inputRequest.type[type] != 0) {
1455 String newField = inputRequest.original[type];
1456 // normally we just replace the field. However HOUR is special; we only change the length
1459 } else if (field.length() != newField.length()){
1460 char c = field.charAt(0);
1462 for (int i = newField.length(); i > 0; --i) field += c;
1465 newPattern.append(field);
1468 //if (SHOW_DISTANCE) System.out.println("\tRaw: " + pattern);
1469 return newPattern.toString();
1472 // public static String repeat(String s, int count) {
1473 // StringBuffer result = new StringBuffer();
1474 // for (int i = 0; i < count; ++i) {
1475 // result.append(s);
1477 // return result.toString();
1483 * @return field value
1485 * @deprecated This API is ICU internal only.
1487 public String getFields(String pattern) {
1489 StringBuffer newPattern = new StringBuffer();
1490 for (Iterator it = fp.getItems().iterator(); it.hasNext();) {
1491 Object item = it.next();
1492 if (item instanceof String) {
1493 newPattern.append(fp.quoteLiteral((String)item));
1495 newPattern.append("{" + getName(item.toString()) + "}");
1498 return newPattern.toString();
1501 private static String showMask(int mask) {
1503 for (int i = 0; i < TYPE_LIMIT; ++i) {
1504 if ((mask & (1<<i)) == 0) continue;
1505 if (result.length() != 0) result += " | ";
1506 result += FIELD_NAME[i] + " ";
1511 static private String[] CLDR_FIELD_APPEND = {
1512 "Era", "Year", "Quarter", "Month", "Week", "*", "Day-Of-Week",
1513 "Day", "*", "*", "*",
1514 "Hour", "Minute", "Second", "*", "Timezone"
1517 static private String[] CLDR_FIELD_NAME = {
1518 "era", "year", "*", "month", "week", "*", "weekday",
1519 "day", "*", "*", "dayperiod",
1520 "hour", "minute", "second", "*", "zone"
1523 static private String[] FIELD_NAME = {
1524 "Era", "Year", "Quarter", "Month", "Week_in_Year", "Week_in_Month", "Weekday",
1525 "Day", "Day_Of_Year", "Day_of_Week_in_Month", "Dayperiod",
1526 "Hour", "Minute", "Second", "Fractional_Second", "Zone"
1530 static private String[] CANONICAL_ITEMS = {
1531 "G", "y", "Q", "M", "w", "W", "e",
1533 "H", "m", "s", "S", "v"
1536 static private Set CANONICAL_SET = new HashSet(Arrays.asList(CANONICAL_ITEMS));
1537 private Set cldrAvailableFormatKeys = new HashSet(20);
1539 static final private int
1540 DATE_MASK = (1<<DAYPERIOD) - 1,
1541 TIME_MASK = (1<<TYPE_LIMIT) - 1 - DATE_MASK;
1543 static final private int // numbers are chosen to express 'distance'
1550 EXTRA_FIELD = 0x10000,
1551 MISSING_FIELD = 0x1000;
1554 static private String getName(String s) {
1555 int i = getCanonicalIndex(s, true);
1556 String name = FIELD_NAME[types[i][1]];
1557 int subtype = types[i][2];
1558 boolean string = subtype < 0;
1559 if (string) subtype = -subtype;
1560 if (subtype < 0) name += ":S";
1566 * Get the canonical index, or return -1 if illegal.
1568 * @param strict TODO
1571 static private int getCanonicalIndex(String s, boolean strict) {
1572 int len = s.length();
1576 int ch = s.charAt(0);
1577 // verify that all are the same character
1578 for (int i = 1; i < len; ++i) {
1579 if (s.charAt(i) != ch) {
1584 for (int i = 0; i < types.length; ++i) {
1585 int[] row = types[i];
1586 if (row[0] != ch) continue;
1588 if (row[3] > len) continue;
1589 if (row[row.length-1] < len) continue;
1592 return strict ? -1 : bestRow;
1595 static private int[][] types = {
1596 // the order here makes a difference only when searching for single field.
1598 // pattern character, main type, weight, min length, weight
1599 {'G', ERA, SHORT, 1, 3},
1600 {'G', ERA, LONG, 4},
1602 {'y', YEAR, NUMERIC, 1, 20},
1603 {'Y', YEAR, NUMERIC + DELTA, 1, 20},
1604 {'u', YEAR, NUMERIC + 2*DELTA, 1, 20},
1606 {'Q', QUARTER, NUMERIC, 1, 2},
1607 {'Q', QUARTER, SHORT, 3},
1608 {'Q', QUARTER, LONG, 4},
1610 {'q', QUARTER, NUMERIC + DELTA, 1, 2},
1611 {'q', QUARTER, SHORT + DELTA, 3},
1612 {'q', QUARTER, LONG + DELTA, 4},
1614 {'M', MONTH, NUMERIC, 1, 2},
1615 {'M', MONTH, SHORT, 3},
1616 {'M', MONTH, LONG, 4},
1617 {'M', MONTH, NARROW, 5},
1618 {'L', MONTH, NUMERIC + DELTA, 1, 2},
1619 {'L', MONTH, SHORT - DELTA, 3},
1620 {'L', MONTH, LONG - DELTA, 4},
1621 {'L', MONTH, NARROW - DELTA, 5},
1623 {'w', WEEK_OF_YEAR, NUMERIC, 1, 2},
1624 {'W', WEEK_OF_MONTH, NUMERIC + DELTA, 1},
1626 {'e', WEEKDAY, NUMERIC + DELTA, 1, 2},
1627 {'e', WEEKDAY, SHORT - DELTA, 3},
1628 {'e', WEEKDAY, LONG - DELTA, 4},
1629 {'e', WEEKDAY, NARROW - DELTA, 5},
1630 {'E', WEEKDAY, SHORT, 1, 3},
1631 {'E', WEEKDAY, LONG, 4},
1632 {'E', WEEKDAY, NARROW, 5},
1633 {'c', WEEKDAY, NUMERIC + 2*DELTA, 1, 2},
1634 {'c', WEEKDAY, SHORT - 2*DELTA, 3},
1635 {'c', WEEKDAY, LONG - 2*DELTA, 4},
1636 {'c', WEEKDAY, NARROW - 2*DELTA, 5},
1638 {'d', DAY, NUMERIC, 1, 2},
1639 {'D', DAY_OF_YEAR, NUMERIC + DELTA, 1, 3},
1640 {'F', DAY_OF_WEEK_IN_MONTH, NUMERIC + 2*DELTA, 1},
1641 {'g', DAY, NUMERIC + 3*DELTA, 1, 20}, // really internal use, so we don't care
1643 {'a', DAYPERIOD, SHORT, 1},
1645 {'H', HOUR, NUMERIC + 10*DELTA, 1, 2}, // 24 hour
1646 {'k', HOUR, NUMERIC + 11*DELTA, 1, 2},
1647 {'h', HOUR, NUMERIC, 1, 2}, // 12 hour
1648 {'K', HOUR, NUMERIC + DELTA, 1, 2},
1650 {'m', MINUTE, NUMERIC, 1, 2},
1652 {'s', SECOND, NUMERIC, 1, 2},
1653 {'S', FRACTIONAL_SECOND, NUMERIC + DELTA, 1, 1000},
1654 {'A', SECOND, NUMERIC + 2*DELTA, 1, 1000},
1656 {'v', ZONE, SHORT - 2*DELTA, 1},
1657 {'v', ZONE, LONG - 2*DELTA, 4},
1658 {'z', ZONE, SHORT, 1, 3},
1659 {'z', ZONE, LONG, 4},
1660 {'Z', ZONE, SHORT - DELTA, 1, 3},
1661 {'Z', ZONE, LONG - DELTA, 4},
1662 {'V', ZONE, SHORT - DELTA, 1, 3},
1663 {'V', ZONE, LONG - DELTA, 4},
1666 private static class DateTimeMatcher implements Comparable {
1667 //private String pattern = null;
1668 private int[] type = new int[TYPE_LIMIT];
1669 private String[] original = new String[TYPE_LIMIT];
1670 private String[] baseOriginal = new String[TYPE_LIMIT];
1672 // just for testing; fix to make multi-threaded later
1673 // private static FormatParser fp = new FormatParser();
1675 public String toString() {
1676 StringBuffer result = new StringBuffer();
1677 for (int i = 0; i < TYPE_LIMIT; ++i) {
1678 if (original[i].length() != 0) result.append(original[i]);
1680 return result.toString();
1683 String getBasePattern() {
1684 StringBuffer result = new StringBuffer();
1685 for (int i = 0; i < TYPE_LIMIT; ++i) {
1686 if (baseOriginal[i].length() != 0) result.append(baseOriginal[i]);
1688 return result.toString();
1691 DateTimeMatcher set(String pattern, FormatParser fp) {
1692 for (int i = 0; i < TYPE_LIMIT; ++i) {
1695 baseOriginal[i] = "";
1698 for (Iterator it = fp.getItems().iterator(); it.hasNext();) {
1699 Object obj = it.next();
1700 if (!(obj instanceof VariableField)) {
1703 VariableField item = (VariableField)obj;
1704 String field = item.toString();
1705 if (field.charAt(0) == 'a') continue; // skip day period, special case
1706 int canonicalIndex = item.getCanonicalIndex();
1707 // if (canonicalIndex < 0) {
1708 // throw new IllegalArgumentException("Illegal field:\t"
1709 // + field + "\t in " + pattern);
1711 int[] row = types[canonicalIndex];
1712 int typeValue = row[1];
1713 if (original[typeValue].length() != 0) {
1714 throw new IllegalArgumentException("Conflicting fields:\t"
1715 + original[typeValue] + ", " + field + "\t in " + pattern);
1717 original[typeValue] = field;
1718 char repeatChar = (char)row[0];
1719 int repeatCount = row[3];
1720 if (repeatCount > 3) repeatCount = 3; // hack to discard differences
1721 if ("GEzvQ".indexOf(repeatChar) >= 0) repeatCount = 1;
1722 baseOriginal[typeValue] = Utility.repeat(String.valueOf(repeatChar),repeatCount);
1723 int subTypeValue = row[2];
1724 if (subTypeValue > 0) subTypeValue += field.length();
1725 type[typeValue] = subTypeValue;
1733 int getFieldMask() {
1735 for (int i = 0; i < type.length; ++i) {
1736 if (type[i] != 0) result |= (1<<i);
1744 void extractFrom(DateTimeMatcher source, int fieldMask) {
1745 for (int i = 0; i < type.length; ++i) {
1746 if ((fieldMask & (1<<i)) != 0) {
1747 type[i] = source.type[i];
1748 original[i] = source.original[i];
1756 int getDistance(DateTimeMatcher other, int includeMask, DistanceInfo distanceInfo) {
1758 distanceInfo.clear();
1759 for (int i = 0; i < type.length; ++i) {
1760 int myType = (includeMask & (1<<i)) == 0 ? 0 : type[i];
1761 int otherType = other.type[i];
1762 if (myType == otherType) continue; // identical (maybe both zero) add 0
1763 if (myType == 0) { // and other is not
1764 result += EXTRA_FIELD;
1765 distanceInfo.addExtra(i);
1766 } else if (otherType == 0) { // and mine is not
1767 result += MISSING_FIELD;
1768 distanceInfo.addMissing(i);
1770 result += Math.abs(myType - otherType); // square of mismatch
1776 public int compareTo(Object o) {
1777 DateTimeMatcher that = (DateTimeMatcher) o;
1778 for (int i = 0; i < original.length; ++i) {
1779 int comp = original[i].compareTo(that.original[i]);
1780 if (comp != 0) return -comp;
1785 public boolean equals(Object other) {
1786 if (other == null) return false;
1787 DateTimeMatcher that = (DateTimeMatcher) other;
1788 for (int i = 0; i < original.length; ++i) {
1789 if (!original[i].equals(that.original[i])) return false;
1793 public int hashCode() {
1795 for (int i = 0; i < original.length; ++i) {
1796 result ^= original[i].hashCode();
1802 private static class DistanceInfo {
1803 int missingFieldMask;
1806 missingFieldMask = extraFieldMask = 0;
1811 void setTo(DistanceInfo other) {
1812 missingFieldMask = other.missingFieldMask;
1813 extraFieldMask = other.extraFieldMask;
1815 void addMissing(int field) {
1816 missingFieldMask |= (1<<field);
1818 void addExtra(int field) {
1819 extraFieldMask |= (1<<field);
1821 public String toString() {
1822 return "missingFieldMask: " + DateTimePatternGenerator.showMask(missingFieldMask)
1823 + ", extraFieldMask: " + DateTimePatternGenerator.showMask(extraFieldMask);