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