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