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