1/* 2 ******************************************************************************* 3 * Copyright (C) 2013-2016, International Business Machines Corporation and 4 * others. All Rights Reserved. 5 ******************************************************************************* 6 */ 7package com.ibm.icu.text; 8 9import java.util.EnumMap; 10import java.util.Locale; 11 12import com.ibm.icu.impl.CalendarData; 13import com.ibm.icu.impl.DontCareFieldPosition; 14import com.ibm.icu.impl.ICUCache; 15import com.ibm.icu.impl.ICUResourceBundle; 16import com.ibm.icu.impl.SimpleCache; 17import com.ibm.icu.impl.SimplePatternFormatter; 18import com.ibm.icu.impl.StandardPlural; 19import com.ibm.icu.lang.UCharacter; 20import com.ibm.icu.util.Calendar; 21import com.ibm.icu.util.ICUException; 22import com.ibm.icu.util.ULocale; 23import com.ibm.icu.impl.UResource; 24import com.ibm.icu.util.UResourceBundle; 25 26 27/** 28 * Formats simple relative dates. There are two types of relative dates that 29 * it handles: 30 * <ul> 31 * <li>relative dates with a quantity e.g "in 5 days"</li> 32 * <li>relative dates without a quantity e.g "next Tuesday"</li> 33 * </ul> 34 * <p> 35 * This API is very basic and is intended to be a building block for more 36 * fancy APIs. The caller tells it exactly what to display in a locale 37 * independent way. While this class automatically provides the correct plural 38 * forms, the grammatical form is otherwise as neutral as possible. It is the 39 * caller's responsibility to handle cut-off logic such as deciding between 40 * displaying "in 7 days" or "in 1 week." This API supports relative dates 41 * involving one single unit. This API does not support relative dates 42 * involving compound units. 43 * e.g "in 5 days and 4 hours" nor does it support parsing. 44 * This class is both immutable and thread-safe. 45 * <p> 46 * Here are some examples of use: 47 * <blockquote> 48 * <pre> 49 * RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(); 50 * fmt.format(1, Direction.NEXT, RelativeUnit.DAYS); // "in 1 day" 51 * fmt.format(3, Direction.NEXT, RelativeUnit.DAYS); // "in 3 days" 52 * fmt.format(3.2, Direction.LAST, RelativeUnit.YEARS); // "3.2 years ago" 53 * 54 * fmt.format(Direction.LAST, AbsoluteUnit.SUNDAY); // "last Sunday" 55 * fmt.format(Direction.THIS, AbsoluteUnit.SUNDAY); // "this Sunday" 56 * fmt.format(Direction.NEXT, AbsoluteUnit.SUNDAY); // "next Sunday" 57 * fmt.format(Direction.PLAIN, AbsoluteUnit.SUNDAY); // "Sunday" 58 * 59 * fmt.format(Direction.LAST, AbsoluteUnit.DAY); // "yesterday" 60 * fmt.format(Direction.THIS, AbsoluteUnit.DAY); // "today" 61 * fmt.format(Direction.NEXT, AbsoluteUnit.DAY); // "tomorrow" 62 * 63 * fmt.format(Direction.PLAIN, AbsoluteUnit.NOW); // "now" 64 * </pre> 65 * </blockquote> 66 * <p> 67 * In the future, we may add more forms, such as abbreviated/short forms 68 * (3 secs ago), and relative day periods ("yesterday afternoon"), etc. 69 * 70 * @stable ICU 53 71 */ 72public final class RelativeDateTimeFormatter { 73 74 /** 75 * The formatting style 76 * @stable ICU 54 77 * 78 */ 79 public static enum Style { 80 81 /** 82 * Everything spelled out. 83 * @stable ICU 54 84 */ 85 LONG, 86 87 /** 88 * Abbreviations used when possible. 89 * @stable ICU 54 90 */ 91 SHORT, 92 93 /** 94 * Use single letters when possible. 95 * @stable ICU 54 96 */ 97 NARROW; 98 99 private static final int INDEX_COUNT = 3; // NARROW.ordinal() + 1 100 } 101 102 /** 103 * Represents the unit for formatting a relative date. e.g "in 5 days" 104 * or "in 3 months" 105 * @stable ICU 53 106 */ 107 public static enum RelativeUnit { 108 109 /** 110 * Seconds 111 * @stable ICU 53 112 */ 113 SECONDS, 114 115 /** 116 * Minutes 117 * @stable ICU 53 118 */ 119 MINUTES, 120 121 /** 122 * Hours 123 * @stable ICU 53 124 */ 125 HOURS, 126 127 /** 128 * Days 129 * @stable ICU 53 130 */ 131 DAYS, 132 133 /** 134 * Weeks 135 * @stable ICU 53 136 */ 137 WEEKS, 138 139 /** 140 * Months 141 * @stable ICU 53 142 */ 143 MONTHS, 144 145 /** 146 * Years 147 * @stable ICU 53 148 */ 149 YEARS, 150 151 /** 152 * Quarters 153 * @internal TODO: propose for addition in ICU 57 154 * @deprecated This API is ICU internal only. 155 */ 156 @Deprecated 157 QUARTERS, 158 } 159 160 /** 161 * Represents an absolute unit. 162 * @stable ICU 53 163 */ 164 public static enum AbsoluteUnit { 165 166 /** 167 * Sunday 168 * @stable ICU 53 169 */ 170 SUNDAY, 171 172 /** 173 * Monday 174 * @stable ICU 53 175 */ 176 MONDAY, 177 178 /** 179 * Tuesday 180 * @stable ICU 53 181 */ 182 TUESDAY, 183 184 /** 185 * Wednesday 186 * @stable ICU 53 187 */ 188 WEDNESDAY, 189 190 /** 191 * Thursday 192 * @stable ICU 53 193 */ 194 THURSDAY, 195 196 /** 197 * Friday 198 * @stable ICU 53 199 */ 200 FRIDAY, 201 202 /** 203 * Saturday 204 * @stable ICU 53 205 */ 206 SATURDAY, 207 208 /** 209 * Day 210 * @stable ICU 53 211 */ 212 DAY, 213 214 /** 215 * Week 216 * @stable ICU 53 217 */ 218 WEEK, 219 220 /** 221 * Month 222 * @stable ICU 53 223 */ 224 MONTH, 225 226 /** 227 * Year 228 * @stable ICU 53 229 */ 230 YEAR, 231 232 /** 233 * Now 234 * @stable ICU 53 235 */ 236 NOW, 237 238 /** 239 * Quarter 240 * @internal TODO: propose for addition in ICU 57 241 * @deprecated This API is ICU internal only. 242 */ 243 @Deprecated 244 QUARTER, 245 } 246 247 /** 248 * Represents a direction for an absolute unit e.g "Next Tuesday" 249 * or "Last Tuesday" 250 * @stable ICU 53 251 */ 252 public static enum Direction { 253 /** 254 * Two before. Not fully supported in every locale 255 * @stable ICU 53 256 */ 257 LAST_2, 258 259 /** 260 * Last 261 * @stable ICU 53 262 */ 263 LAST, 264 265 /** 266 * This 267 * @stable ICU 53 268 */ 269 THIS, 270 271 /** 272 * Next 273 * @stable ICU 53 274 */ 275 NEXT, 276 277 /** 278 * Two after. Not fully supported in every locale 279 * @stable ICU 53 280 */ 281 NEXT_2, 282 283 /** 284 * Plain, which means the absence of a qualifier 285 * @stable ICU 53 286 */ 287 PLAIN; 288 } 289 290 /** 291 * Returns a RelativeDateTimeFormatter for the default locale. 292 * @stable ICU 53 293 */ 294 public static RelativeDateTimeFormatter getInstance() { 295 return getInstance(ULocale.getDefault(), null, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 296 } 297 298 /** 299 * Returns a RelativeDateTimeFormatter for a particular locale. 300 * 301 * @param locale the locale. 302 * @return An instance of RelativeDateTimeFormatter. 303 * @stable ICU 53 304 */ 305 public static RelativeDateTimeFormatter getInstance(ULocale locale) { 306 return getInstance(locale, null, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 307 } 308 309 /** 310 * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale}. 311 * 312 * @param locale the {@link java.util.Locale}. 313 * @return An instance of RelativeDateTimeFormatter. 314 * @stable ICU 54 315 */ 316 public static RelativeDateTimeFormatter getInstance(Locale locale) { 317 return getInstance(ULocale.forLocale(locale)); 318 } 319 320 /** 321 * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular 322 * NumberFormat object. 323 * 324 * @param locale the locale 325 * @param nf the number format object. It is defensively copied to ensure thread-safety 326 * and immutability of this class. 327 * @return An instance of RelativeDateTimeFormatter. 328 * @stable ICU 53 329 */ 330 public static RelativeDateTimeFormatter getInstance(ULocale locale, NumberFormat nf) { 331 return getInstance(locale, nf, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 332 } 333 334 /** 335 * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular 336 * NumberFormat object, style, and capitalization context 337 * 338 * @param locale the locale 339 * @param nf the number format object. It is defensively copied to ensure thread-safety 340 * and immutability of this class. May be null. 341 * @param style the style. 342 * @param capitalizationContext the capitalization context. 343 * @stable ICU 54 344 */ 345 public static RelativeDateTimeFormatter getInstance( 346 ULocale locale, 347 NumberFormat nf, 348 Style style, 349 DisplayContext capitalizationContext) { 350 RelativeDateTimeFormatterData data = cache.get(locale); 351 if (nf == null) { 352 nf = NumberFormat.getInstance(locale); 353 } else { 354 nf = (NumberFormat) nf.clone(); 355 } 356 return new RelativeDateTimeFormatter( 357 data.qualitativeUnitMap, 358 data.relUnitPatternMap, 359 new MessageFormat(data.dateTimePattern), 360 PluralRules.forLocale(locale), 361 nf, 362 style, 363 capitalizationContext, 364 capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ? 365 BreakIterator.getSentenceInstance(locale) : null, 366 locale); 367 } 368 369 /** 370 * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale} that uses a 371 * particular NumberFormat object. 372 * 373 * @param locale the {@link java.util.Locale} 374 * @param nf the number format object. It is defensively copied to ensure thread-safety 375 * and immutability of this class. 376 * @return An instance of RelativeDateTimeFormatter. 377 * @stable ICU 54 378 */ 379 public static RelativeDateTimeFormatter getInstance(Locale locale, NumberFormat nf) { 380 return getInstance(ULocale.forLocale(locale), nf); 381 } 382 383 /** 384 * Formats a relative date with a quantity such as "in 5 days" or 385 * "3 months ago" 386 * @param quantity The numerical amount e.g 5. This value is formatted 387 * according to this object's {@link NumberFormat} object. 388 * @param direction NEXT means a future relative date; LAST means a past 389 * relative date. 390 * @param unit the unit e.g day? month? year? 391 * @return the formatted string 392 * @throws IllegalArgumentException if direction is something other than 393 * NEXT or LAST. 394 * @stable ICU 53 395 */ 396 public String format(double quantity, Direction direction, RelativeUnit unit) { 397 if (direction != Direction.LAST && direction != Direction.NEXT) { 398 throw new IllegalArgumentException("direction must be NEXT or LAST"); 399 } 400 String result; 401 int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0); 402 403 // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this 404 // class we must guarantee that only one thread at a time uses our numberFormat. 405 synchronized (numberFormat) { 406 StringBuffer formatStr = new StringBuffer(); 407 DontCareFieldPosition fieldPosition = DontCareFieldPosition.INSTANCE; 408 StandardPlural pluralForm = QuantityFormatter.selectPlural(quantity, 409 numberFormat, pluralRules, formatStr, fieldPosition); 410 411 String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); 412 result = SimplePatternFormatter.formatCompiledPattern(formatter, formatStr); 413 } 414 return adjustForContext(result); 415 416 } 417 418 private int[] styleToDateFormatSymbolsWidth = { 419 DateFormatSymbols.WIDE, DateFormatSymbols.SHORT, DateFormatSymbols.NARROW 420 }; 421 422 /** 423 * Formats a relative date without a quantity. 424 * @param direction NEXT, LAST, THIS, etc. 425 * @param unit e.g SATURDAY, DAY, MONTH 426 * @return the formatted string. If direction has a value that is documented as not being 427 * fully supported in every locale (for example NEXT_2 or LAST_2) then this function may 428 * return null to signal that no formatted string is available. 429 * @throws IllegalArgumentException if the direction is incompatible with 430 * unit this can occur with NOW which can only take PLAIN. 431 * @stable ICU 53 432 */ 433 public String format(Direction direction, AbsoluteUnit unit) { 434 if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) { 435 throw new IllegalArgumentException("NOW can only accept direction PLAIN."); 436 } 437 String result; 438 // Get plain day of week names from DateFormatSymbols. 439 if ((direction == Direction.PLAIN) && (AbsoluteUnit.SUNDAY.ordinal() <= unit.ordinal() && 440 unit.ordinal() <= AbsoluteUnit.SATURDAY.ordinal())) { 441 // Convert from AbsoluteUnit days to Calendar class indexing. 442 int dateSymbolsDayOrdinal = (unit.ordinal() - AbsoluteUnit.SUNDAY.ordinal()) + Calendar.SUNDAY; 443 String[] dayNames = 444 dateFormatSymbols.getWeekdays(DateFormatSymbols.STANDALONE, 445 styleToDateFormatSymbolsWidth[style.ordinal()]); 446 result = dayNames[dateSymbolsDayOrdinal]; 447 } else { 448 // Not PLAIN, or not a weekday. 449 result = getAbsoluteUnitString(style, unit, direction); 450 } 451 return result != null ? adjustForContext(result) : null; 452 } 453 454 /** 455 * Gets the string value from qualitativeUnitMap with fallback based on style. 456 * @param style 457 * @param unit 458 * @param direction 459 * @return 460 */ 461 private String getAbsoluteUnitString(Style style, AbsoluteUnit unit, Direction direction) { 462 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap; 463 EnumMap<Direction, String> dirMap; 464 465 do { 466 unitMap = qualitativeUnitMap.get(style); 467 if (unitMap != null) { 468 dirMap = unitMap.get(unit); 469 if (dirMap != null) { 470 String result = dirMap.get(direction); 471 if (result != null) { 472 return result; 473 } 474 } 475 476 } 477 478 // Consider other styles from alias fallback. 479 // Data loading guaranteed no endless loops. 480 } while ((style = fallbackCache[style.ordinal()]) != null); 481 return null; 482 } 483 484 /** 485 * Combines a relative date string and a time string in this object's 486 * locale. This is done with the same date-time separator used for the 487 * default calendar in this locale. 488 * @param relativeDateString the relative date e.g 'yesterday' 489 * @param timeString the time e.g '3:45' 490 * @return the date and time concatenated according to the default 491 * calendar in this locale e.g 'yesterday, 3:45' 492 * @stable ICU 53 493 */ 494 public String combineDateAndTime(String relativeDateString, String timeString) { 495 return this.combinedDateAndTime.format( 496 new Object[]{timeString, relativeDateString}, new StringBuffer(), null).toString(); 497 } 498 499 /** 500 * Returns a copy of the NumberFormat this object is using. 501 * @return A copy of the NumberFormat. 502 * @stable ICU 53 503 */ 504 public NumberFormat getNumberFormat() { 505 // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this 506 // class we must guarantee that only one thread at a time uses our numberFormat. 507 synchronized (numberFormat) { 508 return (NumberFormat) numberFormat.clone(); 509 } 510 } 511 512 /** 513 * Return capitalization context. 514 * 515 * @stable ICU 54 516 */ 517 public DisplayContext getCapitalizationContext() { 518 return capitalizationContext; 519 } 520 521 /** 522 * Return style 523 * 524 * @stable ICU 54 525 */ 526 public Style getFormatStyle() { 527 return style; 528 } 529 530 private String adjustForContext(String originalFormattedString) { 531 if (breakIterator == null || originalFormattedString.length() == 0 532 || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) { 533 return originalFormattedString; 534 } 535 synchronized (breakIterator) { 536 return UCharacter.toTitleCase( 537 locale, 538 originalFormattedString, 539 breakIterator, 540 UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT); 541 } 542 } 543 544 private RelativeDateTimeFormatter( 545 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap, 546 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap, 547 MessageFormat combinedDateAndTime, 548 PluralRules pluralRules, 549 NumberFormat numberFormat, 550 Style style, 551 DisplayContext capitalizationContext, 552 BreakIterator breakIterator, 553 ULocale locale) { 554 this.qualitativeUnitMap = qualitativeUnitMap; 555 this.patternMap = patternMap; 556 this.combinedDateAndTime = combinedDateAndTime; 557 this.pluralRules = pluralRules; 558 this.numberFormat = numberFormat; 559 this.style = style; 560 if (capitalizationContext.type() != DisplayContext.Type.CAPITALIZATION) { 561 throw new IllegalArgumentException(capitalizationContext.toString()); 562 } 563 this.capitalizationContext = capitalizationContext; 564 this.breakIterator = breakIterator; 565 this.locale = locale; 566 this.dateFormatSymbols = new DateFormatSymbols(locale); 567 } 568 569 private String getRelativeUnitPluralPattern( 570 Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) { 571 if (pluralForm != StandardPlural.OTHER) { 572 String formatter = getRelativeUnitPattern(style, unit, pastFutureIndex, pluralForm); 573 if (formatter != null) { 574 return formatter; 575 } 576 } 577 return getRelativeUnitPattern(style, unit, pastFutureIndex, StandardPlural.OTHER); 578 } 579 580 private String getRelativeUnitPattern( 581 Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) { 582 int pluralIndex = pluralForm.ordinal(); 583 do { 584 EnumMap<RelativeUnit, String[][]> unitMap = patternMap.get(style); 585 if (unitMap != null) { 586 String[][] spfCompiledPatterns = unitMap.get(unit); 587 if (spfCompiledPatterns != null) { 588 if (spfCompiledPatterns[pastFutureIndex][pluralIndex] != null) { 589 return spfCompiledPatterns[pastFutureIndex][pluralIndex]; 590 } 591 } 592 593 } 594 595 // Consider other styles from alias fallback. 596 // Data loading guaranteed no endless loops. 597 } while ((style = fallbackCache[style.ordinal()]) != null); 598 return null; 599 } 600 601 private final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap; 602 private final EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap; 603 604 private final MessageFormat combinedDateAndTime; 605 private final PluralRules pluralRules; 606 private final NumberFormat numberFormat; 607 608 private final Style style; 609 private final DisplayContext capitalizationContext; 610 private final BreakIterator breakIterator; 611 private final ULocale locale; 612 613 private final DateFormatSymbols dateFormatSymbols; 614 615 private static final Style fallbackCache[] = new Style[Style.INDEX_COUNT]; 616 617 private static class RelativeDateTimeFormatterData { 618 public RelativeDateTimeFormatterData( 619 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap, 620 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap, 621 String dateTimePattern) { 622 this.qualitativeUnitMap = qualitativeUnitMap; 623 this.relUnitPatternMap = relUnitPatternMap; 624 625 this.dateTimePattern = dateTimePattern; 626 } 627 628 public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap; 629 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap; 630 public final String dateTimePattern; // Example: "{1}, {0}" 631 } 632 633 private static class Cache { 634 private final ICUCache<String, RelativeDateTimeFormatterData> cache = 635 new SimpleCache<String, RelativeDateTimeFormatterData>(); 636 637 public RelativeDateTimeFormatterData get(ULocale locale) { 638 String key = locale.toString(); 639 RelativeDateTimeFormatterData result = cache.get(key); 640 if (result == null) { 641 result = new Loader(locale).load(); 642 cache.put(key, result); 643 } 644 return result; 645 } 646 } 647 648 private static Direction keyToDirection(UResource.Key key) { 649 if (key.contentEquals("-2")) { 650 return Direction.LAST_2; 651 } 652 if (key.contentEquals("-1")) { 653 return Direction.LAST; 654 } 655 if (key.contentEquals("0")) { 656 return Direction.THIS; 657 } 658 if (key.contentEquals("1")) { 659 return Direction.NEXT; 660 } 661 if (key.contentEquals("2")) { 662 return Direction.NEXT_2; 663 } 664 return null; 665 } 666 667 /** 668 * Sink for enumerating all of the relative data time formatter names. 669 * Contains inner sink classes, each one corresponding to a type of resource table. 670 * The outer sink handles the top-level 'fields'. 671 * 672 * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root): 673 * Only store a value if it is still missing, that is, it has not been overridden. 674 * 675 * C++: Each inner sink class has a reference to the main outer sink. 676 * Java: Use non-static inner classes instead. 677 */ 678 private static final class RelDateTimeFmtDataSink extends UResource.TableSink { 679 // For white list of units to handle in RelativeDateTimeFormatter. 680 private static enum DateTimeUnit { 681 SECOND(RelativeUnit.SECONDS, null), 682 MINUTE(RelativeUnit.MINUTES, null), 683 HOUR(RelativeUnit.HOURS, null), 684 DAY(RelativeUnit.DAYS, AbsoluteUnit.DAY), 685 WEEK(RelativeUnit.WEEKS, AbsoluteUnit.WEEK), 686 MONTH(RelativeUnit.MONTHS, AbsoluteUnit.MONTH), 687 QUARTER(RelativeUnit.QUARTERS, AbsoluteUnit.QUARTER), 688 YEAR(RelativeUnit.YEARS, AbsoluteUnit.YEAR), 689 SUNDAY(null, AbsoluteUnit.SUNDAY), 690 MONDAY(null, AbsoluteUnit.MONDAY), 691 TUESDAY(null, AbsoluteUnit.TUESDAY), 692 WEDNESDAY(null, AbsoluteUnit.WEDNESDAY), 693 THURSDAY(null, AbsoluteUnit.THURSDAY), 694 FRIDAY(null, AbsoluteUnit.FRIDAY), 695 SATURDAY(null, AbsoluteUnit.SATURDAY); 696 697 RelativeUnit relUnit; 698 AbsoluteUnit absUnit; 699 700 DateTimeUnit(RelativeUnit relUnit, AbsoluteUnit absUnit) { 701 this.relUnit = relUnit; 702 this.absUnit = absUnit; 703 } 704 705 private static final DateTimeUnit orNullFromString(CharSequence keyword) { 706 // Quick check from string to enum. 707 switch (keyword.length()) { 708 case 3: 709 if ("day".contentEquals(keyword)) { 710 return DAY; 711 } else if ("sun".contentEquals(keyword)) { 712 return SUNDAY; 713 } else if ("mon".contentEquals(keyword)) { 714 return MONDAY; 715 } else if ("tue".contentEquals(keyword)) { 716 return TUESDAY; 717 } else if ("wed".contentEquals(keyword)) { 718 return WEDNESDAY; 719 } else if ("thu".contentEquals(keyword)) { 720 return THURSDAY; 721 } else if ("fri".contentEquals(keyword)) { 722 return FRIDAY; 723 } else if ("sat".contentEquals(keyword)) { 724 return SATURDAY; 725 } 726 break; 727 case 4: 728 if ("hour".contentEquals(keyword)) { 729 return HOUR; 730 } else if ("week".contentEquals(keyword)) { 731 return WEEK; 732 } else if ("year".contentEquals(keyword)) { 733 return YEAR; 734 } 735 break; 736 case 5: 737 if ("month".contentEquals(keyword)) { 738 return MONTH; 739 } 740 break; 741 case 6: 742 if ("minute".contentEquals(keyword)) { 743 return MINUTE; 744 }else if ("second".contentEquals(keyword)) { 745 return SECOND; 746 } 747 break; 748 case 7: 749 if ("quarter".contentEquals(keyword)) { 750 return QUARTER; // TODO: Check @provisional 751 } 752 break; 753 default: 754 break; 755 } 756 return null; 757 } 758 } 759 760 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap = 761 new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class); 762 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> styleRelUnitPatterns = 763 new EnumMap<Style, EnumMap<RelativeUnit, String[][]>>(Style.class); 764 765 private ULocale ulocale = null; 766 767 StringBuilder sb = new StringBuilder(); 768 769 public RelDateTimeFmtDataSink(ULocale locale) { 770 ulocale = locale; 771 } 772 773 // Values keep between levels of parsing the CLDR data. 774 int pastFutureIndex; 775 Style style; // {LONG, SHORT, NARROW} Derived from unit key string. 776 DateTimeUnit unit; // From the unit key string, with the style (e.g., "-short") separated out. 777 778 private Style styleFromKey(UResource.Key key) { 779 if (key.endsWith("-short")) { 780 return Style.SHORT; 781 } else if (key.endsWith("-narrow")) { 782 return Style.NARROW; 783 } else { 784 return Style.LONG; 785 } 786 } 787 788 private Style styleFromAlias(UResource.Value value) { 789 String s = value.getAliasString(); 790 if (s.endsWith("-short")) { 791 return Style.SHORT; 792 } else if (s.endsWith("-narrow")) { 793 return Style.NARROW; 794 } else { 795 return Style.LONG; 796 } 797 } 798 799 private static int styleSuffixLength(Style style) { 800 switch (style) { 801 case SHORT: return 6; 802 case NARROW: return 7; 803 default: return 0; 804 } 805 } 806 807 @Override 808 public void put(UResource.Key key, UResource.Value value) { 809 // Parse and store aliases. 810 if (value.getType() != ICUResourceBundle.ALIAS) { return; } 811 812 Style sourceStyle = styleFromKey(key); 813 int limit = key.length() - styleSuffixLength(sourceStyle); 814 DateTimeUnit unit = DateTimeUnit.orNullFromString(key.substring(0, limit)); 815 if (unit != null) { 816 // Record the fallback chain for the values. 817 // At formatting time, limit to 2 levels of fallback. 818 Style targetStyle = styleFromAlias(value); 819 if (sourceStyle == targetStyle) { 820 throw new ICUException("Invalid style fallback from " + sourceStyle + " to itself"); 821 } 822 823 // Check for inconsistent fallbacks. 824 if (fallbackCache[sourceStyle.ordinal()] == null) { 825 fallbackCache[sourceStyle.ordinal()] = targetStyle; 826 } else if (fallbackCache[sourceStyle.ordinal()] != targetStyle) { 827 throw new ICUException( 828 "Inconsistent style fallback for style " + sourceStyle + " to " + targetStyle); 829 } 830 } 831 } 832 833 @Override 834 public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) { 835 // Get base unit and style from the key value. 836 style = styleFromKey(key); 837 int limit = key.length() - styleSuffixLength(style); 838 String unitString = key.substring(0, limit); 839 840 // Process only if unitString is in the white list. 841 unit = DateTimeUnit.orNullFromString(unitString); 842 if (unit == null) { 843 return null; 844 } 845 return unitSink; // Continue parsing this path. 846 } 847 848 // Sinks for additional levels under /fields/*/relative/ and /fields/*/relativeTime/ 849 850 // Sets values under relativeTime paths, e.g., "hour/relativeTime/future/one" 851 class RelativeTimeDetailSink extends UResource.TableSink { 852 @Override 853 public void put(UResource.Key key, UResource.Value value) { 854 /* Make two lists of simplePatternFmtList, one for past and one for future. 855 * Set a SimplePatternFormatter for the <style, relative unit, plurality> 856 * 857 * Fill in values for the particular plural given, e.g., ONE, FEW, OTHER, etc. 858 */ 859 EnumMap<RelativeUnit, String[][]> unitPatterns = 860 styleRelUnitPatterns.get(style); 861 if (unitPatterns == null) { 862 unitPatterns = new EnumMap<RelativeUnit, String[][]>(RelativeUnit.class); 863 styleRelUnitPatterns.put(style, unitPatterns); 864 } 865 String[][] patterns = unitPatterns.get(unit.relUnit); 866 if (patterns == null) { 867 patterns = new String[2][StandardPlural.COUNT]; 868 unitPatterns.put(unit.relUnit, patterns); 869 } 870 int pluralIndex = StandardPlural.indexFromString(key.toString()); 871 if (patterns[pastFutureIndex][pluralIndex] == null) { 872 patterns[pastFutureIndex][pluralIndex] = 873 SimplePatternFormatter.compileToStringMinMaxPlaceholders(value.getString(), 874 sb, 0, 1); 875 } 876 } 877 } 878 RelativeTimeDetailSink relativeTimeDetailSink = new RelativeTimeDetailSink(); 879 880 // Handles "relativeTime" entries, e.g., under "day", "hour", "minute", "minute-short", etc. 881 class RelativeTimeSink extends UResource.TableSink { 882 @Override 883 public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) { 884 if (key.contentEquals("past")) { 885 pastFutureIndex = 0; 886 } else if (key.contentEquals("future")) { 887 pastFutureIndex = 1; 888 } else { 889 return null; 890 } 891 if (unit.relUnit == null) { 892 return null; 893 } 894 return relativeTimeDetailSink; 895 } 896 } 897 RelativeTimeSink relativeTimeSink = new RelativeTimeSink(); 898 899 // Handles "relative" entries, e.g., under "day", "day-short", "fri", "fri-narrow", "fri-short", etc. 900 class RelativeSink extends UResource.TableSink { 901 @Override 902 public void put(UResource.Key key, UResource.Value value) { 903 904 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> absMap = qualitativeUnitMap.get(style); 905 906 if (unit.relUnit == RelativeUnit.SECONDS) { 907 if (key.contentEquals("0")) { 908 // Handle Zero seconds for "now". 909 EnumMap<Direction, String> unitStrings = absMap.get(AbsoluteUnit.NOW); 910 if (unitStrings == null) { 911 unitStrings = new EnumMap<Direction, String>(Direction.class); 912 absMap.put(AbsoluteUnit.NOW, unitStrings); 913 } 914 if (unitStrings.get(Direction.PLAIN) == null) { 915 unitStrings.put(Direction.PLAIN, value.getString()); 916 } 917 return; 918 } 919 } 920 Direction keyDirection = keyToDirection(key); 921 if (keyDirection == null) { 922 return; 923 } 924 AbsoluteUnit absUnit = unit.absUnit; 925 if (absUnit == null) { 926 return; 927 } 928 929 if (absMap == null) { 930 absMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class); 931 qualitativeUnitMap.put(style, absMap); 932 } 933 EnumMap<Direction, String> dirMap = absMap.get(absUnit); 934 if (dirMap == null) { 935 dirMap = new EnumMap<Direction, String>(Direction.class); 936 absMap.put(absUnit, dirMap); 937 } 938 if (dirMap.get(keyDirection) == null) { 939 // Do not override values already entered. 940 dirMap.put(keyDirection, value.getString()); 941 } 942 } 943 } 944 RelativeSink relativeSink = new RelativeSink(); 945 946 // Handles entries under units, recognizing "relative" and "relativeTime" entries. 947 class UnitSink extends UResource.TableSink { 948 @Override 949 public void put(UResource.Key key, UResource.Value value) { 950 if (key.contentEquals("dn")) { 951 // Handle Display Name for PLAIN direction for some units. 952 AbsoluteUnit absUnit = unit.absUnit; 953 if (absUnit == null) { 954 return; // Not interesting. 955 } 956 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap = 957 qualitativeUnitMap.get(style); 958 if (unitMap == null) { 959 unitMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class); 960 qualitativeUnitMap.put(style, unitMap); 961 } 962 EnumMap<Direction,String> dirMap = unitMap.get(absUnit); 963 if (dirMap == null) { 964 dirMap = new EnumMap<Direction,String>(Direction.class); 965 unitMap.put(absUnit, dirMap); 966 } 967 if (dirMap.get(Direction.PLAIN) == null) { 968 String displayName = value.toString(); 969 // TODO(Travis Keep): This is a hack to get around CLDR bug 6818. 970 if (ulocale.getLanguage().equals("en")) { 971 displayName = displayName.toLowerCase(Locale.ROOT); 972 } 973 dirMap.put(Direction.PLAIN, displayName); 974 } 975 } 976 } 977 978 @Override 979 public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) { 980 if (key.contentEquals("relative")) { 981 return relativeSink; 982 } else if (key.contentEquals("relativeTime")) { 983 return relativeTimeSink; 984 } 985 return null; 986 } 987 } 988 UnitSink unitSink = new UnitSink(); 989 } 990 991 private static class Loader { 992 private final ULocale ulocale; 993 994 public Loader(ULocale ulocale) { 995 this.ulocale = ulocale; 996 } 997 998 public RelativeDateTimeFormatterData load() { 999 // Sink for traversing data. 1000 RelDateTimeFmtDataSink sink = new RelDateTimeFmtDataSink(ulocale); 1001 ICUResourceBundle r = (ICUResourceBundle)UResourceBundle. 1002 getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale); 1003 1004 // Use sink mechanism to traverse data structure. 1005 r.getAllTableItemsWithFallback("fields", sink); 1006 1007 // Check fallbacks array for loops or too many levels. 1008 for (Style testStyle : Style.values()) { 1009 Style newStyle1 = fallbackCache[testStyle.ordinal()]; 1010 // Data loading guaranteed newStyle1 != testStyle. 1011 if (newStyle1 != null) { 1012 Style newStyle2 = fallbackCache[newStyle1.ordinal()]; 1013 if (newStyle2 != null) { 1014 // No fallback should take more than 2 steps. 1015 if (fallbackCache[newStyle2.ordinal()] != null) { 1016 throw new IllegalStateException("Style fallback too deep"); 1017 } 1018 } 1019 } 1020 } 1021 1022 // TODO: Replace this use of CalendarData. 1023 CalendarData calData = new CalendarData( 1024 ulocale, r.getStringWithFallback("calendar/default")); 1025 1026 return new RelativeDateTimeFormatterData( 1027 sink.qualitativeUnitMap, sink.styleRelUnitPatterns, 1028 calData.getDateTimePattern()); 1029 } 1030 } 1031 1032 private static final Cache cache = new Cache(); 1033} 1034