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