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