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