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