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) 2013-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9package com.ibm.icu.text; 10 11import java.util.EnumMap; 12import java.util.Locale; 13 14import com.ibm.icu.impl.CacheBase; 15import com.ibm.icu.impl.DontCareFieldPosition; 16import com.ibm.icu.impl.ICUData; 17import com.ibm.icu.impl.ICUResourceBundle; 18import com.ibm.icu.impl.SimpleFormatterImpl; 19import com.ibm.icu.impl.SoftCache; 20import com.ibm.icu.impl.StandardPlural; 21import com.ibm.icu.impl.UResource; 22import com.ibm.icu.lang.UCharacter; 23import com.ibm.icu.util.Calendar; 24import com.ibm.icu.util.ICUException; 25import com.ibm.icu.util.ULocale; 26import com.ibm.icu.util.UResourceBundle; 27 28 29/** 30 * Formats simple relative dates. There are two types of relative dates that 31 * it handles: 32 * <ul> 33 * <li>relative dates with a quantity e.g "in 5 days"</li> 34 * <li>relative dates without a quantity e.g "next Tuesday"</li> 35 * </ul> 36 * <p> 37 * This API is very basic and is intended to be a building block for more 38 * fancy APIs. The caller tells it exactly what to display in a locale 39 * independent way. While this class automatically provides the correct plural 40 * forms, the grammatical form is otherwise as neutral as possible. It is the 41 * caller's responsibility to handle cut-off logic such as deciding between 42 * displaying "in 7 days" or "in 1 week." This API supports relative dates 43 * involving one single unit. This API does not support relative dates 44 * involving compound units. 45 * e.g "in 5 days and 4 hours" nor does it support parsing. 46 * This class is both immutable and thread-safe. 47 * <p> 48 * Here are some examples of use: 49 * <blockquote> 50 * <pre> 51 * RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(); 52 * fmt.format(1, Direction.NEXT, RelativeUnit.DAYS); // "in 1 day" 53 * fmt.format(3, Direction.NEXT, RelativeUnit.DAYS); // "in 3 days" 54 * fmt.format(3.2, Direction.LAST, RelativeUnit.YEARS); // "3.2 years ago" 55 * 56 * fmt.format(Direction.LAST, AbsoluteUnit.SUNDAY); // "last Sunday" 57 * fmt.format(Direction.THIS, AbsoluteUnit.SUNDAY); // "this Sunday" 58 * fmt.format(Direction.NEXT, AbsoluteUnit.SUNDAY); // "next Sunday" 59 * fmt.format(Direction.PLAIN, AbsoluteUnit.SUNDAY); // "Sunday" 60 * 61 * fmt.format(Direction.LAST, AbsoluteUnit.DAY); // "yesterday" 62 * fmt.format(Direction.THIS, AbsoluteUnit.DAY); // "today" 63 * fmt.format(Direction.NEXT, AbsoluteUnit.DAY); // "tomorrow" 64 * 65 * fmt.format(Direction.PLAIN, AbsoluteUnit.NOW); // "now" 66 * </pre> 67 * </blockquote> 68 * <p> 69 * In the future, we may add more forms, such as abbreviated/short forms 70 * (3 secs ago), and relative day periods ("yesterday afternoon"), etc. 71 * 72 * @stable ICU 53 73 */ 74public final class RelativeDateTimeFormatter { 75 76 /** 77 * The formatting style 78 * @stable ICU 54 79 * 80 */ 81 public static enum Style { 82 83 /** 84 * Everything spelled out. 85 * @stable ICU 54 86 */ 87 LONG, 88 89 /** 90 * Abbreviations used when possible. 91 * @stable ICU 54 92 */ 93 SHORT, 94 95 /** 96 * Use single letters when possible. 97 * @stable ICU 54 98 */ 99 NARROW; 100 101 private static final int INDEX_COUNT = 3; // NARROW.ordinal() + 1 102 } 103 104 /** 105 * Represents the unit for formatting a relative date. e.g "in 5 days" 106 * or "in 3 months" 107 * @stable ICU 53 108 */ 109 public static enum RelativeUnit { 110 111 /** 112 * Seconds 113 * @stable ICU 53 114 */ 115 SECONDS, 116 117 /** 118 * Minutes 119 * @stable ICU 53 120 */ 121 MINUTES, 122 123 /** 124 * Hours 125 * @stable ICU 53 126 */ 127 HOURS, 128 129 /** 130 * Days 131 * @stable ICU 53 132 */ 133 DAYS, 134 135 /** 136 * Weeks 137 * @stable ICU 53 138 */ 139 WEEKS, 140 141 /** 142 * Months 143 * @stable ICU 53 144 */ 145 MONTHS, 146 147 /** 148 * Years 149 * @stable ICU 53 150 */ 151 YEARS, 152 153 /** 154 * Quarters 155 * @internal TODO: propose for addition in ICU 57 156 * @deprecated This API is ICU internal only. 157 */ 158 @Deprecated 159 QUARTERS, 160 } 161 162 /** 163 * Represents an absolute unit. 164 * @stable ICU 53 165 */ 166 public static enum AbsoluteUnit { 167 168 /** 169 * Sunday 170 * @stable ICU 53 171 */ 172 SUNDAY, 173 174 /** 175 * Monday 176 * @stable ICU 53 177 */ 178 MONDAY, 179 180 /** 181 * Tuesday 182 * @stable ICU 53 183 */ 184 TUESDAY, 185 186 /** 187 * Wednesday 188 * @stable ICU 53 189 */ 190 WEDNESDAY, 191 192 /** 193 * Thursday 194 * @stable ICU 53 195 */ 196 THURSDAY, 197 198 /** 199 * Friday 200 * @stable ICU 53 201 */ 202 FRIDAY, 203 204 /** 205 * Saturday 206 * @stable ICU 53 207 */ 208 SATURDAY, 209 210 /** 211 * Day 212 * @stable ICU 53 213 */ 214 DAY, 215 216 /** 217 * Week 218 * @stable ICU 53 219 */ 220 WEEK, 221 222 /** 223 * Month 224 * @stable ICU 53 225 */ 226 MONTH, 227 228 /** 229 * Year 230 * @stable ICU 53 231 */ 232 YEAR, 233 234 /** 235 * Now 236 * @stable ICU 53 237 */ 238 NOW, 239 240 /** 241 * Quarter 242 * @internal TODO: propose for addition in ICU 57 243 * @deprecated This API is ICU internal only. 244 */ 245 @Deprecated 246 QUARTER, 247 } 248 249 /** 250 * Represents a direction for an absolute unit e.g "Next Tuesday" 251 * or "Last Tuesday" 252 * @stable ICU 53 253 */ 254 public static enum Direction { 255 /** 256 * Two before. Not fully supported in every locale 257 * @stable ICU 53 258 */ 259 LAST_2, 260 261 /** 262 * Last 263 * @stable ICU 53 264 */ 265 LAST, 266 267 /** 268 * This 269 * @stable ICU 53 270 */ 271 THIS, 272 273 /** 274 * Next 275 * @stable ICU 53 276 */ 277 NEXT, 278 279 /** 280 * Two after. Not fully supported in every locale 281 * @stable ICU 53 282 */ 283 NEXT_2, 284 285 /** 286 * Plain, which means the absence of a qualifier 287 * @stable ICU 53 288 */ 289 PLAIN, 290 } 291 292 /** 293 * Represents the unit for formatting a relative date. e.g "in 5 days" 294 * or "next year" 295 * @draft ICU 57 296 * @provisional This API might change or be removed in a future release. 297 */ 298 public static enum RelativeDateTimeUnit { 299 /** 300 * Specifies that relative unit is year, e.g. "last year", 301 * "in 5 years". 302 * @draft ICU 57 303 * @provisional This API might change or be removed in a future release. 304 */ 305 YEAR, 306 /** 307 * Specifies that relative unit is quarter, e.g. "last quarter", 308 * "in 5 quarters". 309 * @draft ICU 57 310 * @provisional This API might change or be removed in a future release. 311 */ 312 QUARTER, 313 /** 314 * Specifies that relative unit is month, e.g. "last month", 315 * "in 5 months". 316 * @draft ICU 57 317 * @provisional This API might change or be removed in a future release. 318 */ 319 MONTH, 320 /** 321 * Specifies that relative unit is week, e.g. "last week", 322 * "in 5 weeks". 323 * @draft ICU 57 324 * @provisional This API might change or be removed in a future release. 325 */ 326 WEEK, 327 /** 328 * Specifies that relative unit is day, e.g. "yesterday", 329 * "in 5 days". 330 * @draft ICU 57 331 * @provisional This API might change or be removed in a future release. 332 */ 333 DAY, 334 /** 335 * Specifies that relative unit is hour, e.g. "1 hour ago", 336 * "in 5 hours". 337 * @draft ICU 57 338 * @provisional This API might change or be removed in a future release. 339 */ 340 HOUR, 341 /** 342 * Specifies that relative unit is minute, e.g. "1 minute ago", 343 * "in 5 minutes". 344 * @draft ICU 57 345 * @provisional This API might change or be removed in a future release. 346 */ 347 MINUTE, 348 /** 349 * Specifies that relative unit is second, e.g. "1 second ago", 350 * "in 5 seconds". 351 * @draft ICU 57 352 * @provisional This API might change or be removed in a future release. 353 */ 354 SECOND, 355 /** 356 * Specifies that relative unit is Sunday, e.g. "last Sunday", 357 * "this Sunday", "next Sunday", "in 5 Sundays". 358 * @draft ICU 57 359 * @provisional This API might change or be removed in a future release. 360 */ 361 SUNDAY, 362 /** 363 * Specifies that relative unit is Monday, e.g. "last Monday", 364 * "this Monday", "next Monday", "in 5 Mondays". 365 * @draft ICU 57 366 * @provisional This API might change or be removed in a future release. 367 */ 368 MONDAY, 369 /** 370 * Specifies that relative unit is Tuesday, e.g. "last Tuesday", 371 * "this Tuesday", "next Tuesday", "in 5 Tuesdays". 372 * @draft ICU 57 373 * @provisional This API might change or be removed in a future release. 374 */ 375 TUESDAY, 376 /** 377 * Specifies that relative unit is Wednesday, e.g. "last Wednesday", 378 * "this Wednesday", "next Wednesday", "in 5 Wednesdays". 379 * @draft ICU 57 380 * @provisional This API might change or be removed in a future release. 381 */ 382 WEDNESDAY, 383 /** 384 * Specifies that relative unit is Thursday, e.g. "last Thursday", 385 * "this Thursday", "next Thursday", "in 5 Thursdays". 386 * @draft ICU 57 387 * @provisional This API might change or be removed in a future release. 388 */ 389 THURSDAY, 390 /** 391 * Specifies that relative unit is Friday, e.g. "last Friday", 392 * "this Friday", "next Friday", "in 5 Fridays". 393 * @draft ICU 57 394 * @provisional This API might change or be removed in a future release. 395 */ 396 FRIDAY, 397 /** 398 * Specifies that relative unit is Saturday, e.g. "last Saturday", 399 * "this Saturday", "next Saturday", "in 5 Saturdays". 400 * @draft ICU 57 401 * @provisional This API might change or be removed in a future release. 402 */ 403 SATURDAY, 404 } 405 406 /** 407 * Returns a RelativeDateTimeFormatter for the default locale. 408 * @stable ICU 53 409 */ 410 public static RelativeDateTimeFormatter getInstance() { 411 return getInstance(ULocale.getDefault(), null, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 412 } 413 414 /** 415 * Returns a RelativeDateTimeFormatter for a particular locale. 416 * 417 * @param locale the locale. 418 * @return An instance of RelativeDateTimeFormatter. 419 * @stable ICU 53 420 */ 421 public static RelativeDateTimeFormatter getInstance(ULocale locale) { 422 return getInstance(locale, null, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 423 } 424 425 /** 426 * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale}. 427 * 428 * @param locale the {@link java.util.Locale}. 429 * @return An instance of RelativeDateTimeFormatter. 430 * @stable ICU 54 431 */ 432 public static RelativeDateTimeFormatter getInstance(Locale locale) { 433 return getInstance(ULocale.forLocale(locale)); 434 } 435 436 /** 437 * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular 438 * NumberFormat object. 439 * 440 * @param locale the locale 441 * @param nf the number format object. It is defensively copied to ensure thread-safety 442 * and immutability of this class. 443 * @return An instance of RelativeDateTimeFormatter. 444 * @stable ICU 53 445 */ 446 public static RelativeDateTimeFormatter getInstance(ULocale locale, NumberFormat nf) { 447 return getInstance(locale, nf, Style.LONG, DisplayContext.CAPITALIZATION_NONE); 448 } 449 450 /** 451 * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular 452 * NumberFormat object, style, and capitalization context 453 * 454 * @param locale the locale 455 * @param nf the number format object. It is defensively copied to ensure thread-safety 456 * and immutability of this class. May be null. 457 * @param style the style. 458 * @param capitalizationContext the capitalization context. 459 * @stable ICU 54 460 */ 461 public static RelativeDateTimeFormatter getInstance( 462 ULocale locale, 463 NumberFormat nf, 464 Style style, 465 DisplayContext capitalizationContext) { 466 RelativeDateTimeFormatterData data = cache.get(locale); 467 if (nf == null) { 468 nf = NumberFormat.getInstance(locale); 469 } else { 470 nf = (NumberFormat) nf.clone(); 471 } 472 return new RelativeDateTimeFormatter( 473 data.qualitativeUnitMap, 474 data.relUnitPatternMap, 475 SimpleFormatterImpl.compileToStringMinMaxArguments( 476 data.dateTimePattern, new StringBuilder(), 2, 2), 477 PluralRules.forLocale(locale), 478 nf, 479 style, 480 capitalizationContext, 481 capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ? 482 BreakIterator.getSentenceInstance(locale) : null, 483 locale); 484 } 485 486 /** 487 * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale} that uses a 488 * particular NumberFormat object. 489 * 490 * @param locale the {@link java.util.Locale} 491 * @param nf the number format object. It is defensively copied to ensure thread-safety 492 * and immutability of this class. 493 * @return An instance of RelativeDateTimeFormatter. 494 * @stable ICU 54 495 */ 496 public static RelativeDateTimeFormatter getInstance(Locale locale, NumberFormat nf) { 497 return getInstance(ULocale.forLocale(locale), nf); 498 } 499 500 /** 501 * Formats a relative date with a quantity such as "in 5 days" or 502 * "3 months ago" 503 * @param quantity The numerical amount e.g 5. This value is formatted 504 * according to this object's {@link NumberFormat} object. 505 * @param direction NEXT means a future relative date; LAST means a past 506 * relative date. 507 * @param unit the unit e.g day? month? year? 508 * @return the formatted string 509 * @throws IllegalArgumentException if direction is something other than 510 * NEXT or LAST. 511 * @stable ICU 53 512 */ 513 public String format(double quantity, Direction direction, RelativeUnit unit) { 514 if (direction != Direction.LAST && direction != Direction.NEXT) { 515 throw new IllegalArgumentException("direction must be NEXT or LAST"); 516 } 517 String result; 518 int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0); 519 520 // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this 521 // class we must guarantee that only one thread at a time uses our numberFormat. 522 synchronized (numberFormat) { 523 StringBuffer formatStr = new StringBuffer(); 524 DontCareFieldPosition fieldPosition = DontCareFieldPosition.INSTANCE; 525 StandardPlural pluralForm = QuantityFormatter.selectPlural(quantity, 526 numberFormat, pluralRules, formatStr, fieldPosition); 527 528 String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); 529 result = SimpleFormatterImpl.formatCompiledPattern(formatter, formatStr); 530 } 531 return adjustForContext(result); 532 533 } 534 535 /** 536 * Format a combination of RelativeDateTimeUnit and numeric offset 537 * using a numeric style, e.g. "1 week ago", "in 1 week", 538 * "5 weeks ago", "in 5 weeks". 539 * 540 * @param offset The signed offset for the specified unit. This 541 * will be formatted according to this object's 542 * NumberFormat object. 543 * @param unit The unit to use when formatting the relative 544 * date, e.g. RelativeDateTimeUnit.WEEK, 545 * RelativeDateTimeUnit.FRIDAY. 546 * @return The formatted string (may be empty in case of error) 547 * @draft ICU 57 548 * @provisional This API might change or be removed in a future release. 549 */ 550 public String formatNumeric(double offset, RelativeDateTimeUnit unit) { 551 // TODO: 552 // The full implementation of this depends on CLDR data that is not yet available, 553 // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data. 554 // In the meantime do a quick bring-up by calling the old format method. When the 555 // new CLDR data is available, update the data storage accordingly, rewrite this 556 // to use it directly, and rewrite the old format method to call this new one; 557 // that is covered by http://bugs.icu-project.org/trac/ticket/12171. 558 RelativeUnit relunit = RelativeUnit.SECONDS; 559 switch (unit) { 560 case YEAR: relunit = RelativeUnit.YEARS; break; 561 case QUARTER: relunit = RelativeUnit.QUARTERS; break; 562 case MONTH: relunit = RelativeUnit.MONTHS; break; 563 case WEEK: relunit = RelativeUnit.WEEKS; break; 564 case DAY: relunit = RelativeUnit.DAYS; break; 565 case HOUR: relunit = RelativeUnit.HOURS; break; 566 case MINUTE: relunit = RelativeUnit.MINUTES; break; 567 case SECOND: break; // set above 568 default: // SUNDAY..SATURDAY 569 throw new UnsupportedOperationException("formatNumeric does not currently support RelativeUnit.SUNDAY..SATURDAY"); 570 } 571 Direction direction = Direction.NEXT; 572 if (offset < 0) { 573 direction = Direction.LAST; 574 offset = -offset; 575 } 576 String result = format(offset, direction, relunit); 577 return (result != null)? result: ""; 578 } 579 580 private int[] styleToDateFormatSymbolsWidth = { 581 DateFormatSymbols.WIDE, DateFormatSymbols.SHORT, DateFormatSymbols.NARROW 582 }; 583 584 /** 585 * Formats a relative date without a quantity. 586 * @param direction NEXT, LAST, THIS, etc. 587 * @param unit e.g SATURDAY, DAY, MONTH 588 * @return the formatted string. If direction has a value that is documented as not being 589 * fully supported in every locale (for example NEXT_2 or LAST_2) then this function may 590 * return null to signal that no formatted string is available. 591 * @throws IllegalArgumentException if the direction is incompatible with 592 * unit this can occur with NOW which can only take PLAIN. 593 * @stable ICU 53 594 */ 595 public String format(Direction direction, AbsoluteUnit unit) { 596 if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) { 597 throw new IllegalArgumentException("NOW can only accept direction PLAIN."); 598 } 599 String result; 600 // Get plain day of week names from DateFormatSymbols. 601 if ((direction == Direction.PLAIN) && (AbsoluteUnit.SUNDAY.ordinal() <= unit.ordinal() && 602 unit.ordinal() <= AbsoluteUnit.SATURDAY.ordinal())) { 603 // Convert from AbsoluteUnit days to Calendar class indexing. 604 int dateSymbolsDayOrdinal = (unit.ordinal() - AbsoluteUnit.SUNDAY.ordinal()) + Calendar.SUNDAY; 605 String[] dayNames = 606 dateFormatSymbols.getWeekdays(DateFormatSymbols.STANDALONE, 607 styleToDateFormatSymbolsWidth[style.ordinal()]); 608 result = dayNames[dateSymbolsDayOrdinal]; 609 } else { 610 // Not PLAIN, or not a weekday. 611 result = getAbsoluteUnitString(style, unit, direction); 612 } 613 return result != null ? adjustForContext(result) : null; 614 } 615 616 /** 617 * Format a combination of RelativeDateTimeUnit and numeric offset 618 * using a text style if possible, e.g. "last week", "this week", 619 * "next week", "yesterday", "tomorrow". Falls back to numeric 620 * style if no appropriate text term is available for the specified 621 * offset in the object’s locale. 622 * 623 * @param offset The signed offset for the specified field. 624 * @param unit The unit to use when formatting the relative 625 * date, e.g. RelativeDateTimeUnit.WEEK, 626 * RelativeDateTimeUnit.FRIDAY. 627 * @return The formatted string (may be empty in case of error) 628 * @draft ICU 57 629 * @provisional This API might change or be removed in a future release. 630 */ 631 public String format(double offset, RelativeDateTimeUnit unit) { 632 // TODO: 633 // The full implementation of this depends on CLDR data that is not yet available, 634 // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data. 635 // In the meantime do a quick bring-up by calling the old format method. When the 636 // new CLDR data is available, update the data storage accordingly, rewrite this 637 // to use it directly, and rewrite the old format method to call this new one; 638 // that is covered by http://bugs.icu-project.org/trac/ticket/12171. 639 boolean useNumeric = true; 640 Direction direction = Direction.THIS; 641 if (offset > -2.1 && offset < 2.1) { 642 // Allow a 1% epsilon, so offsets in -1.01..-0.99 map to LAST 643 double offsetx100 = offset * 100.0; 644 int intoffsetx100 = (offsetx100 < 0)? (int)(offsetx100-0.5) : (int)(offsetx100+0.5); 645 switch (intoffsetx100) { 646 case -200/*-2*/: direction = Direction.LAST_2; useNumeric = false; break; 647 case -100/*-1*/: direction = Direction.LAST; useNumeric = false; break; 648 case 0/* 0*/: useNumeric = false; break; // direction = Direction.THIS was set above 649 case 100/* 1*/: direction = Direction.NEXT; useNumeric = false; break; 650 case 200/* 2*/: direction = Direction.NEXT_2; useNumeric = false; break; 651 default: break; 652 } 653 } 654 AbsoluteUnit absunit = AbsoluteUnit.NOW; 655 switch (unit) { 656 case YEAR: absunit = AbsoluteUnit.YEAR; break; 657 case QUARTER: absunit = AbsoluteUnit.QUARTER; break; 658 case MONTH: absunit = AbsoluteUnit.MONTH; break; 659 case WEEK: absunit = AbsoluteUnit.WEEK; break; 660 case DAY: absunit = AbsoluteUnit.DAY; break; 661 case SUNDAY: absunit = AbsoluteUnit.SUNDAY; break; 662 case MONDAY: absunit = AbsoluteUnit.MONDAY; break; 663 case TUESDAY: absunit = AbsoluteUnit.TUESDAY; break; 664 case WEDNESDAY: absunit = AbsoluteUnit.WEDNESDAY; break; 665 case THURSDAY: absunit = AbsoluteUnit.THURSDAY; break; 666 case FRIDAY: absunit = AbsoluteUnit.FRIDAY; break; 667 case SATURDAY: absunit = AbsoluteUnit.SATURDAY; break; 668 case SECOND: 669 if (direction == Direction.THIS) { 670 // absunit = AbsoluteUnit.NOW was set above 671 direction = Direction.PLAIN; 672 break; 673 } 674 // could just fall through here but that produces warnings 675 useNumeric = true; 676 break; 677 case HOUR: 678 default: 679 useNumeric = true; 680 break; 681 } 682 if (!useNumeric) { 683 String result = format(direction, absunit); 684 if (result != null && result.length() > 0) { 685 return result; 686 } 687 } 688 // otherwise fallback to formatNumeric 689 return formatNumeric(offset, unit); 690 } 691 692 /** 693 * Gets the string value from qualitativeUnitMap with fallback based on style. 694 */ 695 private String getAbsoluteUnitString(Style style, AbsoluteUnit unit, Direction direction) { 696 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap; 697 EnumMap<Direction, String> dirMap; 698 699 do { 700 unitMap = qualitativeUnitMap.get(style); 701 if (unitMap != null) { 702 dirMap = unitMap.get(unit); 703 if (dirMap != null) { 704 String result = dirMap.get(direction); 705 if (result != null) { 706 return result; 707 } 708 } 709 710 } 711 712 // Consider other styles from alias fallback. 713 // Data loading guaranteed no endless loops. 714 } while ((style = fallbackCache[style.ordinal()]) != null); 715 return null; 716 } 717 718 /** 719 * Combines a relative date string and a time string in this object's 720 * locale. This is done with the same date-time separator used for the 721 * default calendar in this locale. 722 * @param relativeDateString the relative date e.g 'yesterday' 723 * @param timeString the time e.g '3:45' 724 * @return the date and time concatenated according to the default 725 * calendar in this locale e.g 'yesterday, 3:45' 726 * @stable ICU 53 727 */ 728 public String combineDateAndTime(String relativeDateString, String timeString) { 729 return SimpleFormatterImpl.formatCompiledPattern( 730 combinedDateAndTime, timeString, relativeDateString); 731 } 732 733 /** 734 * Returns a copy of the NumberFormat this object is using. 735 * @return A copy of the NumberFormat. 736 * @stable ICU 53 737 */ 738 public NumberFormat getNumberFormat() { 739 // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this 740 // class we must guarantee that only one thread at a time uses our numberFormat. 741 synchronized (numberFormat) { 742 return (NumberFormat) numberFormat.clone(); 743 } 744 } 745 746 /** 747 * Return capitalization context. 748 * @return The capitalization context. 749 * @stable ICU 54 750 */ 751 public DisplayContext getCapitalizationContext() { 752 return capitalizationContext; 753 } 754 755 /** 756 * Return style 757 * @return The formatting style. 758 * @stable ICU 54 759 */ 760 public Style getFormatStyle() { 761 return style; 762 } 763 764 private String adjustForContext(String originalFormattedString) { 765 if (breakIterator == null || originalFormattedString.length() == 0 766 || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) { 767 return originalFormattedString; 768 } 769 synchronized (breakIterator) { 770 return UCharacter.toTitleCase( 771 locale, 772 originalFormattedString, 773 breakIterator, 774 UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT); 775 } 776 } 777 778 private RelativeDateTimeFormatter( 779 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap, 780 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap, 781 String combinedDateAndTime, 782 PluralRules pluralRules, 783 NumberFormat numberFormat, 784 Style style, 785 DisplayContext capitalizationContext, 786 BreakIterator breakIterator, 787 ULocale locale) { 788 this.qualitativeUnitMap = qualitativeUnitMap; 789 this.patternMap = patternMap; 790 this.combinedDateAndTime = combinedDateAndTime; 791 this.pluralRules = pluralRules; 792 this.numberFormat = numberFormat; 793 this.style = style; 794 if (capitalizationContext.type() != DisplayContext.Type.CAPITALIZATION) { 795 throw new IllegalArgumentException(capitalizationContext.toString()); 796 } 797 this.capitalizationContext = capitalizationContext; 798 this.breakIterator = breakIterator; 799 this.locale = locale; 800 this.dateFormatSymbols = new DateFormatSymbols(locale); 801 } 802 803 private String getRelativeUnitPluralPattern( 804 Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) { 805 if (pluralForm != StandardPlural.OTHER) { 806 String formatter = getRelativeUnitPattern(style, unit, pastFutureIndex, pluralForm); 807 if (formatter != null) { 808 return formatter; 809 } 810 } 811 return getRelativeUnitPattern(style, unit, pastFutureIndex, StandardPlural.OTHER); 812 } 813 814 private String getRelativeUnitPattern( 815 Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) { 816 int pluralIndex = pluralForm.ordinal(); 817 do { 818 EnumMap<RelativeUnit, String[][]> unitMap = patternMap.get(style); 819 if (unitMap != null) { 820 String[][] spfCompiledPatterns = unitMap.get(unit); 821 if (spfCompiledPatterns != null) { 822 if (spfCompiledPatterns[pastFutureIndex][pluralIndex] != null) { 823 return spfCompiledPatterns[pastFutureIndex][pluralIndex]; 824 } 825 } 826 827 } 828 829 // Consider other styles from alias fallback. 830 // Data loading guaranteed no endless loops. 831 } while ((style = fallbackCache[style.ordinal()]) != null); 832 return null; 833 } 834 835 private final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap; 836 private final EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap; 837 838 private final String combinedDateAndTime; // compiled SimpleFormatter pattern 839 private final PluralRules pluralRules; 840 private final NumberFormat numberFormat; 841 842 private final Style style; 843 private final DisplayContext capitalizationContext; 844 private final BreakIterator breakIterator; 845 private final ULocale locale; 846 847 private final DateFormatSymbols dateFormatSymbols; 848 849 private static final Style fallbackCache[] = new Style[Style.INDEX_COUNT]; 850 851 private static class RelativeDateTimeFormatterData { 852 public RelativeDateTimeFormatterData( 853 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap, 854 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap, 855 String dateTimePattern) { 856 this.qualitativeUnitMap = qualitativeUnitMap; 857 this.relUnitPatternMap = relUnitPatternMap; 858 859 this.dateTimePattern = dateTimePattern; 860 } 861 862 public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap; 863 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap; 864 public final String dateTimePattern; // Example: "{1}, {0}" 865 } 866 867 private static class Cache { 868 private final CacheBase<String, RelativeDateTimeFormatterData, ULocale> cache = 869 new SoftCache<String, RelativeDateTimeFormatterData, ULocale>() { 870 @Override 871 protected RelativeDateTimeFormatterData createInstance(String key, ULocale locale) { 872 return new Loader(locale).load(); 873 } 874 }; 875 876 public RelativeDateTimeFormatterData get(ULocale locale) { 877 String key = locale.toString(); 878 return cache.getInstance(key, locale); 879 } 880 } 881 882 private static Direction keyToDirection(UResource.Key key) { 883 if (key.contentEquals("-2")) { 884 return Direction.LAST_2; 885 } 886 if (key.contentEquals("-1")) { 887 return Direction.LAST; 888 } 889 if (key.contentEquals("0")) { 890 return Direction.THIS; 891 } 892 if (key.contentEquals("1")) { 893 return Direction.NEXT; 894 } 895 if (key.contentEquals("2")) { 896 return Direction.NEXT_2; 897 } 898 return null; 899 } 900 901 /** 902 * Sink for enumerating all of the relative data time formatter names. 903 * 904 * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root): 905 * Only store a value if it is still missing, that is, it has not been overridden. 906 */ 907 private static final class RelDateTimeDataSink extends UResource.Sink { 908 909 // For white list of units to handle in RelativeDateTimeFormatter. 910 private enum DateTimeUnit { 911 SECOND(RelativeUnit.SECONDS, null), 912 MINUTE(RelativeUnit.MINUTES, null), 913 HOUR(RelativeUnit.HOURS, null), 914 DAY(RelativeUnit.DAYS, AbsoluteUnit.DAY), 915 WEEK(RelativeUnit.WEEKS, AbsoluteUnit.WEEK), 916 MONTH(RelativeUnit.MONTHS, AbsoluteUnit.MONTH), 917 QUARTER(RelativeUnit.QUARTERS, AbsoluteUnit.QUARTER), 918 YEAR(RelativeUnit.YEARS, AbsoluteUnit.YEAR), 919 SUNDAY(null, AbsoluteUnit.SUNDAY), 920 MONDAY(null, AbsoluteUnit.MONDAY), 921 TUESDAY(null, AbsoluteUnit.TUESDAY), 922 WEDNESDAY(null, AbsoluteUnit.WEDNESDAY), 923 THURSDAY(null, AbsoluteUnit.THURSDAY), 924 FRIDAY(null, AbsoluteUnit.FRIDAY), 925 SATURDAY(null, AbsoluteUnit.SATURDAY); 926 927 RelativeUnit relUnit; 928 AbsoluteUnit absUnit; 929 930 DateTimeUnit(RelativeUnit relUnit, AbsoluteUnit absUnit) { 931 this.relUnit = relUnit; 932 this.absUnit = absUnit; 933 } 934 935 private static final DateTimeUnit orNullFromString(CharSequence keyword) { 936 // Quick check from string to enum. 937 switch (keyword.length()) { 938 case 3: 939 if ("day".contentEquals(keyword)) { 940 return DAY; 941 } else if ("sun".contentEquals(keyword)) { 942 return SUNDAY; 943 } else if ("mon".contentEquals(keyword)) { 944 return MONDAY; 945 } else if ("tue".contentEquals(keyword)) { 946 return TUESDAY; 947 } else if ("wed".contentEquals(keyword)) { 948 return WEDNESDAY; 949 } else if ("thu".contentEquals(keyword)) { 950 return THURSDAY; 951 } else if ("fri".contentEquals(keyword)) { 952 return FRIDAY; 953 } else if ("sat".contentEquals(keyword)) { 954 return SATURDAY; 955 } 956 break; 957 case 4: 958 if ("hour".contentEquals(keyword)) { 959 return HOUR; 960 } else if ("week".contentEquals(keyword)) { 961 return WEEK; 962 } else if ("year".contentEquals(keyword)) { 963 return YEAR; 964 } 965 break; 966 case 5: 967 if ("month".contentEquals(keyword)) { 968 return MONTH; 969 } 970 break; 971 case 6: 972 if ("minute".contentEquals(keyword)) { 973 return MINUTE; 974 }else if ("second".contentEquals(keyword)) { 975 return SECOND; 976 } 977 break; 978 case 7: 979 if ("quarter".contentEquals(keyword)) { 980 return QUARTER; // TODO: Check @provisional 981 } 982 break; 983 default: 984 break; 985 } 986 return null; 987 } 988 } 989 990 EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap = 991 new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class); 992 EnumMap<Style, EnumMap<RelativeUnit, String[][]>> styleRelUnitPatterns = 993 new EnumMap<Style, EnumMap<RelativeUnit, String[][]>>(Style.class); 994 995 StringBuilder sb = new StringBuilder(); 996 997 // Values keep between levels of parsing the CLDR data. 998 int pastFutureIndex; 999 Style style; // {LONG, SHORT, NARROW} Derived from unit key string. 1000 DateTimeUnit unit; // From the unit key string, with the style (e.g., "-short") separated out. 1001 1002 private Style styleFromKey(UResource.Key key) { 1003 if (key.endsWith("-short")) { 1004 return Style.SHORT; 1005 } else if (key.endsWith("-narrow")) { 1006 return Style.NARROW; 1007 } else { 1008 return Style.LONG; 1009 } 1010 } 1011 1012 private Style styleFromAlias(UResource.Value value) { 1013 String s = value.getAliasString(); 1014 if (s.endsWith("-short")) { 1015 return Style.SHORT; 1016 } else if (s.endsWith("-narrow")) { 1017 return Style.NARROW; 1018 } else { 1019 return Style.LONG; 1020 } 1021 } 1022 1023 private static int styleSuffixLength(Style style) { 1024 switch (style) { 1025 case SHORT: return 6; 1026 case NARROW: return 7; 1027 default: return 0; 1028 } 1029 } 1030 1031 public void consumeTableRelative(UResource.Key key, UResource.Value value) { 1032 UResource.Table unitTypesTable = value.getTable(); 1033 for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) { 1034 if (value.getType() == ICUResourceBundle.STRING) { 1035 String valueString = value.getString(); 1036 1037 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> absMap = qualitativeUnitMap.get(style); 1038 1039 if (unit.relUnit == RelativeUnit.SECONDS) { 1040 if (key.contentEquals("0")) { 1041 // Handle Zero seconds for "now". 1042 EnumMap<Direction, String> unitStrings = absMap.get(AbsoluteUnit.NOW); 1043 if (unitStrings == null) { 1044 unitStrings = new EnumMap<Direction, String>(Direction.class); 1045 absMap.put(AbsoluteUnit.NOW, unitStrings); 1046 } 1047 if (unitStrings.get(Direction.PLAIN) == null) { 1048 unitStrings.put(Direction.PLAIN, valueString); 1049 } 1050 continue; 1051 } 1052 } 1053 Direction keyDirection = keyToDirection(key); 1054 if (keyDirection == null) { 1055 continue; 1056 } 1057 AbsoluteUnit absUnit = unit.absUnit; 1058 if (absUnit == null) { 1059 continue; 1060 } 1061 1062 if (absMap == null) { 1063 absMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class); 1064 qualitativeUnitMap.put(style, absMap); 1065 } 1066 EnumMap<Direction, String> dirMap = absMap.get(absUnit); 1067 if (dirMap == null) { 1068 dirMap = new EnumMap<Direction, String>(Direction.class); 1069 absMap.put(absUnit, dirMap); 1070 } 1071 if (dirMap.get(keyDirection) == null) { 1072 // Do not override values already entered. 1073 dirMap.put(keyDirection, value.getString()); 1074 } 1075 } 1076 } 1077 } 1078 1079 // Record past or future and 1080 public void consumeTableRelativeTime(UResource.Key key, UResource.Value value) { 1081 if (unit.relUnit == null) { 1082 return; 1083 } 1084 UResource.Table unitTypesTable = value.getTable(); 1085 for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) { 1086 if (key.contentEquals("past")) { 1087 pastFutureIndex = 0; 1088 } else if (key.contentEquals("future")) { 1089 pastFutureIndex = 1; 1090 } else { 1091 continue; 1092 } 1093 // Get the details of the relative time. 1094 consumeTimeDetail(key, value); 1095 } 1096 } 1097 1098 public void consumeTimeDetail(UResource.Key key, UResource.Value value) { 1099 UResource.Table unitTypesTable = value.getTable(); 1100 1101 EnumMap<RelativeUnit, String[][]> unitPatterns = styleRelUnitPatterns.get(style); 1102 if (unitPatterns == null) { 1103 unitPatterns = new EnumMap<RelativeUnit, String[][]>(RelativeUnit.class); 1104 styleRelUnitPatterns.put(style, unitPatterns); 1105 } 1106 String[][] patterns = unitPatterns.get(unit.relUnit); 1107 if (patterns == null) { 1108 patterns = new String[2][StandardPlural.COUNT]; 1109 unitPatterns.put(unit.relUnit, patterns); 1110 } 1111 1112 // Stuff the pattern for the correct plural index with a simple formatter. 1113 for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) { 1114 if (value.getType() == ICUResourceBundle.STRING) { 1115 int pluralIndex = StandardPlural.indexFromString(key.toString()); 1116 if (patterns[pastFutureIndex][pluralIndex] == null) { 1117 patterns[pastFutureIndex][pluralIndex] = 1118 SimpleFormatterImpl.compileToStringMinMaxArguments( 1119 value.getString(), sb, 0, 1); 1120 } 1121 } 1122 } 1123 } 1124 1125 private void handlePlainDirection(UResource.Key key, UResource.Value value) { 1126 AbsoluteUnit absUnit = unit.absUnit; 1127 if (absUnit == null) { 1128 return; // Not interesting. 1129 } 1130 EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap = 1131 qualitativeUnitMap.get(style); 1132 if (unitMap == null) { 1133 unitMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class); 1134 qualitativeUnitMap.put(style, unitMap); 1135 } 1136 EnumMap<Direction,String> dirMap = unitMap.get(absUnit); 1137 if (dirMap == null) { 1138 dirMap = new EnumMap<Direction,String>(Direction.class); 1139 unitMap.put(absUnit, dirMap); 1140 } 1141 if (dirMap.get(Direction.PLAIN) == null) { 1142 dirMap.put(Direction.PLAIN, value.toString()); 1143 } 1144 } 1145 1146 // Handle at the Unit level, 1147 public void consumeTimeUnit(UResource.Key key, UResource.Value value) { 1148 UResource.Table unitTypesTable = value.getTable(); 1149 for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) { 1150 if (key.contentEquals("dn") && value.getType() == ICUResourceBundle.STRING) { 1151 handlePlainDirection(key, value); 1152 } 1153 if (value.getType() == ICUResourceBundle.TABLE) { 1154 if (key.contentEquals("relative")) { 1155 consumeTableRelative(key, value); 1156 } else if (key.contentEquals("relativeTime")) { 1157 consumeTableRelativeTime(key, value); 1158 } 1159 } 1160 } 1161 } 1162 1163 private void handleAlias(UResource.Key key, UResource.Value value, boolean noFallback) { 1164 Style sourceStyle = styleFromKey(key); 1165 int limit = key.length() - styleSuffixLength(sourceStyle); 1166 DateTimeUnit unit = DateTimeUnit.orNullFromString(key.substring(0, limit)); 1167 if (unit != null) { 1168 // Record the fallback chain for the values. 1169 // At formatting time, limit to 2 levels of fallback. 1170 Style targetStyle = styleFromAlias(value); 1171 if (sourceStyle == targetStyle) { 1172 throw new ICUException("Invalid style fallback from " + sourceStyle + " to itself"); 1173 } 1174 1175 // Check for inconsistent fallbacks. 1176 if (fallbackCache[sourceStyle.ordinal()] == null) { 1177 fallbackCache[sourceStyle.ordinal()] = targetStyle; 1178 } else if (fallbackCache[sourceStyle.ordinal()] != targetStyle) { 1179 throw new ICUException( 1180 "Inconsistent style fallback for style " + sourceStyle + " to " + targetStyle); 1181 } 1182 return; 1183 } 1184 } 1185 1186 @Override 1187 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 1188 // Main entry point to sink 1189 if (value.getType() == ICUResourceBundle.ALIAS) { 1190 return; 1191 } 1192 1193 UResource.Table table = value.getTable(); 1194 // Process each key / value in this table. 1195 for (int i = 0; table.getKeyAndValue(i, key, value); i++) { 1196 if (value.getType() == ICUResourceBundle.ALIAS) { 1197 handleAlias(key, value, noFallback); 1198 } else { 1199 // Remember style and unit for deeper levels. 1200 style = styleFromKey(key); 1201 int limit = key.length() - styleSuffixLength(style); 1202 unit = DateTimeUnit.orNullFromString(key.substring(0, limit)); 1203 if (unit != null) { 1204 // Process only if unitString is in the white list. 1205 consumeTimeUnit(key, value); 1206 } 1207 } 1208 } 1209 } 1210 1211 RelDateTimeDataSink() { 1212 } 1213 } 1214 1215 private static class Loader { 1216 private final ULocale ulocale; 1217 1218 public Loader(ULocale ulocale) { 1219 this.ulocale = ulocale; 1220 } 1221 1222 private String getDateTimePattern(ICUResourceBundle r) { 1223 String calType = r.getStringWithFallback("calendar/default"); 1224 if (calType == null || calType.equals("")) { 1225 calType = "gregorian"; 1226 } 1227 String resourcePath = "calendar/" + calType + "/DateTimePatterns"; 1228 ICUResourceBundle patternsRb = r.findWithFallback(resourcePath); 1229 if (patternsRb == null && calType.equals("gregorian")) { 1230 // Try with gregorian. 1231 patternsRb = r.findWithFallback("calendar/gregorian/DateTimePatterns"); 1232 } 1233 if (patternsRb == null || patternsRb.getSize() < 9) { 1234 // Undefined or too few elements. 1235 return "{1} {0}"; 1236 } else { 1237 int elementType = patternsRb.get(8).getType(); 1238 if (elementType == UResourceBundle.ARRAY) { 1239 return patternsRb.get(8).getString(0); 1240 } else { 1241 return patternsRb.getString(8); 1242 } 1243 } 1244 } 1245 1246 public RelativeDateTimeFormatterData load() { 1247 // Sink for traversing data. 1248 RelDateTimeDataSink sink = new RelDateTimeDataSink(); 1249 1250 ICUResourceBundle r = (ICUResourceBundle)UResourceBundle. 1251 getBundleInstance(ICUData.ICU_BASE_NAME, ulocale); 1252 r.getAllItemsWithFallback("fields", sink); 1253 1254 // Check fallbacks array for loops or too many levels. 1255 for (Style testStyle : Style.values()) { 1256 Style newStyle1 = fallbackCache[testStyle.ordinal()]; 1257 // Data loading guaranteed newStyle1 != testStyle. 1258 if (newStyle1 != null) { 1259 Style newStyle2 = fallbackCache[newStyle1.ordinal()]; 1260 if (newStyle2 != null) { 1261 // No fallback should take more than 2 steps. 1262 if (fallbackCache[newStyle2.ordinal()] != null) { 1263 throw new IllegalStateException("Style fallback too deep"); 1264 } 1265 } 1266 } 1267 } 1268 1269 return new RelativeDateTimeFormatterData( 1270 sink.qualitativeUnitMap, sink.styleRelUnitPatterns, 1271 getDateTimePattern(r)); 1272 } 1273 } 1274 1275 private static final Cache cache = new Cache(); 1276} 1277