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