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