2 *******************************************************************************
3 * Copyright (C) 2008-2013, International Business Machines Corporation and *
4 * others. All Rights Reserved. *
5 *******************************************************************************
8 package com.ibm.icu.text;
10 import java.io.Serializable;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.LinkedHashMap;
14 import java.util.LinkedHashSet;
16 import java.util.Map.Entry;
17 import java.util.MissingResourceException;
20 import com.ibm.icu.impl.ICUCache;
21 import com.ibm.icu.impl.ICUResourceBundle;
22 import com.ibm.icu.impl.SimpleCache;
23 import com.ibm.icu.impl.Utility;
24 import com.ibm.icu.util.Calendar;
25 import com.ibm.icu.util.Freezable;
26 import com.ibm.icu.util.ULocale;
27 import com.ibm.icu.util.UResourceBundle;
30 * DateIntervalInfo is a public class for encapsulating localizable
31 * date time interval patterns. It is used by DateIntervalFormat.
34 * For most users, ordinary use of DateIntervalFormat does not need to create
35 * DateIntervalInfo object directly.
36 * DateIntervalFormat will take care of it when creating a date interval
37 * formatter when user pass in skeleton and locale.
40 * For power users, who want to create their own date interval patterns,
41 * or want to re-set date interval patterns, they could do so by
42 * directly creating DateIntervalInfo and manupulating it.
45 * Logically, the interval patterns are mappings
46 * from (skeleton, the_largest_different_calendar_field)
47 * to (date_interval_pattern).
53 * only keeps the field pattern letter and ignores all other parts
54 * in a pattern, such as space, punctuations, and string literals.
56 * hides the order of fields.
58 * might hide a field's pattern letter length.
60 * For those non-digit calendar fields, the pattern letter length is
61 * important, such as MMM, MMMM, and MMMMM; EEE and EEEE,
62 * and the field's pattern letter length is honored.
64 * For the digit calendar fields, such as M or MM, d or dd, yy or yyyy,
65 * the field pattern length is ignored and the best match, which is defined
66 * in date time patterns, will be returned without honor the field pattern
67 * letter length in skeleton.
71 * The calendar fields we support for interval formatting are:
72 * year, month, date, day-of-week, am-pm, hour, hour-of-day, and minute.
73 * Those calendar fields can be defined in the following order:
74 * year > month > date > am-pm > hour > minute
76 * The largest different calendar fields between 2 calendars is the
77 * first different calendar field in above order.
79 * For example: the largest different calendar fields between "Jan 10, 2007"
80 * and "Feb 20, 2008" is year.
83 * There is a set of pre-defined static skeleton strings.
84 * There are pre-defined interval patterns for those pre-defined skeletons
85 * in locales' resource files.
86 * For example, for a skeleton YEAR_ABBR_MONTH_DAY, which is "yMMMd",
87 * in en_US, if the largest different calendar field between date1 and date2
88 * is "year", the date interval pattern is "MMM d, yyyy - MMM d, yyyy",
89 * such as "Jan 10, 2007 - Jan 10, 2008".
90 * If the largest different calendar field between date1 and date2 is "month",
91 * the date interval pattern is "MMM d - MMM d, yyyy",
92 * such as "Jan 10 - Feb 10, 2007".
93 * If the largest different calendar field between date1 and date2 is "day",
94 * the date interval pattern is ""MMM d-d, yyyy", such as "Jan 10-20, 2007".
96 * For date skeleton, the interval patterns when year, or month, or date is
97 * different are defined in resource files.
98 * For time skeleton, the interval patterns when am/pm, or hour, or minute is
99 * different are defined in resource files.
103 * There are 2 dates in interval pattern. For most locales, the first date
104 * in an interval pattern is the earlier date. There might be a locale in which
105 * the first date in an interval pattern is the later date.
106 * We use fallback format for the default order for the locale.
107 * For example, if the fallback format is "{0} - {1}", it means
108 * the first date in the interval pattern for this locale is earlier date.
109 * If the fallback format is "{1} - {0}", it means the first date is the
111 * For a particular interval pattern, the default order can be overriden
112 * by prefixing "latestFirst:" or "earliestFirst:" to the interval pattern.
113 * For example, if the fallback format is "{0}-{1}",
114 * but for skeleton "yMMMd", the interval pattern when day is different is
115 * "latestFirst:d-d MMM yy", it means by default, the first date in interval
116 * pattern is the earlier date. But for skeleton "yMMMd", when day is different,
117 * the first date in "d-d MMM yy" is the later date.
120 * The recommended way to create a DateIntervalFormat object is to pass in
122 * By using a Locale parameter, the DateIntervalFormat object is
123 * initialized with the pre-defined interval patterns for a given or
126 * Users can also create DateIntervalFormat object
127 * by supplying their own interval patterns.
128 * It provides flexibility for power usage.
131 * After a DateIntervalInfo object is created, clients may modify
132 * the interval patterns using setIntervalPattern function as so desired.
133 * Currently, users can only set interval patterns when the following
134 * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH,
135 * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, and MINUTE.
136 * Interval patterns when other calendar fields are different is not supported.
138 * DateIntervalInfo objects are cloneable.
139 * When clients obtain a DateIntervalInfo object,
140 * they can feel free to modify it as necessary.
142 * DateIntervalInfo are not expected to be subclassed.
143 * Data for a calendar is loaded out of resource bundles.
144 * Through ICU 4.4, date interval patterns are only supported in the Gregoria
145 * calendar; non-Gregorian calendars are supported from ICU 4.4.1.
150 public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, Serializable {
151 /* Save the interval pattern information.
152 * Interval pattern consists of 2 single date patterns and the separator.
153 * For example, interval pattern "MMM d - MMM d, yyyy" consists
154 * a single date pattern "MMM d", another single date pattern "MMM d, yyyy",
155 * and a separator "-".
156 * Also, the first date appears in an interval pattern could be
157 * the earlier date or the later date.
158 * And such information is saved in the interval pattern as well.
160 static final int currentSerialVersion = 1;
163 * PatternInfo class saves the first and second part of interval pattern,
164 * and whether the interval pattern is earlier date first.
167 public static final class PatternInfo implements Cloneable, Serializable {
168 static final int currentSerialVersion = 1;
169 private static final long serialVersionUID = 1;
170 private final String fIntervalPatternFirstPart;
171 private final String fIntervalPatternSecondPart;
173 * Whether the first date in interval pattern is later date or not.
174 * Fallback format set the default ordering.
175 * And for a particular interval pattern, the order can be
176 * overriden by prefixing the interval pattern with "latestFirst:" or
178 * For example, given 2 date, Jan 10, 2007 to Feb 10, 2007.
179 * if the fallback format is "{0} - {1}",
180 * and the pattern is "d MMM - d MMM yyyy", the interval format is
181 * "10 Jan - 10 Feb, 2007".
182 * If the pattern is "latestFirst:d MMM - d MMM yyyy",
183 * the interval format is "10 Feb - 10 Jan, 2007"
185 private final boolean fFirstDateInPtnIsLaterDate;
191 public PatternInfo(String firstPart, String secondPart,
192 boolean firstDateInPtnIsLaterDate) {
193 fIntervalPatternFirstPart = firstPart;
194 fIntervalPatternSecondPart = secondPart;
195 fFirstDateInPtnIsLaterDate = firstDateInPtnIsLaterDate;
202 public String getFirstPart() {
203 return fIntervalPatternFirstPart;
210 public String getSecondPart() {
211 return fIntervalPatternSecondPart;
218 public boolean firstDateInPtnIsLaterDate() {
219 return fFirstDateInPtnIsLaterDate;
226 public boolean equals(Object a) {
227 if ( a instanceof PatternInfo ) {
228 PatternInfo patternInfo = (PatternInfo)a;
229 return Utility.objectEquals(fIntervalPatternFirstPart, patternInfo.fIntervalPatternFirstPart) &&
230 Utility.objectEquals(fIntervalPatternSecondPart, fIntervalPatternSecondPart) &&
231 fFirstDateInPtnIsLaterDate == patternInfo.fFirstDateInPtnIsLaterDate;
240 public int hashCode() {
241 int hash = fIntervalPatternFirstPart != null ? fIntervalPatternFirstPart.hashCode() : 0;
242 if (fIntervalPatternSecondPart != null) {
243 hash ^= fIntervalPatternSecondPart.hashCode();
245 if (fFirstDateInPtnIsLaterDate) {
252 // Following is package protected since
253 // it is shared with DateIntervalFormat.
254 static final String[] CALENDAR_FIELD_TO_PATTERN_LETTER =
264 private static final long serialVersionUID = 1;
265 private static final int MINIMUM_SUPPORTED_CALENDAR_FIELD =
267 //private static boolean DEBUG = true;
269 private static String FALLBACK_STRING = "fallback";
270 private static String LATEST_FIRST_PREFIX = "latestFirst:";
271 private static String EARLIEST_FIRST_PREFIX = "earliestFirst:";
273 // DateIntervalInfo cache
274 private final static ICUCache<String, DateIntervalInfo> DIICACHE = new SimpleCache<String, DateIntervalInfo>();
276 // default interval pattern on the skeleton, {0} - {1}
277 private String fFallbackIntervalPattern;
279 private boolean fFirstDateInPtnIsLaterDate = false;
281 // HashMap( skeleton, HashMap(largest_different_field, pattern) )
282 private Map<String, Map<String, PatternInfo>> fIntervalPatterns = null;
284 private transient boolean frozen = false;
286 // If true, fIntervalPatterns should not be modified in-place because it
287 // is shared with other objects. Unlike frozen which is always true once
288 // set to true, this field can go from true to false as long as frozen is
290 private transient boolean fIntervalPatternsReadOnly = false;
294 * Create empty instance.
295 * It does not initialize any interval patterns except
296 * that it initialize default fall-back pattern as "{0} - {1}",
297 * which can be reset by setFallbackIntervalPattern().
299 * It should be followed by setFallbackIntervalPattern() and
300 * setIntervalPattern(),
301 * and is recommended to be used only for power users who
302 * wants to create their own interval patterns and use them to create
303 * date interval formatter.
305 * @deprecated This API is ICU internal only.
307 public DateIntervalInfo()
309 fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>();
310 fFallbackIntervalPattern = "{0} \u2013 {1}";
315 * Construct DateIntervalInfo for the given locale,
316 * @param locale the interval patterns are loaded from the appropriate
317 * calendar data (specified calendar or default calendar)
321 public DateIntervalInfo(ULocale locale)
323 initializeData(locale);
328 * Initialize the DateIntervalInfo from locale
329 * @param locale the given locale.
331 private void initializeData(ULocale locale)
333 String key = locale.toString();
334 DateIntervalInfo dii = DIICACHE.get(key);
336 // initialize data from scratch
338 // Marking fIntervalPatterns read-only makes cloning cheaper.
339 fIntervalPatternsReadOnly = true;
340 // We freeze what goes in the cache without freezing this object.
341 DIICACHE.put(key, ((DateIntervalInfo) clone()).freeze());
343 initializeFromReadOnlyPatterns(dii);
350 * Initialize this object
351 * @param dii must have read-only fIntervalPatterns.
353 private void initializeFromReadOnlyPatterns(DateIntervalInfo dii) {
354 fFallbackIntervalPattern = dii.fFallbackIntervalPattern;
355 fFirstDateInPtnIsLaterDate = dii.fFirstDateInPtnIsLaterDate;
356 fIntervalPatterns = dii.fIntervalPatterns;
357 fIntervalPatternsReadOnly = true;
362 * Initialize DateIntervalInfo from calendar data
363 * @param calData calendar data
365 private void setup(ULocale locale) {
366 int DEFAULT_HASH_SIZE = 19;
367 fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>(DEFAULT_HASH_SIZE);
368 // initialize to guard if there is no interval date format defined in
370 fFallbackIntervalPattern = "{0} \u2013 {1}";
371 HashSet<String> skeletonSet = new HashSet<String>();
373 // loop through all locales to get all available skeletons'
375 ULocale currentLocale = locale;
376 // Get the correct calendar type
377 String calendarTypeToUse = locale.getKeywordValue("calendar");
378 if ( calendarTypeToUse == null ) {
379 String[] preferredCalendarTypes = Calendar.getKeywordValuesForLocale("calendar", locale, true);
380 calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar
382 if ( calendarTypeToUse == null ) {
383 calendarTypeToUse = "gregorian"; // fallback
386 String name = currentLocale.getName();
387 if ( name.length() == 0 ) {
391 ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,currentLocale);
393 // ICU4J getWithFallback does not work well when
394 // 1) A nested table is an alias to /LOCALE/...
395 // 2) getWithFallback is called multiple times for going down hierarchical resource path
396 // #9987 resolved the issue of alias table when full path is specified in getWithFallback,
397 // but there is no easy solution when the equivalent operation is done by multiple operations.
398 // This issue is addressed in #9964.
399 // ICUResourceBundle calBundle = rb.getWithFallback("calendar");
400 // ICUResourceBundle calTypeBundle = calBundle.getWithFallback(calendarTypeToUse);
401 ICUResourceBundle itvDtPtnResource =rb.getWithFallback("calendar/" + calendarTypeToUse + "/intervalFormats");
402 // look for fallback first, since it establishes the default order
403 String fallback = itvDtPtnResource.getStringWithFallback(FALLBACK_STRING);
404 setFallbackIntervalPattern(fallback);
405 int size = itvDtPtnResource.getSize();
406 for ( int index = 0; index < size; ++index ) {
407 String skeleton = itvDtPtnResource.get(index).getKey();
408 if ( skeletonSet.contains(skeleton) ) {
411 skeletonSet.add(skeleton);
412 if ( skeleton.compareTo(FALLBACK_STRING) == 0 ) {
415 ICUResourceBundle intervalPatterns = (ICUResourceBundle)itvDtPtnResource.get(skeleton);
416 int ptnNum = intervalPatterns.getSize();
417 for ( int ptnIndex = 0; ptnIndex < ptnNum; ++ptnIndex) {
418 String key = intervalPatterns.get(ptnIndex).getKey();
419 String pattern = intervalPatterns.get(ptnIndex).getString();
421 int calendarField = -1; // initialize with an invalid value.
422 if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR]) == 0 ) {
423 calendarField = Calendar.YEAR;
424 } else if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.MONTH]) == 0 ) {
425 calendarField = Calendar.MONTH;
426 } else if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE]) == 0 ) {
427 calendarField = Calendar.DATE;
428 } else if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM]) == 0 ) {
429 calendarField = Calendar.AM_PM;
430 } else if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR]) == 0 ) {
431 calendarField = Calendar.HOUR;
432 } else if ( key.compareTo(CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.MINUTE]) == 0 ) {
433 calendarField = Calendar.MINUTE;
436 if ( calendarField != -1 ) {
437 setIntervalPatternInternally(skeleton, key, pattern);
442 UResourceBundle parentNameBundle = rb.get("%%Parent");
443 currentLocale = new ULocale(parentNameBundle.getString());
444 } catch (MissingResourceException e) {
445 currentLocale = currentLocale.getFallback();
447 } while (currentLocale != null && !currentLocale.getBaseName().equals("root"));
448 } catch ( MissingResourceException e) {
449 // ok, will fallback to {data0} - {date1}
455 * Split interval patterns into 2 part.
456 * @param intervalPattern interval pattern
457 * @return the index in interval pattern which split the pattern into 2 part
459 private static int splitPatternInto2Part(String intervalPattern) {
460 boolean inQuote = false;
464 /* repeatedPattern used to record whether a pattern has already seen.
465 It is a pattern applies to first calendar if it is first time seen,
466 otherwise, it is a pattern applies to the second calendar
468 int[] patternRepeated = new int[58];
470 int PATTERN_CHAR_BASE = 0x41;
472 /* loop through the pattern string character by character looking for
473 * the first repeated pattern letter, which breaks the interval pattern
477 boolean foundRepetition = false;
478 for (i = 0; i < intervalPattern.length(); ++i) {
479 char ch = intervalPattern.charAt(i);
481 if (ch != prevCh && count > 0) {
482 // check the repeativeness of pattern letter
483 int repeated = patternRepeated[prevCh - PATTERN_CHAR_BASE];
484 if ( repeated == 0 ) {
485 patternRepeated[prevCh - PATTERN_CHAR_BASE] = 1;
487 foundRepetition = true;
493 // Consecutive single quotes are a single quote literal,
494 // either outside of quotes or between quotes
495 if ((i+1) < intervalPattern.length() &&
496 intervalPattern.charAt(i+1) == '\'') {
502 else if (!inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/)
503 || (ch >= 0x0041 /*'A'*/ && ch <= 0x005A /*'Z'*/))) {
504 // ch is a date-time pattern character
509 // check last pattern char, distinguish
510 // "dd MM" ( no repetition ),
511 // "d-d"(last char repeated ), and
512 // "d-d MM" ( repetition found )
513 if ( count > 0 && foundRepetition == false ) {
514 if ( patternRepeated[prevCh - PATTERN_CHAR_BASE] == 0 ) {
523 * Provides a way for client to build interval patterns.
524 * User could construct DateIntervalInfo by providing
525 * a list of skeletons and their patterns.
529 * DateIntervalInfo dIntervalInfo = new DateIntervalInfo();
530 * dIntervalInfo.setIntervalPattern("yMd", Calendar.YEAR, "'from' yyyy-M-d 'to' yyyy-M-d");
531 * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.MONTH, "'from' yyyy MMM d 'to' MMM d");
532 * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.DAY, "yyyy MMM d-d");
533 * dIntervalInfo.setFallbackIntervalPattern("{0} ~ {1}");
537 * Currently, users can only set interval patterns when the following
538 * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH,
539 * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, and MINUTE.
540 * Interval patterns when other calendar fields are different are
543 * @param skeleton the skeleton on which interval pattern based
544 * @param lrgDiffCalUnit the largest different calendar unit.
545 * @param intervalPattern the interval pattern on the largest different
547 * For example, if lrgDiffCalUnit is
548 * "year", the interval pattern for en_US when year
549 * is different could be "'from' yyyy 'to' yyyy".
550 * @throws IllegalArgumentException if setting interval pattern on
551 * a calendar field that is smaller
552 * than the MINIMUM_SUPPORTED_CALENDAR_FIELD
553 * @throws UnsupportedOperationException if the object is frozen
556 public void setIntervalPattern(String skeleton,
558 String intervalPattern)
561 throw new UnsupportedOperationException("no modification is allowed after DII is frozen");
563 if ( lrgDiffCalUnit > MINIMUM_SUPPORTED_CALENDAR_FIELD ) {
564 throw new IllegalArgumentException("calendar field is larger than MINIMUM_SUPPORTED_CALENDAR_FIELD");
566 if (fIntervalPatternsReadOnly) {
567 fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
568 fIntervalPatternsReadOnly = false;
570 PatternInfo ptnInfo = setIntervalPatternInternally(skeleton,
571 CALENDAR_FIELD_TO_PATTERN_LETTER[lrgDiffCalUnit],
573 if ( lrgDiffCalUnit == Calendar.HOUR_OF_DAY ) {
574 setIntervalPattern(skeleton,
575 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM],
577 setIntervalPattern(skeleton,
578 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR],
580 } else if ( lrgDiffCalUnit == Calendar.DAY_OF_MONTH ||
581 lrgDiffCalUnit == Calendar.DAY_OF_WEEK ) {
582 setIntervalPattern(skeleton,
583 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE],
589 /* Set Interval pattern.
591 * It generates the interval pattern info,
592 * afer which, not only sets the interval pattern info into the hash map,
593 * but also returns the interval pattern info to the caller
594 * so that caller can re-use it.
596 * @param skeleton skeleton on which the interval pattern based
597 * @param lrgDiffCalUnit the largest different calendar unit.
598 * @param intervalPattern the interval pattern on the largest different
600 * @return the interval pattern pattern information
602 private PatternInfo setIntervalPatternInternally(String skeleton,
603 String lrgDiffCalUnit,
604 String intervalPattern) {
605 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
606 boolean emptyHash = false;
607 if (patternsOfOneSkeleton == null) {
608 patternsOfOneSkeleton = new HashMap<String, PatternInfo>();
611 boolean order = fFirstDateInPtnIsLaterDate;
612 // check for "latestFirst:" or "earliestFirst:" prefix
613 if ( intervalPattern.startsWith(LATEST_FIRST_PREFIX) ) {
615 int prefixLength = LATEST_FIRST_PREFIX.length();
616 intervalPattern = intervalPattern.substring(prefixLength, intervalPattern.length());
617 } else if ( intervalPattern.startsWith(EARLIEST_FIRST_PREFIX) ) {
619 int earliestFirstLength = EARLIEST_FIRST_PREFIX.length();
620 intervalPattern = intervalPattern.substring(earliestFirstLength, intervalPattern.length());
622 PatternInfo itvPtnInfo = genPatternInfo(intervalPattern, order);
624 patternsOfOneSkeleton.put(lrgDiffCalUnit, itvPtnInfo);
625 if ( emptyHash == true ) {
626 fIntervalPatterns.put(skeleton, patternsOfOneSkeleton);
633 /* Set Interval pattern.
635 * @param skeleton skeleton on which the interval pattern based
636 * @param lrgDiffCalUnit the largest different calendar unit.
637 * @param ptnInfo interval pattern infomration
639 private void setIntervalPattern(String skeleton,
640 String lrgDiffCalUnit,
641 PatternInfo ptnInfo) {
642 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
643 patternsOfOneSkeleton.put(lrgDiffCalUnit, ptnInfo);
648 * Break interval patterns as 2 part and save them into pattern info.
649 * @param intervalPattern interval pattern
650 * @param laterDateFirst whether the first date in intervalPattern
651 * is earlier date or later date
652 * @return pattern info object
654 static PatternInfo genPatternInfo(String intervalPattern,
655 boolean laterDateFirst) {
656 int splitPoint = splitPatternInto2Part(intervalPattern);
658 String firstPart = intervalPattern.substring(0, splitPoint);
659 String secondPart = null;
660 if ( splitPoint < intervalPattern.length() ) {
661 secondPart = intervalPattern.substring(splitPoint, intervalPattern.length());
664 return new PatternInfo(firstPart, secondPart, laterDateFirst);
669 * Get the interval pattern given the largest different calendar field.
670 * @param skeleton the skeleton
671 * @param field the largest different calendar field
672 * @return interval pattern return null if interval pattern is not found.
673 * @throws IllegalArgumentException if getting interval pattern on
674 * a calendar field that is smaller
675 * than the MINIMUM_SUPPORTED_CALENDAR_FIELD
678 public PatternInfo getIntervalPattern(String skeleton, int field)
680 if ( field > MINIMUM_SUPPORTED_CALENDAR_FIELD ) {
681 throw new IllegalArgumentException("no support for field less than MINUTE");
683 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
684 if ( patternsOfOneSkeleton != null ) {
685 PatternInfo intervalPattern = patternsOfOneSkeleton.
686 get(CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
687 if ( intervalPattern != null ) {
688 return intervalPattern;
697 * Get the fallback interval pattern.
698 * @return fallback interval pattern
701 public String getFallbackIntervalPattern()
703 return fFallbackIntervalPattern;
708 * Re-set the fallback interval pattern.
710 * In construction, default fallback pattern is set as "{0} - {1}".
711 * And constructor taking locale as parameter will set the
712 * fallback pattern as what defined in the locale resource file.
714 * This method provides a way for user to replace the fallback pattern.
716 * @param fallbackPattern fall-back interval pattern.
717 * @throws UnsupportedOperationException if the object is frozen
718 * @throws IllegalArgumentException if there is no pattern {0} or
719 * pattern {1} in fallbakckPattern
723 public void setFallbackIntervalPattern(String fallbackPattern)
726 throw new UnsupportedOperationException("no modification is allowed after DII is frozen");
728 int firstPatternIndex = fallbackPattern.indexOf("{0}");
729 int secondPatternIndex = fallbackPattern.indexOf("{1}");
730 if ( firstPatternIndex == -1 || secondPatternIndex == -1 ) {
731 throw new IllegalArgumentException("no pattern {0} or pattern {1} in fallbackPattern");
733 if ( firstPatternIndex > secondPatternIndex ) {
734 fFirstDateInPtnIsLaterDate = true;
736 fFallbackIntervalPattern = fallbackPattern;
741 * Get default order -- whether the first date in pattern is later date
744 * return default date ordering in interval pattern. TRUE if the first date
745 * in pattern is later date, FALSE otherwise.
748 public boolean getDefaultOrder()
750 return fFirstDateInPtnIsLaterDate;
755 * Boilerplate. Clone this object.
756 * @return a copy of the object
759 public Object clone()
764 return cloneUnfrozenDII();
769 * Clone an unfrozen DateIntervalInfo object.
770 * @return a copy of the object
772 private Object cloneUnfrozenDII() //throws IllegalStateException
775 DateIntervalInfo other = (DateIntervalInfo) super.clone();
776 other.fFallbackIntervalPattern=fFallbackIntervalPattern;
777 other.fFirstDateInPtnIsLaterDate = fFirstDateInPtnIsLaterDate;
778 if (fIntervalPatternsReadOnly) {
779 other.fIntervalPatterns = fIntervalPatterns;
780 other.fIntervalPatternsReadOnly = true;
782 other.fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
783 other.fIntervalPatternsReadOnly = false;
785 other.frozen = false;
787 } catch ( CloneNotSupportedException e ) {
789 throw new IllegalStateException("clone is not supported");
794 private static Map<String, Map<String, PatternInfo>> cloneIntervalPatterns(
795 Map<String, Map<String, PatternInfo>> patterns) {
796 Map<String, Map<String, PatternInfo>> result = new HashMap<String, Map<String, PatternInfo>>();
797 for (Entry<String, Map<String, PatternInfo>> skeletonEntry : patterns.entrySet()) {
798 String skeleton = skeletonEntry.getKey();
799 Map<String, PatternInfo> patternsOfOneSkeleton = skeletonEntry.getValue();
800 Map<String, PatternInfo> oneSetPtn = new HashMap<String, PatternInfo>();
801 for (Entry<String, PatternInfo> calEntry : patternsOfOneSkeleton.entrySet()) {
802 String calField = calEntry.getKey();
803 PatternInfo value = calEntry.getValue();
804 oneSetPtn.put(calField, value);
806 result.put(skeleton, oneSetPtn);
814 * Boilerplate for Freezable
817 public boolean isFrozen() {
822 * Boilerplate for Freezable
825 public DateIntervalInfo freeze() {
827 fIntervalPatternsReadOnly = true;
832 * Boilerplate for Freezable
835 public DateIntervalInfo cloneAsThawed() {
836 DateIntervalInfo result = (DateIntervalInfo) (this.cloneUnfrozenDII());
842 * Parse skeleton, save each field's width.
843 * It is used for looking for best match skeleton,
844 * and adjust pattern field width.
845 * @param skeleton skeleton to be parsed
846 * @param skeletonFieldWidth parsed skeleton field width
848 static void parseSkeleton(String skeleton, int[] skeletonFieldWidth) {
849 int PATTERN_CHAR_BASE = 0x41;
850 for ( int i = 0; i < skeleton.length(); ++i ) {
851 ++skeletonFieldWidth[skeleton.charAt(i) - PATTERN_CHAR_BASE];
858 * Check whether one field width is numeric while the other is string.
860 * TODO (xji): make it general
862 * @param fieldWidth one field width
863 * @param anotherFieldWidth another field width
864 * @param patternLetter pattern letter char
865 * @return true if one field width is numeric and the other is string,
868 private static boolean stringNumeric(int fieldWidth,
869 int anotherFieldWidth,
870 char patternLetter) {
871 if ( patternLetter == 'M' ) {
872 if ( fieldWidth <= 2 && anotherFieldWidth > 2 ||
873 fieldWidth > 2 && anotherFieldWidth <= 2 ) {
882 * given an input skeleton, get the best match skeleton
883 * which has pre-defined interval pattern in resource file.
885 * TODO (xji): set field weight or
886 * isolate the funtionality in DateTimePatternGenerator
887 * @param inputSkeleton input skeleton
888 * @return 0, if there is exact match for input skeleton
889 * 1, if there is only field width difference between
890 * the best match and the input skeleton
891 * 2, the only field difference is 'v' and 'z'
892 * -1, if there is calendar field difference between
893 * the best match and the input skeleton
895 DateIntervalFormat.BestMatchInfo getBestSkeleton(String inputSkeleton) {
896 String bestSkeleton = inputSkeleton;
897 int[] inputSkeletonFieldWidth = new int[58];
898 int[] skeletonFieldWidth = new int[58];
900 final int DIFFERENT_FIELD = 0x1000;
901 final int STRING_NUMERIC_DIFFERENCE = 0x100;
902 final int BASE = 0x41;
904 // TODO: this is a hack for 'v' and 'z'
905 // resource bundle only have time skeletons ending with 'v',
906 // but not for time skeletons ending with 'z'.
907 boolean replaceZWithV = false;
908 if ( inputSkeleton.indexOf('z') != -1 ) {
909 inputSkeleton = inputSkeleton.replace('z', 'v');
910 replaceZWithV = true;
913 parseSkeleton(inputSkeleton, inputSkeletonFieldWidth);
914 int bestDistance = Integer.MAX_VALUE;
915 // 0 means exact the same skeletons;
916 // 1 means having the same field, but with different length,
917 // 2 means only z/v differs
918 // -1 means having different field.
919 int bestFieldDifference = 0;
920 for (String skeleton : fIntervalPatterns.keySet()) {
921 // clear skeleton field width
922 for ( int i = 0; i < skeletonFieldWidth.length; ++i ) {
923 skeletonFieldWidth[i] = 0;
925 parseSkeleton(skeleton, skeletonFieldWidth);
926 // calculate distance
928 int fieldDifference = 1;
929 for ( int i = 0; i < inputSkeletonFieldWidth.length; ++i ) {
930 int inputFieldWidth = inputSkeletonFieldWidth[i];
931 int fieldWidth = skeletonFieldWidth[i];
932 if ( inputFieldWidth == fieldWidth ) {
935 if ( inputFieldWidth == 0 ) {
936 fieldDifference = -1;
937 distance += DIFFERENT_FIELD;
938 } else if ( fieldWidth == 0 ) {
939 fieldDifference = -1;
940 distance += DIFFERENT_FIELD;
941 } else if (stringNumeric(inputFieldWidth, fieldWidth,
943 distance += STRING_NUMERIC_DIFFERENCE;
945 distance += Math.abs(inputFieldWidth - fieldWidth);
948 if ( distance < bestDistance ) {
949 bestSkeleton = skeleton;
950 bestDistance = distance;
951 bestFieldDifference = fieldDifference;
953 if ( distance == 0 ) {
954 bestFieldDifference = 0;
958 if ( replaceZWithV && bestFieldDifference != -1 ) {
959 bestFieldDifference = 2;
961 return new DateIntervalFormat.BestMatchInfo(bestSkeleton, bestFieldDifference);
968 public boolean equals(Object a) {
969 if ( a instanceof DateIntervalInfo ) {
970 DateIntervalInfo dtInfo = (DateIntervalInfo)a;
971 return fIntervalPatterns.equals(dtInfo.fIntervalPatterns);
980 public int hashCode() {
981 return fIntervalPatterns.hashCode();
986 * @deprecated This API is ICU internal only.
988 public Map<String,Set<String>> getPatterns() {
989 LinkedHashMap<String,Set<String>> result = new LinkedHashMap<String,Set<String>>();
990 for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) {
991 result.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue().keySet()));
995 }// end class DateIntervalInfo