1// © 2016 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3/* 4 ******************************************************************************* 5 * Copyright (C) 2008-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9 10package com.ibm.icu.text; 11 12import java.io.Serializable; 13import java.util.HashMap; 14import java.util.HashSet; 15import java.util.LinkedHashMap; 16import java.util.LinkedHashSet; 17import java.util.Locale; 18import java.util.Map; 19import java.util.Map.Entry; 20import java.util.MissingResourceException; 21import java.util.Set; 22 23import com.ibm.icu.impl.ICUCache; 24import com.ibm.icu.impl.ICUData; 25import com.ibm.icu.impl.ICUResourceBundle; 26import com.ibm.icu.impl.SimpleCache; 27import com.ibm.icu.impl.UResource; 28import com.ibm.icu.impl.UResource.Key; 29import com.ibm.icu.impl.UResource.Value; 30import com.ibm.icu.impl.Utility; 31import com.ibm.icu.util.Calendar; 32import com.ibm.icu.util.Freezable; 33import com.ibm.icu.util.ICUCloneNotSupportedException; 34import com.ibm.icu.util.ICUException; 35import com.ibm.icu.util.ULocale; 36import com.ibm.icu.util.UResourceBundle; 37 38/** 39 * DateIntervalInfo is a public class for encapsulating localizable 40 * date time interval patterns. It is used by DateIntervalFormat. 41 * 42 * <P> 43 * For most users, ordinary use of DateIntervalFormat does not need to create 44 * DateIntervalInfo object directly. 45 * DateIntervalFormat will take care of it when creating a date interval 46 * formatter when user pass in skeleton and locale. 47 * 48 * <P> 49 * For power users, who want to create their own date interval patterns, 50 * or want to re-set date interval patterns, they could do so by 51 * directly creating DateIntervalInfo and manipulating it. 52 * 53 * <P> 54 * Logically, the interval patterns are mappings 55 * from (skeleton, the_largest_different_calendar_field) 56 * to (date_interval_pattern). 57 * 58 * <P> 59 * A skeleton 60 * <ol> 61 * <li> 62 * only keeps the field pattern letter and ignores all other parts 63 * in a pattern, such as space, punctuations, and string literals. 64 * <li> 65 * hides the order of fields. 66 * <li> 67 * might hide a field's pattern letter length. 68 * 69 * For those non-digit calendar fields, the pattern letter length is 70 * important, such as MMM, MMMM, and MMMMM; EEE and EEEE, 71 * and the field's pattern letter length is honored. 72 * 73 * For the digit calendar fields, such as M or MM, d or dd, yy or yyyy, 74 * the field pattern length is ignored and the best match, which is defined 75 * in date time patterns, will be returned without honor the field pattern 76 * letter length in skeleton. 77 * </ol> 78 * 79 * <P> 80 * The calendar fields we support for interval formatting are: 81 * year, month, date, day-of-week, am-pm, hour, hour-of-day, minute, and 82 * second (though we do not currently have specific intervalFormat data for 83 * skeletons with seconds). 84 * Those calendar fields can be defined in the following order: 85 * year > month > date > am-pm > hour > minute > second 86 * 87 * The largest different calendar fields between 2 calendars is the 88 * first different calendar field in above order. 89 * 90 * For example: the largest different calendar fields between "Jan 10, 2007" 91 * and "Feb 20, 2008" is year. 92 * 93 * <P> 94 * There is a set of pre-defined static skeleton strings. 95 * There are pre-defined interval patterns for those pre-defined skeletons 96 * in locales' resource files. 97 * For example, for a skeleton YEAR_ABBR_MONTH_DAY, which is "yMMMd", 98 * in en_US, if the largest different calendar field between date1 and date2 99 * is "year", the date interval pattern is "MMM d, yyyy - MMM d, yyyy", 100 * such as "Jan 10, 2007 - Jan 10, 2008". 101 * If the largest different calendar field between date1 and date2 is "month", 102 * the date interval pattern is "MMM d - MMM d, yyyy", 103 * such as "Jan 10 - Feb 10, 2007". 104 * If the largest different calendar field between date1 and date2 is "day", 105 * the date interval pattern is ""MMM d-d, yyyy", such as "Jan 10-20, 2007". 106 * 107 * For date skeleton, the interval patterns when year, or month, or date is 108 * different are defined in resource files. 109 * For time skeleton, the interval patterns when am/pm, or hour, or minute is 110 * different are defined in resource files. 111 * 112 * 113 * <P> 114 * There are 2 dates in interval pattern. For most locales, the first date 115 * in an interval pattern is the earlier date. There might be a locale in which 116 * the first date in an interval pattern is the later date. 117 * We use fallback format for the default order for the locale. 118 * For example, if the fallback format is "{0} - {1}", it means 119 * the first date in the interval pattern for this locale is earlier date. 120 * If the fallback format is "{1} - {0}", it means the first date is the 121 * later date. 122 * For a particular interval pattern, the default order can be overriden 123 * by prefixing "latestFirst:" or "earliestFirst:" to the interval pattern. 124 * For example, if the fallback format is "{0}-{1}", 125 * but for skeleton "yMMMd", the interval pattern when day is different is 126 * "latestFirst:d-d MMM yy", it means by default, the first date in interval 127 * pattern is the earlier date. But for skeleton "yMMMd", when day is different, 128 * the first date in "d-d MMM yy" is the later date. 129 * 130 * <P> 131 * The recommended way to create a DateIntervalFormat object is to pass in 132 * the locale. 133 * By using a Locale parameter, the DateIntervalFormat object is 134 * initialized with the pre-defined interval patterns for a given or 135 * default locale. 136 * <P> 137 * Users can also create DateIntervalFormat object 138 * by supplying their own interval patterns. 139 * It provides flexibility for power usage. 140 * 141 * <P> 142 * After a DateIntervalInfo object is created, clients may modify 143 * the interval patterns using setIntervalPattern function as so desired. 144 * Currently, users can only set interval patterns when the following 145 * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, 146 * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE and SECOND. 147 * Interval patterns when other calendar fields are different is not supported. 148 * <P> 149 * DateIntervalInfo objects are cloneable. 150 * When clients obtain a DateIntervalInfo object, 151 * they can feel free to modify it as necessary. 152 * <P> 153 * DateIntervalInfo are not expected to be subclassed. 154 * Data for a calendar is loaded out of resource bundles. 155 * Through ICU 4.4, date interval patterns are only supported in the Gregoria 156 * calendar; non-Gregorian calendars are supported from ICU 4.4.1. 157 * 158 * @stable ICU 4.0 159 */ 160 161public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, Serializable { 162 163 /* Save the interval pattern information. 164 * Interval pattern consists of 2 single date patterns and the separator. 165 * For example, interval pattern "MMM d - MMM d, yyyy" consists 166 * a single date pattern "MMM d", another single date pattern "MMM d, yyyy", 167 * and a separator "-". 168 * Also, the first date appears in an interval pattern could be 169 * the earlier date or the later date. 170 * And such information is saved in the interval pattern as well. 171 */ 172 static final int currentSerialVersion = 1; 173 174 /** 175 * PatternInfo class saves the first and second part of interval pattern, 176 * and whether the interval pattern is earlier date first. 177 * @stable ICU 4.0 178 */ 179 public static final class PatternInfo implements Cloneable, Serializable { 180 static final int currentSerialVersion = 1; 181 private static final long serialVersionUID = 1; 182 private final String fIntervalPatternFirstPart; 183 private final String fIntervalPatternSecondPart; 184 /* 185 * Whether the first date in interval pattern is later date or not. 186 * Fallback format set the default ordering. 187 * And for a particular interval pattern, the order can be 188 * overriden by prefixing the interval pattern with "latestFirst:" or 189 * "earliestFirst:" 190 * For example, given 2 date, Jan 10, 2007 to Feb 10, 2007. 191 * if the fallback format is "{0} - {1}", 192 * and the pattern is "d MMM - d MMM yyyy", the interval format is 193 * "10 Jan - 10 Feb, 2007". 194 * If the pattern is "latestFirst:d MMM - d MMM yyyy", 195 * the interval format is "10 Feb - 10 Jan, 2007" 196 */ 197 private final boolean fFirstDateInPtnIsLaterDate; 198 199 /** 200 * Constructs a <code>PatternInfo</code> object. 201 * @param firstPart The first part of interval pattern. 202 * @param secondPart The second part of interval pattern. 203 * @param firstDateInPtnIsLaterDate Whether the first date in interval patter is later date or not. 204 * @stable ICU 4.0 205 */ 206 public PatternInfo(String firstPart, String secondPart, 207 boolean firstDateInPtnIsLaterDate) { 208 fIntervalPatternFirstPart = firstPart; 209 fIntervalPatternSecondPart = secondPart; 210 fFirstDateInPtnIsLaterDate = firstDateInPtnIsLaterDate; 211 } 212 213 /** 214 * Returns the first part of interval pattern. 215 * @return The first part of interval pattern. 216 * @stable ICU 4.0 217 */ 218 public String getFirstPart() { 219 return fIntervalPatternFirstPart; 220 } 221 222 /** 223 * Returns the second part of interval pattern. 224 * @return The second part of interval pattern. 225 * @stable ICU 4.0 226 */ 227 public String getSecondPart() { 228 return fIntervalPatternSecondPart; 229 } 230 231 /** 232 * Returns whether the first date in interval patter is later date or not. 233 * @return Whether the first date in interval patter is later date or not. 234 * @stable ICU 4.0 235 */ 236 public boolean firstDateInPtnIsLaterDate() { 237 return fFirstDateInPtnIsLaterDate; 238 } 239 240 /** 241 * Compares the specified object with this <code>PatternInfo</code> for equality. 242 * @param a The object to be compared. 243 * @return <code>true</code> if the specified object is equal to this <code>PatternInfo</code>. 244 * @stable ICU 4.0 245 */ 246 @Override 247 public boolean equals(Object a) { 248 if (a instanceof PatternInfo) { 249 PatternInfo patternInfo = (PatternInfo)a; 250 return Utility.objectEquals(fIntervalPatternFirstPart, patternInfo.fIntervalPatternFirstPart) && 251 Utility.objectEquals(fIntervalPatternSecondPart, patternInfo.fIntervalPatternSecondPart) && 252 fFirstDateInPtnIsLaterDate == patternInfo.fFirstDateInPtnIsLaterDate; 253 } 254 return false; 255 } 256 257 /** 258 * Returns the hash code of this <code>PatternInfo</code>. 259 * @return A hash code value for this object. 260 * @stable ICU 4.0 261 */ 262 @Override 263 public int hashCode() { 264 int hash = fIntervalPatternFirstPart != null ? fIntervalPatternFirstPart.hashCode() : 0; 265 if (fIntervalPatternSecondPart != null) { 266 hash ^= fIntervalPatternSecondPart.hashCode(); 267 } 268 if (fFirstDateInPtnIsLaterDate) { 269 hash ^= -1; 270 } 271 return hash; 272 } 273 274 /** 275 * {@inheritDoc} 276 * @internal 277 * @deprecated This API is ICU internal only. 278 */ 279 @Deprecated 280 @Override 281 public String toString() { 282 return "{first=«" + fIntervalPatternFirstPart + "», second=«" + fIntervalPatternSecondPart + "», reversed:" + fFirstDateInPtnIsLaterDate + "}"; 283 } 284 } 285 286 // Following is package protected since 287 // it is shared with DateIntervalFormat. 288 static final String[] CALENDAR_FIELD_TO_PATTERN_LETTER = 289 { 290 "G", "y", "M", 291 "w", "W", "d", 292 "D", "E", "F", 293 "a", "h", "H", 294 "m", "s", "S", // MINUTE, SECOND, MILLISECOND 295 "z", " ", "Y", // ZONE_OFFSET, DST_OFFSET, YEAR_WOY 296 "e", "u", "g", // DOW_LOCAL, EXTENDED_YEAR, JULIAN_DAY 297 "A", " ", " ", // MILLISECONDS_IN_DAY, IS_LEAP_MONTH. 298 }; 299 300 301 private static final long serialVersionUID = 1; 302 private static final int MINIMUM_SUPPORTED_CALENDAR_FIELD = 303 Calendar.SECOND; 304 //private static boolean DEBUG = true; 305 306 private static String CALENDAR_KEY = "calendar"; 307 private static String INTERVAL_FORMATS_KEY = "intervalFormats"; 308 private static String FALLBACK_STRING = "fallback"; 309 private static String LATEST_FIRST_PREFIX = "latestFirst:"; 310 private static String EARLIEST_FIRST_PREFIX = "earliestFirst:"; 311 312 // DateIntervalInfo cache 313 private final static ICUCache<String, DateIntervalInfo> DIICACHE = new SimpleCache<String, DateIntervalInfo>(); 314 315 316 // default interval pattern on the skeleton, {0} - {1} 317 private String fFallbackIntervalPattern; 318 // default order 319 private boolean fFirstDateInPtnIsLaterDate = false; 320 321 // HashMap( skeleton, HashMap(largest_different_field, pattern) ) 322 private Map<String, Map<String, PatternInfo>> fIntervalPatterns = null; 323 324 private transient volatile boolean frozen = false; 325 326 // If true, fIntervalPatterns should not be modified in-place because it 327 // is shared with other objects. Unlike frozen which is always true once 328 // set to true, this field can go from true to false as long as frozen is 329 // false. 330 private transient boolean fIntervalPatternsReadOnly = false; 331 332 333 /** 334 * Create empty instance. 335 * It does not initialize any interval patterns except 336 * that it initialize default fall-back pattern as "{0} - {1}", 337 * which can be reset by setFallbackIntervalPattern(). 338 * 339 * It should be followed by setFallbackIntervalPattern() and 340 * setIntervalPattern(), 341 * and is recommended to be used only for power users who 342 * wants to create their own interval patterns and use them to create 343 * date interval formatter. 344 * @internal 345 * @deprecated This API is ICU internal only. 346 */ 347 @Deprecated 348 public DateIntervalInfo() 349 { 350 fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>(); 351 fFallbackIntervalPattern = "{0} \u2013 {1}"; 352 } 353 354 355 /** 356 * Construct DateIntervalInfo for the given locale, 357 * @param locale the interval patterns are loaded from the appropriate 358 * calendar data (specified calendar or default calendar) 359 * in this locale. 360 * @stable ICU 4.0 361 */ 362 public DateIntervalInfo(ULocale locale) 363 { 364 initializeData(locale); 365 } 366 367 368 /** 369 * Construct DateIntervalInfo for the given {@link java.util.Locale}. 370 * @param locale the interval patterns are loaded from the appropriate 371 * calendar data (specified calendar or default calendar) 372 * in this locale. 373 * @stable ICU 54 374 */ 375 public DateIntervalInfo(Locale locale) 376 { 377 this(ULocale.forLocale(locale)); 378 } 379 380 /* 381 * Initialize the DateIntervalInfo from locale 382 * @param locale the given locale. 383 */ 384 private void initializeData(ULocale locale) 385 { 386 String key = locale.toString(); 387 DateIntervalInfo dii = DIICACHE.get(key); 388 if ( dii == null ) { 389 // initialize data from scratch 390 setup(locale); 391 // Marking fIntervalPatterns read-only makes cloning cheaper. 392 fIntervalPatternsReadOnly = true; 393 // We freeze what goes in the cache without freezing this object. 394 DIICACHE.put(key, ((DateIntervalInfo) clone()).freeze()); 395 } else { 396 initializeFromReadOnlyPatterns(dii); 397 } 398 } 399 400 401 402 /** 403 * Initialize this object 404 * @param dii must have read-only fIntervalPatterns. 405 */ 406 private void initializeFromReadOnlyPatterns(DateIntervalInfo dii) { 407 fFallbackIntervalPattern = dii.fFallbackIntervalPattern; 408 fFirstDateInPtnIsLaterDate = dii.fFirstDateInPtnIsLaterDate; 409 fIntervalPatterns = dii.fIntervalPatterns; 410 fIntervalPatternsReadOnly = true; 411 } 412 413 414 415 /** 416 * Sink for enumerating all of the date interval skeletons. 417 */ 418 private static final class DateIntervalSink extends UResource.Sink { 419 420 /** 421 * Accepted pattern letters: 422 * Calendar.YEAR 423 * Calendar.MONTH 424 * Calendar.DATE 425 * Calendar.AM_PM 426 * Calendar.HOUR 427 * Calendar.HOUR_OF_DAY 428 * Calendar.MINUTE 429 * Calendar.SECOND 430 */ 431 private static final String ACCEPTED_PATTERN_LETTERS = "yMdahHms"; 432 433 // Output data 434 DateIntervalInfo dateIntervalInfo; 435 436 // Alias handling 437 String nextCalendarType; 438 439 // Constructor 440 public DateIntervalSink(DateIntervalInfo dateIntervalInfo) { 441 this.dateIntervalInfo = dateIntervalInfo; 442 } 443 444 @Override 445 public void put(Key key, Value value, boolean noFallback) { 446 // Iterate over all the calendar entries and only pick the 'intervalFormats' table. 447 UResource.Table dateIntervalData = value.getTable(); 448 for (int i = 0; dateIntervalData.getKeyAndValue(i, key, value); i++) { 449 if (!key.contentEquals(INTERVAL_FORMATS_KEY)) { 450 continue; 451 } 452 453 // Handle aliases and tables. Ignore the rest. 454 if (value.getType() == ICUResourceBundle.ALIAS) { 455 // Get the calendar type from the alias path. 456 nextCalendarType = getCalendarTypeFromPath(value.getAliasString()); 457 break; 458 459 } else if (value.getType() == ICUResourceBundle.TABLE) { 460 // Iterate over all the skeletons in the 'intervalFormat' table. 461 UResource.Table skeletonData = value.getTable(); 462 for (int j = 0; skeletonData.getKeyAndValue(j, key, value); j++) { 463 if (value.getType() == ICUResourceBundle.TABLE) { 464 // Process the skeleton 465 processSkeletonTable(key, value); 466 } 467 } 468 break; 469 } 470 } 471 } 472 473 /** Processes the patterns for a skeleton table. */ 474 public void processSkeletonTable(Key key, Value value) { 475 // Iterate over all the patterns in the current skeleton table 476 String currentSkeleton = key.toString(); 477 UResource.Table patternData = value.getTable(); 478 for (int k = 0; patternData.getKeyAndValue(k, key, value); k++) { 479 if (value.getType() == ICUResourceBundle.STRING) { 480 // Process the key 481 CharSequence patternLetter = validateAndProcessPatternLetter(key); 482 483 // If the calendar field has a valid value 484 if (patternLetter != null) { 485 // Get the largest different calendar unit 486 String lrgDiffCalUnit = patternLetter.toString(); 487 488 // Set the interval pattern 489 setIntervalPatternIfAbsent(currentSkeleton, lrgDiffCalUnit, value); 490 } 491 } 492 } 493 } 494 495 /** 496 * Returns and resets the next calendar type. 497 * @return Next calendar type 498 */ 499 public String getAndResetNextCalendarType() { 500 String tmpCalendarType = nextCalendarType; 501 nextCalendarType = null; 502 return tmpCalendarType; 503 } 504 505 // Alias' path prefix and suffix. 506 private static final String DATE_INTERVAL_PATH_PREFIX = 507 "/LOCALE/" + CALENDAR_KEY + "/"; 508 private static final String DATE_INTERVAL_PATH_SUFFIX = 509 "/" + INTERVAL_FORMATS_KEY; 510 511 /** 512 * Extracts the calendar type from the path 513 * @param path 514 * @return Calendar Type 515 */ 516 private String getCalendarTypeFromPath(String path) { 517 if (path.startsWith(DATE_INTERVAL_PATH_PREFIX) && 518 path.endsWith(DATE_INTERVAL_PATH_SUFFIX)) { 519 return path.substring(DATE_INTERVAL_PATH_PREFIX.length(), 520 path.length() - DATE_INTERVAL_PATH_SUFFIX.length()); 521 } 522 throw new ICUException("Malformed 'intervalFormat' alias path: " + path); 523 } 524 525 /** 526 * Processes the pattern letter 527 * @param patternLetter 528 * @return Pattern letter 529 */ 530 private CharSequence validateAndProcessPatternLetter(CharSequence patternLetter) { 531 // Check that patternLetter is just one letter 532 if (patternLetter.length() != 1) { return null; } 533 534 // Check that the pattern letter is accepted 535 char letter = patternLetter.charAt(0); 536 if (ACCEPTED_PATTERN_LETTERS.indexOf(letter) < 0) { 537 return null; 538 } 539 540 // Replace 'h' for 'H' 541 if (letter == CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR_OF_DAY].charAt(0)) { 542 patternLetter = CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR]; 543 } 544 545 return patternLetter; 546 } 547 548 /** 549 * Stores the interval pattern for the current skeleton in the internal data structure 550 * if it's not present. 551 * @param lrgDiffCalUnit 552 * @param intervalPattern 553 */ 554 private void setIntervalPatternIfAbsent(String currentSkeleton, String lrgDiffCalUnit, Value intervalPattern) { 555 // Check if the pattern has already been stored on the data structure. 556 Map<String, PatternInfo> patternsOfOneSkeleton = 557 dateIntervalInfo.fIntervalPatterns.get(currentSkeleton); 558 if (patternsOfOneSkeleton == null || !patternsOfOneSkeleton.containsKey(lrgDiffCalUnit)) { 559 // Store the pattern 560 dateIntervalInfo.setIntervalPatternInternally(currentSkeleton, lrgDiffCalUnit, 561 intervalPattern.toString()); 562 } 563 } 564 } 565 566 567 /* 568 * Initialize DateIntervalInfo from calendar data 569 * @param calData calendar data 570 */ 571 private void setup(ULocale locale) { 572 int DEFAULT_HASH_SIZE = 19; 573 fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>(DEFAULT_HASH_SIZE); 574 // initialize to guard if there is no interval date format defined in 575 // resource files 576 fFallbackIntervalPattern = "{0} \u2013 {1}"; 577 578 try { 579 // Get the correct calendar type 580 String calendarTypeToUse = locale.getKeywordValue("calendar"); 581 if ( calendarTypeToUse == null ) { 582 String[] preferredCalendarTypes = 583 Calendar.getKeywordValuesForLocale("calendar", locale, true); 584 calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar 585 } 586 if ( calendarTypeToUse == null ) { 587 calendarTypeToUse = "gregorian"; // fallback 588 } 589 590 // Instantiate the sink to process the data and the resource bundle 591 DateIntervalSink sink = new DateIntervalSink(this); 592 ICUResourceBundle resource = 593 (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); 594 595 // Get the fallback pattern 596 String fallbackPattern = resource.getStringWithFallback(CALENDAR_KEY + "/" + calendarTypeToUse 597 + "/" + INTERVAL_FORMATS_KEY + "/" + FALLBACK_STRING); 598 setFallbackIntervalPattern(fallbackPattern); 599 600 // Already loaded calendar types 601 Set<String> loadedCalendarTypes = new HashSet<String>(); 602 603 while (calendarTypeToUse != null) { 604 // Throw an exception when a loop is detected 605 if (loadedCalendarTypes.contains(calendarTypeToUse)) { 606 throw new ICUException("Loop in calendar type fallback: " + calendarTypeToUse); 607 } 608 609 // Register the calendar type to avoid loops 610 loadedCalendarTypes.add(calendarTypeToUse); 611 612 // Get all resources for this calendar type 613 String pathToIntervalFormats = CALENDAR_KEY + "/" + calendarTypeToUse; 614 resource.getAllItemsWithFallback(pathToIntervalFormats, sink); 615 616 // Get next calendar type to load if there was an alias pointing at it 617 calendarTypeToUse = sink.getAndResetNextCalendarType(); 618 } 619 } catch ( MissingResourceException e) { 620 // Will fallback to {data0} - {date1} 621 } 622 } 623 624 625 /* 626 * Split interval patterns into 2 part. 627 * @param intervalPattern interval pattern 628 * @return the index in interval pattern which split the pattern into 2 part 629 */ 630 private static int splitPatternInto2Part(String intervalPattern) { 631 boolean inQuote = false; 632 char prevCh = 0; 633 int count = 0; 634 635 /* repeatedPattern used to record whether a pattern has already seen. 636 It is a pattern applies to first calendar if it is first time seen, 637 otherwise, it is a pattern applies to the second calendar 638 */ 639 int[] patternRepeated = new int[58]; 640 641 int PATTERN_CHAR_BASE = 0x41; 642 643 /* loop through the pattern string character by character looking for 644 * the first repeated pattern letter, which breaks the interval pattern 645 * into 2 parts. 646 */ 647 int i; 648 boolean foundRepetition = false; 649 for (i = 0; i < intervalPattern.length(); ++i) { 650 char ch = intervalPattern.charAt(i); 651 652 if (ch != prevCh && count > 0) { 653 // check the repeativeness of pattern letter 654 int repeated = patternRepeated[prevCh - PATTERN_CHAR_BASE]; 655 if ( repeated == 0 ) { 656 patternRepeated[prevCh - PATTERN_CHAR_BASE] = 1; 657 } else { 658 foundRepetition = true; 659 break; 660 } 661 count = 0; 662 } 663 if (ch == '\'') { 664 // Consecutive single quotes are a single quote literal, 665 // either outside of quotes or between quotes 666 if ((i+1) < intervalPattern.length() && 667 intervalPattern.charAt(i+1) == '\'') { 668 ++i; 669 } else { 670 inQuote = ! inQuote; 671 } 672 } 673 else if (!inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/) 674 || (ch >= 0x0041 /*'A'*/ && ch <= 0x005A /*'Z'*/))) { 675 // ch is a date-time pattern character 676 prevCh = ch; 677 ++count; 678 } 679 } 680 // check last pattern char, distinguish 681 // "dd MM" ( no repetition ), 682 // "d-d"(last char repeated ), and 683 // "d-d MM" ( repetition found ) 684 if ( count > 0 && foundRepetition == false ) { 685 if ( patternRepeated[prevCh - PATTERN_CHAR_BASE] == 0 ) { 686 count = 0; 687 } 688 } 689 return (i - count); 690 } 691 692 693 /** 694 * Provides a way for client to build interval patterns. 695 * User could construct DateIntervalInfo by providing 696 * a list of skeletons and their patterns. 697 * <P> 698 * For example: 699 * <pre> 700 * DateIntervalInfo dIntervalInfo = new DateIntervalInfo(); 701 * dIntervalInfo.setIntervalPattern("yMd", Calendar.YEAR, "'from' yyyy-M-d 'to' yyyy-M-d"); 702 * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.MONTH, "'from' yyyy MMM d 'to' MMM d"); 703 * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.DAY, "yyyy MMM d-d"); 704 * dIntervalInfo.setFallbackIntervalPattern("{0} ~ {1}"); 705 * </pre> 706 * 707 * Restriction: 708 * Currently, users can only set interval patterns when the following 709 * calendar fields are different: ERA, YEAR, MONTH, DATE, DAY_OF_MONTH, 710 * DAY_OF_WEEK, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, and SECOND. 711 * Interval patterns when other calendar fields are different are 712 * not supported. 713 * 714 * @param skeleton the skeleton on which interval pattern based 715 * @param lrgDiffCalUnit the largest different calendar unit. 716 * @param intervalPattern the interval pattern on the largest different 717 * calendar unit. 718 * For example, if lrgDiffCalUnit is 719 * "year", the interval pattern for en_US when year 720 * is different could be "'from' yyyy 'to' yyyy". 721 * @throws IllegalArgumentException if setting interval pattern on 722 * a calendar field that is smaller 723 * than the MINIMUM_SUPPORTED_CALENDAR_FIELD 724 * @throws UnsupportedOperationException if the object is frozen 725 * @stable ICU 4.0 726 */ 727 public void setIntervalPattern(String skeleton, 728 int lrgDiffCalUnit, 729 String intervalPattern) 730 { 731 if ( frozen ) { 732 throw new UnsupportedOperationException("no modification is allowed after DII is frozen"); 733 } 734 if ( lrgDiffCalUnit > MINIMUM_SUPPORTED_CALENDAR_FIELD ) { 735 throw new IllegalArgumentException("calendar field is larger than MINIMUM_SUPPORTED_CALENDAR_FIELD"); 736 } 737 if (fIntervalPatternsReadOnly) { 738 fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns); 739 fIntervalPatternsReadOnly = false; 740 } 741 PatternInfo ptnInfo = setIntervalPatternInternally(skeleton, 742 CALENDAR_FIELD_TO_PATTERN_LETTER[lrgDiffCalUnit], 743 intervalPattern); 744 if ( lrgDiffCalUnit == Calendar.HOUR_OF_DAY ) { 745 setIntervalPattern(skeleton, 746 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM], 747 ptnInfo); 748 setIntervalPattern(skeleton, 749 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR], 750 ptnInfo); 751 } else if ( lrgDiffCalUnit == Calendar.DAY_OF_MONTH || 752 lrgDiffCalUnit == Calendar.DAY_OF_WEEK ) { 753 setIntervalPattern(skeleton, 754 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE], 755 ptnInfo); 756 } 757 } 758 759 760 /* Set Interval pattern. 761 * 762 * It generates the interval pattern info, 763 * afer which, not only sets the interval pattern info into the hash map, 764 * but also returns the interval pattern info to the caller 765 * so that caller can re-use it. 766 * 767 * @param skeleton skeleton on which the interval pattern based 768 * @param lrgDiffCalUnit the largest different calendar unit. 769 * @param intervalPattern the interval pattern on the largest different 770 * calendar unit. 771 * @return the interval pattern pattern information 772 */ 773 private PatternInfo setIntervalPatternInternally(String skeleton, 774 String lrgDiffCalUnit, 775 String intervalPattern) { 776 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton); 777 boolean emptyHash = false; 778 if (patternsOfOneSkeleton == null) { 779 patternsOfOneSkeleton = new HashMap<String, PatternInfo>(); 780 emptyHash = true; 781 } 782 boolean order = fFirstDateInPtnIsLaterDate; 783 // check for "latestFirst:" or "earliestFirst:" prefix 784 if ( intervalPattern.startsWith(LATEST_FIRST_PREFIX) ) { 785 order = true; 786 int prefixLength = LATEST_FIRST_PREFIX.length(); 787 intervalPattern = intervalPattern.substring(prefixLength, intervalPattern.length()); 788 } else if ( intervalPattern.startsWith(EARLIEST_FIRST_PREFIX) ) { 789 order = false; 790 int earliestFirstLength = EARLIEST_FIRST_PREFIX.length(); 791 intervalPattern = intervalPattern.substring(earliestFirstLength, intervalPattern.length()); 792 } 793 PatternInfo itvPtnInfo = genPatternInfo(intervalPattern, order); 794 795 patternsOfOneSkeleton.put(lrgDiffCalUnit, itvPtnInfo); 796 if ( emptyHash == true ) { 797 fIntervalPatterns.put(skeleton, patternsOfOneSkeleton); 798 } 799 800 return itvPtnInfo; 801 } 802 803 804 /* Set Interval pattern. 805 * 806 * @param skeleton skeleton on which the interval pattern based 807 * @param lrgDiffCalUnit the largest different calendar unit. 808 * @param ptnInfo interval pattern infomration 809 */ 810 private void setIntervalPattern(String skeleton, 811 String lrgDiffCalUnit, 812 PatternInfo ptnInfo) { 813 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton); 814 patternsOfOneSkeleton.put(lrgDiffCalUnit, ptnInfo); 815 } 816 817 818 /** 819 * Break interval patterns as 2 part and save them into pattern info. 820 * @param intervalPattern interval pattern 821 * @param laterDateFirst whether the first date in intervalPattern 822 * is earlier date or later date 823 * @return pattern info object 824 * @internal 825 * @deprecated This API is ICU internal only. 826 */ 827 @Deprecated 828 public static PatternInfo genPatternInfo(String intervalPattern, 829 boolean laterDateFirst) { 830 int splitPoint = splitPatternInto2Part(intervalPattern); 831 832 String firstPart = intervalPattern.substring(0, splitPoint); 833 String secondPart = null; 834 if ( splitPoint < intervalPattern.length() ) { 835 secondPart = intervalPattern.substring(splitPoint, intervalPattern.length()); 836 } 837 838 return new PatternInfo(firstPart, secondPart, laterDateFirst); 839 } 840 841 842 /** 843 * Get the interval pattern given the largest different calendar field. 844 * @param skeleton the skeleton 845 * @param field the largest different calendar field 846 * @return interval pattern return null if interval pattern is not found. 847 * @throws IllegalArgumentException if getting interval pattern on 848 * a calendar field that is smaller 849 * than the MINIMUM_SUPPORTED_CALENDAR_FIELD 850 * @stable ICU 4.0 851 */ 852 public PatternInfo getIntervalPattern(String skeleton, int field) 853 { 854 if ( field > MINIMUM_SUPPORTED_CALENDAR_FIELD ) { 855 throw new IllegalArgumentException("no support for field less than SECOND"); 856 } 857 Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton); 858 if ( patternsOfOneSkeleton != null ) { 859 PatternInfo intervalPattern = patternsOfOneSkeleton. 860 get(CALENDAR_FIELD_TO_PATTERN_LETTER[field]); 861 if ( intervalPattern != null ) { 862 return intervalPattern; 863 } 864 } 865 return null; 866 } 867 868 869 870 /** 871 * Get the fallback interval pattern. 872 * @return fallback interval pattern 873 * @stable ICU 4.0 874 */ 875 public String getFallbackIntervalPattern() 876 { 877 return fFallbackIntervalPattern; 878 } 879 880 881 /** 882 * Re-set the fallback interval pattern. 883 * 884 * In construction, default fallback pattern is set as "{0} - {1}". 885 * And constructor taking locale as parameter will set the 886 * fallback pattern as what defined in the locale resource file. 887 * 888 * This method provides a way for user to replace the fallback pattern. 889 * 890 * @param fallbackPattern fall-back interval pattern. 891 * @throws UnsupportedOperationException if the object is frozen 892 * @throws IllegalArgumentException if there is no pattern {0} or 893 * pattern {1} in fallbakckPattern 894 * 895 * @stable ICU 4.0 896 */ 897 public void setFallbackIntervalPattern(String fallbackPattern) 898 { 899 if ( frozen ) { 900 throw new UnsupportedOperationException("no modification is allowed after DII is frozen"); 901 } 902 int firstPatternIndex = fallbackPattern.indexOf("{0}"); 903 int secondPatternIndex = fallbackPattern.indexOf("{1}"); 904 if ( firstPatternIndex == -1 || secondPatternIndex == -1 ) { 905 throw new IllegalArgumentException("no pattern {0} or pattern {1} in fallbackPattern"); 906 } 907 if ( firstPatternIndex > secondPatternIndex ) { 908 fFirstDateInPtnIsLaterDate = true; 909 } 910 fFallbackIntervalPattern = fallbackPattern; 911 } 912 913 914 /** 915 * Get default order -- whether the first date in pattern is later date 916 * or not. 917 * 918 * return default date ordering in interval pattern. TRUE if the first date 919 * in pattern is later date, FALSE otherwise. 920 * @stable ICU 4.0 921 */ 922 public boolean getDefaultOrder() 923 { 924 return fFirstDateInPtnIsLaterDate; 925 } 926 927 928 /** 929 * Clone this object. 930 * @return a copy of the object 931 * @stable ICU4.0 932 */ 933 @Override 934 public Object clone() 935 { 936 if ( frozen ) { 937 return this; 938 } 939 return cloneUnfrozenDII(); 940 } 941 942 943 /* 944 * Clone an unfrozen DateIntervalInfo object. 945 * @return a copy of the object 946 */ 947 private Object cloneUnfrozenDII() //throws IllegalStateException 948 { 949 try { 950 DateIntervalInfo other = (DateIntervalInfo) super.clone(); 951 other.fFallbackIntervalPattern=fFallbackIntervalPattern; 952 other.fFirstDateInPtnIsLaterDate = fFirstDateInPtnIsLaterDate; 953 if (fIntervalPatternsReadOnly) { 954 other.fIntervalPatterns = fIntervalPatterns; 955 other.fIntervalPatternsReadOnly = true; 956 } else { 957 other.fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns); 958 other.fIntervalPatternsReadOnly = false; 959 } 960 other.frozen = false; 961 return other; 962 } catch ( CloneNotSupportedException e ) { 963 ///CLOVER:OFF 964 throw new ICUCloneNotSupportedException("clone is not supported", e); 965 ///CLOVER:ON 966 } 967 } 968 969 private static Map<String, Map<String, PatternInfo>> cloneIntervalPatterns( 970 Map<String, Map<String, PatternInfo>> patterns) { 971 Map<String, Map<String, PatternInfo>> result = new HashMap<String, Map<String, PatternInfo>>(); 972 for (Entry<String, Map<String, PatternInfo>> skeletonEntry : patterns.entrySet()) { 973 String skeleton = skeletonEntry.getKey(); 974 Map<String, PatternInfo> patternsOfOneSkeleton = skeletonEntry.getValue(); 975 Map<String, PatternInfo> oneSetPtn = new HashMap<String, PatternInfo>(); 976 for (Entry<String, PatternInfo> calEntry : patternsOfOneSkeleton.entrySet()) { 977 String calField = calEntry.getKey(); 978 PatternInfo value = calEntry.getValue(); 979 oneSetPtn.put(calField, value); 980 } 981 result.put(skeleton, oneSetPtn); 982 } 983 return result; 984 } 985 986 987 988 /** 989 * {@inheritDoc} 990 * 991 * @stable ICU 4.0 992 */ 993 @Override 994 public boolean isFrozen() { 995 return frozen; 996 } 997 998 /** 999 * {@inheritDoc} 1000 * 1001 * @stable ICU 4.4 1002 */ 1003 @Override 1004 public DateIntervalInfo freeze() { 1005 fIntervalPatternsReadOnly = true; 1006 frozen = true; 1007 return this; 1008 } 1009 1010 /** 1011 * {@inheritDoc} 1012 * 1013 * @stable ICU 4.4 1014 */ 1015 @Override 1016 public DateIntervalInfo cloneAsThawed() { 1017 DateIntervalInfo result = (DateIntervalInfo) (this.cloneUnfrozenDII()); 1018 return result; 1019 } 1020 1021 1022 /** 1023 * Parse skeleton, save each field's width. 1024 * It is used for looking for best match skeleton, 1025 * and adjust pattern field width. 1026 * @param skeleton skeleton to be parsed 1027 * @param skeletonFieldWidth parsed skeleton field width 1028 */ 1029 static void parseSkeleton(String skeleton, int[] skeletonFieldWidth) { 1030 int PATTERN_CHAR_BASE = 0x41; 1031 for ( int i = 0; i < skeleton.length(); ++i ) { 1032 ++skeletonFieldWidth[skeleton.charAt(i) - PATTERN_CHAR_BASE]; 1033 } 1034 } 1035 1036 1037 1038 /* 1039 * Check whether one field width is numeric while the other is string. 1040 * 1041 * TODO (xji): make it general 1042 * 1043 * @param fieldWidth one field width 1044 * @param anotherFieldWidth another field width 1045 * @param patternLetter pattern letter char 1046 * @return true if one field width is numeric and the other is string, 1047 * false otherwise. 1048 */ 1049 private static boolean stringNumeric(int fieldWidth, 1050 int anotherFieldWidth, 1051 char patternLetter) { 1052 if ( patternLetter == 'M' ) { 1053 if ( fieldWidth <= 2 && anotherFieldWidth > 2 || 1054 fieldWidth > 2 && anotherFieldWidth <= 2 ) { 1055 return true; 1056 } 1057 } 1058 return false; 1059 } 1060 1061 1062 /* 1063 * given an input skeleton, get the best match skeleton 1064 * which has pre-defined interval pattern in resource file. 1065 * 1066 * TODO (xji): set field weight or 1067 * isolate the funtionality in DateTimePatternGenerator 1068 * @param inputSkeleton input skeleton 1069 * @return 0, if there is exact match for input skeleton 1070 * 1, if there is only field width difference between 1071 * the best match and the input skeleton 1072 * 2, the only field difference is 'v' and 'z' 1073 * -1, if there is calendar field difference between 1074 * the best match and the input skeleton 1075 */ 1076 DateIntervalFormat.BestMatchInfo getBestSkeleton(String inputSkeleton) { 1077 String bestSkeleton = inputSkeleton; 1078 int[] inputSkeletonFieldWidth = new int[58]; 1079 int[] skeletonFieldWidth = new int[58]; 1080 1081 final int DIFFERENT_FIELD = 0x1000; 1082 final int STRING_NUMERIC_DIFFERENCE = 0x100; 1083 final int BASE = 0x41; 1084 1085 // TODO: this is a hack for 'v' and 'z' 1086 // resource bundle only have time skeletons ending with 'v', 1087 // but not for time skeletons ending with 'z'. 1088 boolean replaceZWithV = false; 1089 if ( inputSkeleton.indexOf('z') != -1 ) { 1090 inputSkeleton = inputSkeleton.replace('z', 'v'); 1091 replaceZWithV = true; 1092 } 1093 1094 parseSkeleton(inputSkeleton, inputSkeletonFieldWidth); 1095 int bestDistance = Integer.MAX_VALUE; 1096 // 0 means exact the same skeletons; 1097 // 1 means having the same field, but with different length, 1098 // 2 means only z/v differs 1099 // -1 means having different field. 1100 int bestFieldDifference = 0; 1101 for (String skeleton : fIntervalPatterns.keySet()) { 1102 // clear skeleton field width 1103 for ( int i = 0; i < skeletonFieldWidth.length; ++i ) { 1104 skeletonFieldWidth[i] = 0; 1105 } 1106 parseSkeleton(skeleton, skeletonFieldWidth); 1107 // calculate distance 1108 int distance = 0; 1109 int fieldDifference = 1; 1110 for ( int i = 0; i < inputSkeletonFieldWidth.length; ++i ) { 1111 int inputFieldWidth = inputSkeletonFieldWidth[i]; 1112 int fieldWidth = skeletonFieldWidth[i]; 1113 if ( inputFieldWidth == fieldWidth ) { 1114 continue; 1115 } 1116 if ( inputFieldWidth == 0 ) { 1117 fieldDifference = -1; 1118 distance += DIFFERENT_FIELD; 1119 } else if ( fieldWidth == 0 ) { 1120 fieldDifference = -1; 1121 distance += DIFFERENT_FIELD; 1122 } else if (stringNumeric(inputFieldWidth, fieldWidth, 1123 (char)(i+BASE) ) ) { 1124 distance += STRING_NUMERIC_DIFFERENCE; 1125 } else { 1126 distance += Math.abs(inputFieldWidth - fieldWidth); 1127 } 1128 } 1129 if ( distance < bestDistance ) { 1130 bestSkeleton = skeleton; 1131 bestDistance = distance; 1132 bestFieldDifference = fieldDifference; 1133 } 1134 if ( distance == 0 ) { 1135 bestFieldDifference = 0; 1136 break; 1137 } 1138 } 1139 if ( replaceZWithV && bestFieldDifference != -1 ) { 1140 bestFieldDifference = 2; 1141 } 1142 return new DateIntervalFormat.BestMatchInfo(bestSkeleton, bestFieldDifference); 1143 } 1144 1145 /** 1146 * Override equals 1147 * @stable ICU 4.0 1148 */ 1149 @Override 1150 public boolean equals(Object a) { 1151 if ( a instanceof DateIntervalInfo ) { 1152 DateIntervalInfo dtInfo = (DateIntervalInfo)a; 1153 return fIntervalPatterns.equals(dtInfo.fIntervalPatterns); 1154 } 1155 return false; 1156 } 1157 1158 /** 1159 * Override hashcode 1160 * @stable ICU 4.0 1161 */ 1162 @Override 1163 public int hashCode() { 1164 return fIntervalPatterns.hashCode(); 1165 } 1166 1167 /** 1168 * @internal CLDR 1169 * @deprecated This API is ICU internal only. 1170 */ 1171 @Deprecated 1172 public Map<String,Set<String>> getPatterns() { 1173 LinkedHashMap<String,Set<String>> result = new LinkedHashMap<String,Set<String>>(); 1174 for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) { 1175 result.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue().keySet())); 1176 } 1177 return result; 1178 } 1179 1180 /** 1181 * Get the internal patterns, with a deep clone for safety. 1182 * @internal CLDR 1183 * @deprecated This API is ICU internal only. 1184 */ 1185 @Deprecated 1186 public Map<String, Map<String, PatternInfo>> getRawPatterns() { 1187 LinkedHashMap<String, Map<String, PatternInfo>> result = new LinkedHashMap<String, Map<String, PatternInfo>>(); 1188 for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) { 1189 result.put(entry.getKey(), new LinkedHashMap<String, PatternInfo>(entry.getValue())); 1190 } 1191 return result; 1192 } 1193}// end class DateIntervalInfo 1194