1/*
2 *******************************************************************************
3 * Copyright (C) 2013-2016, International Business Machines Corporation and
4 * others. All Rights Reserved.
5 *******************************************************************************
6 */
7package com.ibm.icu.text;
8
9import java.util.EnumMap;
10import java.util.Locale;
11
12import com.ibm.icu.impl.CalendarData;
13import com.ibm.icu.impl.DontCareFieldPosition;
14import com.ibm.icu.impl.ICUCache;
15import com.ibm.icu.impl.ICUResourceBundle;
16import com.ibm.icu.impl.SimpleCache;
17import com.ibm.icu.impl.SimplePatternFormatter;
18import com.ibm.icu.impl.StandardPlural;
19import com.ibm.icu.lang.UCharacter;
20import com.ibm.icu.util.Calendar;
21import com.ibm.icu.util.ICUException;
22import com.ibm.icu.util.ULocale;
23import com.ibm.icu.impl.UResource;
24import com.ibm.icu.util.UResourceBundle;
25
26
27/**
28 * Formats simple relative dates. There are two types of relative dates that
29 * it handles:
30 * <ul>
31 *   <li>relative dates with a quantity e.g "in 5 days"</li>
32 *   <li>relative dates without a quantity e.g "next Tuesday"</li>
33 * </ul>
34 * <p>
35 * This API is very basic and is intended to be a building block for more
36 * fancy APIs. The caller tells it exactly what to display in a locale
37 * independent way. While this class automatically provides the correct plural
38 * forms, the grammatical form is otherwise as neutral as possible. It is the
39 * caller's responsibility to handle cut-off logic such as deciding between
40 * displaying "in 7 days" or "in 1 week." This API supports relative dates
41 * involving one single unit. This API does not support relative dates
42 * involving compound units.
43 * e.g "in 5 days and 4 hours" nor does it support parsing.
44 * This class is both immutable and thread-safe.
45 * <p>
46 * Here are some examples of use:
47 * <blockquote>
48 * <pre>
49 * RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance();
50 * fmt.format(1, Direction.NEXT, RelativeUnit.DAYS); // "in 1 day"
51 * fmt.format(3, Direction.NEXT, RelativeUnit.DAYS); // "in 3 days"
52 * fmt.format(3.2, Direction.LAST, RelativeUnit.YEARS); // "3.2 years ago"
53 *
54 * fmt.format(Direction.LAST, AbsoluteUnit.SUNDAY); // "last Sunday"
55 * fmt.format(Direction.THIS, AbsoluteUnit.SUNDAY); // "this Sunday"
56 * fmt.format(Direction.NEXT, AbsoluteUnit.SUNDAY); // "next Sunday"
57 * fmt.format(Direction.PLAIN, AbsoluteUnit.SUNDAY); // "Sunday"
58 *
59 * fmt.format(Direction.LAST, AbsoluteUnit.DAY); // "yesterday"
60 * fmt.format(Direction.THIS, AbsoluteUnit.DAY); // "today"
61 * fmt.format(Direction.NEXT, AbsoluteUnit.DAY); // "tomorrow"
62 *
63 * fmt.format(Direction.PLAIN, AbsoluteUnit.NOW); // "now"
64 * </pre>
65 * </blockquote>
66 * <p>
67 * In the future, we may add more forms, such as abbreviated/short forms
68 * (3 secs ago), and relative day periods ("yesterday afternoon"), etc.
69 *
70 * @stable ICU 53
71 */
72public final class RelativeDateTimeFormatter {
73
74    /**
75     * The formatting style
76     * @stable ICU 54
77     *
78     */
79    public static enum Style {
80
81        /**
82         * Everything spelled out.
83         * @stable ICU 54
84         */
85        LONG,
86
87        /**
88         * Abbreviations used when possible.
89         * @stable ICU 54
90         */
91        SHORT,
92
93        /**
94         * Use single letters when possible.
95         * @stable ICU 54
96         */
97        NARROW;
98
99        private static final int INDEX_COUNT = 3;  // NARROW.ordinal() + 1
100    }
101
102    /**
103     * Represents the unit for formatting a relative date. e.g "in 5 days"
104     * or "in 3 months"
105     * @stable ICU 53
106     */
107    public static enum RelativeUnit {
108
109        /**
110         * Seconds
111         * @stable ICU 53
112         */
113        SECONDS,
114
115        /**
116         * Minutes
117         * @stable ICU 53
118         */
119        MINUTES,
120
121       /**
122        * Hours
123        * @stable ICU 53
124        */
125        HOURS,
126
127        /**
128         * Days
129         * @stable ICU 53
130         */
131        DAYS,
132
133        /**
134         * Weeks
135         * @stable ICU 53
136         */
137        WEEKS,
138
139        /**
140         * Months
141         * @stable ICU 53
142         */
143        MONTHS,
144
145        /**
146         * Years
147         * @stable ICU 53
148         */
149        YEARS,
150
151        /**
152         * Quarters
153         * @internal TODO: propose for addition in ICU 57
154         * @deprecated This API is ICU internal only.
155         */
156        @Deprecated
157        QUARTERS,
158    }
159
160    /**
161     * Represents an absolute unit.
162     * @stable ICU 53
163     */
164    public static enum AbsoluteUnit {
165
166       /**
167        * Sunday
168        * @stable ICU 53
169        */
170        SUNDAY,
171
172        /**
173         * Monday
174         * @stable ICU 53
175         */
176        MONDAY,
177
178        /**
179         * Tuesday
180         * @stable ICU 53
181         */
182        TUESDAY,
183
184        /**
185         * Wednesday
186         * @stable ICU 53
187         */
188        WEDNESDAY,
189
190        /**
191         * Thursday
192         * @stable ICU 53
193         */
194        THURSDAY,
195
196        /**
197         * Friday
198         * @stable ICU 53
199         */
200        FRIDAY,
201
202        /**
203         * Saturday
204         * @stable ICU 53
205         */
206        SATURDAY,
207
208        /**
209         * Day
210         * @stable ICU 53
211         */
212        DAY,
213
214        /**
215         * Week
216         * @stable ICU 53
217         */
218        WEEK,
219
220        /**
221         * Month
222         * @stable ICU 53
223         */
224        MONTH,
225
226        /**
227         * Year
228         * @stable ICU 53
229         */
230        YEAR,
231
232        /**
233         * Now
234         * @stable ICU 53
235         */
236        NOW,
237
238        /**
239         * Quarter
240         * @internal TODO: propose for addition in ICU 57
241         * @deprecated This API is ICU internal only.
242         */
243        @Deprecated
244        QUARTER,
245      }
246
247      /**
248       * Represents a direction for an absolute unit e.g "Next Tuesday"
249       * or "Last Tuesday"
250       * @stable ICU 53
251       */
252      public static enum Direction {
253          /**
254           * Two before. Not fully supported in every locale
255           * @stable ICU 53
256           */
257          LAST_2,
258
259          /**
260           * Last
261           * @stable ICU 53
262           */
263          LAST,
264
265          /**
266           * This
267           * @stable ICU 53
268           */
269          THIS,
270
271          /**
272           * Next
273           * @stable ICU 53
274           */
275          NEXT,
276
277          /**
278           * Two after. Not fully supported in every locale
279           * @stable ICU 53
280           */
281          NEXT_2,
282
283          /**
284           * Plain, which means the absence of a qualifier
285           * @stable ICU 53
286           */
287          PLAIN;
288      }
289
290    /**
291     * Returns a RelativeDateTimeFormatter for the default locale.
292     * @stable ICU 53
293     */
294    public static RelativeDateTimeFormatter getInstance() {
295        return getInstance(ULocale.getDefault(), null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
296    }
297
298    /**
299     * Returns a RelativeDateTimeFormatter for a particular locale.
300     *
301     * @param locale the locale.
302     * @return An instance of RelativeDateTimeFormatter.
303     * @stable ICU 53
304     */
305    public static RelativeDateTimeFormatter getInstance(ULocale locale) {
306        return getInstance(locale, null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
307    }
308
309    /**
310     * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale}.
311     *
312     * @param locale the {@link java.util.Locale}.
313     * @return An instance of RelativeDateTimeFormatter.
314     * @stable ICU 54
315     */
316    public static RelativeDateTimeFormatter getInstance(Locale locale) {
317        return getInstance(ULocale.forLocale(locale));
318    }
319
320    /**
321     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
322     * NumberFormat object.
323     *
324     * @param locale the locale
325     * @param nf the number format object. It is defensively copied to ensure thread-safety
326     * and immutability of this class.
327     * @return An instance of RelativeDateTimeFormatter.
328     * @stable ICU 53
329     */
330    public static RelativeDateTimeFormatter getInstance(ULocale locale, NumberFormat nf) {
331        return getInstance(locale, nf, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
332    }
333
334    /**
335     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
336     * NumberFormat object, style, and capitalization context
337     *
338     * @param locale the locale
339     * @param nf the number format object. It is defensively copied to ensure thread-safety
340     * and immutability of this class. May be null.
341     * @param style the style.
342     * @param capitalizationContext the capitalization context.
343     * @stable ICU 54
344     */
345    public static RelativeDateTimeFormatter getInstance(
346            ULocale locale,
347            NumberFormat nf,
348            Style style,
349            DisplayContext capitalizationContext) {
350        RelativeDateTimeFormatterData data = cache.get(locale);
351        if (nf == null) {
352            nf = NumberFormat.getInstance(locale);
353        } else {
354            nf = (NumberFormat) nf.clone();
355        }
356        return new RelativeDateTimeFormatter(
357                data.qualitativeUnitMap,
358                data.relUnitPatternMap,
359                new MessageFormat(data.dateTimePattern),
360                PluralRules.forLocale(locale),
361                nf,
362                style,
363                capitalizationContext,
364                capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ?
365                    BreakIterator.getSentenceInstance(locale) : null,
366                locale);
367    }
368
369    /**
370     * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale} that uses a
371     * particular NumberFormat object.
372     *
373     * @param locale the {@link java.util.Locale}
374     * @param nf the number format object. It is defensively copied to ensure thread-safety
375     * and immutability of this class.
376     * @return An instance of RelativeDateTimeFormatter.
377     * @stable ICU 54
378     */
379    public static RelativeDateTimeFormatter getInstance(Locale locale, NumberFormat nf) {
380        return getInstance(ULocale.forLocale(locale), nf);
381    }
382
383    /**
384     * Formats a relative date with a quantity such as "in 5 days" or
385     * "3 months ago"
386     * @param quantity The numerical amount e.g 5. This value is formatted
387     * according to this object's {@link NumberFormat} object.
388     * @param direction NEXT means a future relative date; LAST means a past
389     * relative date.
390     * @param unit the unit e.g day? month? year?
391     * @return the formatted string
392     * @throws IllegalArgumentException if direction is something other than
393     * NEXT or LAST.
394     * @stable ICU 53
395     */
396    public String format(double quantity, Direction direction, RelativeUnit unit) {
397        if (direction != Direction.LAST && direction != Direction.NEXT) {
398            throw new IllegalArgumentException("direction must be NEXT or LAST");
399        }
400        String result;
401        int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0);
402
403        // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this
404        // class we must guarantee that only one thread at a time uses our numberFormat.
405        synchronized (numberFormat) {
406            StringBuffer formatStr = new StringBuffer();
407            DontCareFieldPosition fieldPosition = DontCareFieldPosition.INSTANCE;
408            StandardPlural pluralForm = QuantityFormatter.selectPlural(quantity,
409                    numberFormat, pluralRules, formatStr, fieldPosition);
410
411            String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm);
412            result = SimplePatternFormatter.formatCompiledPattern(formatter, formatStr);
413        }
414        return adjustForContext(result);
415
416    }
417
418    private int[] styleToDateFormatSymbolsWidth = {
419                DateFormatSymbols.WIDE, DateFormatSymbols.SHORT, DateFormatSymbols.NARROW
420    };
421
422    /**
423     * Formats a relative date without a quantity.
424     * @param direction NEXT, LAST, THIS, etc.
425     * @param unit e.g SATURDAY, DAY, MONTH
426     * @return the formatted string. If direction has a value that is documented as not being
427     *  fully supported in every locale (for example NEXT_2 or LAST_2) then this function may
428     *  return null to signal that no formatted string is available.
429     * @throws IllegalArgumentException if the direction is incompatible with
430     * unit this can occur with NOW which can only take PLAIN.
431     * @stable ICU 53
432     */
433    public String format(Direction direction, AbsoluteUnit unit) {
434        if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) {
435            throw new IllegalArgumentException("NOW can only accept direction PLAIN.");
436        }
437        String result;
438        // Get plain day of week names from DateFormatSymbols.
439        if ((direction == Direction.PLAIN) &&  (AbsoluteUnit.SUNDAY.ordinal() <= unit.ordinal() &&
440                unit.ordinal() <= AbsoluteUnit.SATURDAY.ordinal())) {
441            // Convert from AbsoluteUnit days to Calendar class indexing.
442            int dateSymbolsDayOrdinal = (unit.ordinal() - AbsoluteUnit.SUNDAY.ordinal()) + Calendar.SUNDAY;
443            String[] dayNames =
444                    dateFormatSymbols.getWeekdays(DateFormatSymbols.STANDALONE,
445                    styleToDateFormatSymbolsWidth[style.ordinal()]);
446            result = dayNames[dateSymbolsDayOrdinal];
447        } else {
448            // Not PLAIN, or not a weekday.
449            result = getAbsoluteUnitString(style, unit, direction);
450        }
451        return result != null ? adjustForContext(result) : null;
452    }
453
454    /**
455     * Gets the string value from qualitativeUnitMap with fallback based on style.
456     * @param style
457     * @param unit
458     * @param direction
459     * @return
460     */
461    private String getAbsoluteUnitString(Style style, AbsoluteUnit unit, Direction direction) {
462        EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap;
463        EnumMap<Direction, String> dirMap;
464
465        do {
466            unitMap = qualitativeUnitMap.get(style);
467            if (unitMap != null) {
468                dirMap = unitMap.get(unit);
469                if (dirMap != null) {
470                    String result = dirMap.get(direction);
471                    if (result != null) {
472                        return result;
473                    }
474                }
475
476            }
477
478            // Consider other styles from alias fallback.
479            // Data loading guaranteed no endless loops.
480        } while ((style = fallbackCache[style.ordinal()]) != null);
481        return null;
482    }
483
484    /**
485     * Combines a relative date string and a time string in this object's
486     * locale. This is done with the same date-time separator used for the
487     * default calendar in this locale.
488     * @param relativeDateString the relative date e.g 'yesterday'
489     * @param timeString the time e.g '3:45'
490     * @return the date and time concatenated according to the default
491     * calendar in this locale e.g 'yesterday, 3:45'
492     * @stable ICU 53
493     */
494    public String combineDateAndTime(String relativeDateString, String timeString) {
495        return this.combinedDateAndTime.format(
496            new Object[]{timeString, relativeDateString}, new StringBuffer(), null).toString();
497    }
498
499    /**
500     * Returns a copy of the NumberFormat this object is using.
501     * @return A copy of the NumberFormat.
502     * @stable ICU 53
503     */
504    public NumberFormat getNumberFormat() {
505        // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this
506        // class we must guarantee that only one thread at a time uses our numberFormat.
507        synchronized (numberFormat) {
508            return (NumberFormat) numberFormat.clone();
509        }
510    }
511
512    /**
513     * Return capitalization context.
514     *
515     * @stable ICU 54
516     */
517    public DisplayContext getCapitalizationContext() {
518        return capitalizationContext;
519    }
520
521    /**
522     * Return style
523     *
524     * @stable ICU 54
525     */
526    public Style getFormatStyle() {
527        return style;
528    }
529
530    private String adjustForContext(String originalFormattedString) {
531        if (breakIterator == null || originalFormattedString.length() == 0
532                || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) {
533            return originalFormattedString;
534        }
535        synchronized (breakIterator) {
536            return UCharacter.toTitleCase(
537                    locale,
538                    originalFormattedString,
539                    breakIterator,
540                    UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT);
541        }
542    }
543
544    private RelativeDateTimeFormatter(
545            EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
546            EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap,
547            MessageFormat combinedDateAndTime,
548            PluralRules pluralRules,
549            NumberFormat numberFormat,
550            Style style,
551            DisplayContext capitalizationContext,
552            BreakIterator breakIterator,
553            ULocale locale) {
554        this.qualitativeUnitMap = qualitativeUnitMap;
555        this.patternMap = patternMap;
556        this.combinedDateAndTime = combinedDateAndTime;
557        this.pluralRules = pluralRules;
558        this.numberFormat = numberFormat;
559        this.style = style;
560        if (capitalizationContext.type() != DisplayContext.Type.CAPITALIZATION) {
561            throw new IllegalArgumentException(capitalizationContext.toString());
562        }
563        this.capitalizationContext = capitalizationContext;
564        this.breakIterator = breakIterator;
565        this.locale = locale;
566        this.dateFormatSymbols = new DateFormatSymbols(locale);
567    }
568
569    private String getRelativeUnitPluralPattern(
570            Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) {
571        if (pluralForm != StandardPlural.OTHER) {
572            String formatter = getRelativeUnitPattern(style, unit, pastFutureIndex, pluralForm);
573            if (formatter != null) {
574                return formatter;
575            }
576        }
577        return getRelativeUnitPattern(style, unit, pastFutureIndex, StandardPlural.OTHER);
578    }
579
580    private String getRelativeUnitPattern(
581            Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) {
582        int pluralIndex = pluralForm.ordinal();
583        do {
584            EnumMap<RelativeUnit, String[][]> unitMap = patternMap.get(style);
585            if (unitMap != null) {
586                String[][] spfCompiledPatterns = unitMap.get(unit);
587                if (spfCompiledPatterns != null) {
588                    if (spfCompiledPatterns[pastFutureIndex][pluralIndex] != null) {
589                        return spfCompiledPatterns[pastFutureIndex][pluralIndex];
590                    }
591                }
592
593            }
594
595            // Consider other styles from alias fallback.
596            // Data loading guaranteed no endless loops.
597        } while ((style = fallbackCache[style.ordinal()]) != null);
598        return null;
599    }
600
601    private final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap;
602    private final EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap;
603
604    private final MessageFormat combinedDateAndTime;
605    private final PluralRules pluralRules;
606    private final NumberFormat numberFormat;
607
608    private final Style style;
609    private final DisplayContext capitalizationContext;
610    private final BreakIterator breakIterator;
611    private final ULocale locale;
612
613    private final DateFormatSymbols dateFormatSymbols;
614
615    private static final Style fallbackCache[] = new Style[Style.INDEX_COUNT];
616
617    private static class RelativeDateTimeFormatterData {
618        public RelativeDateTimeFormatterData(
619                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
620                EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap,
621                String dateTimePattern) {
622            this.qualitativeUnitMap = qualitativeUnitMap;
623            this.relUnitPatternMap = relUnitPatternMap;
624
625            this.dateTimePattern = dateTimePattern;
626        }
627
628        public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap;
629        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap;
630        public final String dateTimePattern;  // Example: "{1}, {0}"
631    }
632
633    private static class Cache {
634        private final ICUCache<String, RelativeDateTimeFormatterData> cache =
635            new SimpleCache<String, RelativeDateTimeFormatterData>();
636
637        public RelativeDateTimeFormatterData get(ULocale locale) {
638            String key = locale.toString();
639            RelativeDateTimeFormatterData result = cache.get(key);
640            if (result == null) {
641                result = new Loader(locale).load();
642                cache.put(key, result);
643            }
644            return result;
645        }
646    }
647
648    private static Direction keyToDirection(UResource.Key key) {
649        if (key.contentEquals("-2")) {
650            return Direction.LAST_2;
651        }
652        if (key.contentEquals("-1")) {
653            return Direction.LAST;
654        }
655        if (key.contentEquals("0")) {
656            return Direction.THIS;
657        }
658        if (key.contentEquals("1")) {
659            return Direction.NEXT;
660        }
661        if (key.contentEquals("2")) {
662            return Direction.NEXT_2;
663        }
664        return null;
665    }
666
667    /**
668     * Sink for enumerating all of the relative data time formatter names.
669     * Contains inner sink classes, each one corresponding to a type of resource table.
670     * The outer sink handles the top-level 'fields'.
671     *
672     * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
673     * Only store a value if it is still missing, that is, it has not been overridden.
674     *
675     * C++: Each inner sink class has a reference to the main outer sink.
676     * Java: Use non-static inner classes instead.
677     */
678    private static final class RelDateTimeFmtDataSink extends UResource.TableSink {
679        // For white list of units to handle in RelativeDateTimeFormatter.
680        private static enum DateTimeUnit {
681            SECOND(RelativeUnit.SECONDS, null),
682            MINUTE(RelativeUnit.MINUTES, null),
683            HOUR(RelativeUnit.HOURS, null),
684            DAY(RelativeUnit.DAYS, AbsoluteUnit.DAY),
685            WEEK(RelativeUnit.WEEKS, AbsoluteUnit.WEEK),
686            MONTH(RelativeUnit.MONTHS, AbsoluteUnit.MONTH),
687            QUARTER(RelativeUnit.QUARTERS, AbsoluteUnit.QUARTER),
688            YEAR(RelativeUnit.YEARS, AbsoluteUnit.YEAR),
689            SUNDAY(null, AbsoluteUnit.SUNDAY),
690            MONDAY(null, AbsoluteUnit.MONDAY),
691            TUESDAY(null, AbsoluteUnit.TUESDAY),
692            WEDNESDAY(null, AbsoluteUnit.WEDNESDAY),
693            THURSDAY(null, AbsoluteUnit.THURSDAY),
694            FRIDAY(null, AbsoluteUnit.FRIDAY),
695            SATURDAY(null, AbsoluteUnit.SATURDAY);
696
697            RelativeUnit relUnit;
698            AbsoluteUnit absUnit;
699
700            DateTimeUnit(RelativeUnit relUnit, AbsoluteUnit absUnit) {
701                this.relUnit = relUnit;
702                this.absUnit = absUnit;
703            }
704
705            private static final DateTimeUnit orNullFromString(CharSequence keyword) {
706                // Quick check from string to enum.
707                switch (keyword.length()) {
708                case 3:
709                    if ("day".contentEquals(keyword)) {
710                        return DAY;
711                    } else if ("sun".contentEquals(keyword)) {
712                        return SUNDAY;
713                    } else if ("mon".contentEquals(keyword)) {
714                        return MONDAY;
715                    } else if ("tue".contentEquals(keyword)) {
716                        return TUESDAY;
717                    } else if ("wed".contentEquals(keyword)) {
718                        return WEDNESDAY;
719                    } else if ("thu".contentEquals(keyword)) {
720                        return THURSDAY;
721                    }    else if ("fri".contentEquals(keyword)) {
722                        return FRIDAY;
723                    } else if ("sat".contentEquals(keyword)) {
724                        return SATURDAY;
725                    }
726                    break;
727                case 4:
728                    if ("hour".contentEquals(keyword)) {
729                        return HOUR;
730                    } else if ("week".contentEquals(keyword)) {
731                        return WEEK;
732                    } else if ("year".contentEquals(keyword)) {
733                        return YEAR;
734                    }
735                    break;
736                case 5:
737                    if ("month".contentEquals(keyword)) {
738                        return MONTH;
739                    }
740                    break;
741                case 6:
742                    if ("minute".contentEquals(keyword)) {
743                        return MINUTE;
744                    }else if ("second".contentEquals(keyword)) {
745                        return SECOND;
746                    }
747                    break;
748                case 7:
749                    if ("quarter".contentEquals(keyword)) {
750                        return QUARTER;  // TODO: Check @provisional
751                    }
752                    break;
753                default:
754                    break;
755                }
756                return null;
757            }
758        }
759
760        EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap =
761                new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class);
762        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> styleRelUnitPatterns =
763                new EnumMap<Style, EnumMap<RelativeUnit, String[][]>>(Style.class);
764
765        private ULocale ulocale = null;
766
767        StringBuilder sb = new StringBuilder();
768
769        public RelDateTimeFmtDataSink(ULocale locale) {
770            ulocale = locale;
771        }
772
773        // Values keep between levels of parsing the CLDR data.
774        int pastFutureIndex;
775        Style style;                        // {LONG, SHORT, NARROW} Derived from unit key string.
776        DateTimeUnit unit;                  // From the unit key string, with the style (e.g., "-short") separated out.
777
778        private Style styleFromKey(UResource.Key key) {
779            if (key.endsWith("-short")) {
780                return Style.SHORT;
781            } else if (key.endsWith("-narrow")) {
782                return Style.NARROW;
783            } else {
784                return Style.LONG;
785            }
786        }
787
788        private Style styleFromAlias(UResource.Value value) {
789                String s = value.getAliasString();
790                if (s.endsWith("-short")) {
791                    return Style.SHORT;
792                } else if (s.endsWith("-narrow")) {
793                    return Style.NARROW;
794                } else {
795                    return Style.LONG;
796                }
797        }
798
799        private static int styleSuffixLength(Style style) {
800            switch (style) {
801            case SHORT: return 6;
802            case NARROW: return 7;
803            default: return 0;
804            }
805        }
806
807        @Override
808        public void put(UResource.Key key, UResource.Value value) {
809            // Parse and store aliases.
810            if (value.getType() != ICUResourceBundle.ALIAS) { return; }
811
812            Style sourceStyle = styleFromKey(key);
813            int limit = key.length() - styleSuffixLength(sourceStyle);
814            DateTimeUnit unit = DateTimeUnit.orNullFromString(key.substring(0, limit));
815            if (unit != null) {
816                // Record the fallback chain for the values.
817                // At formatting time, limit to 2 levels of fallback.
818                Style targetStyle = styleFromAlias(value);
819                if (sourceStyle == targetStyle) {
820                    throw new ICUException("Invalid style fallback from " + sourceStyle + " to itself");
821                }
822
823                // Check for inconsistent fallbacks.
824                if (fallbackCache[sourceStyle.ordinal()] == null) {
825                    fallbackCache[sourceStyle.ordinal()] = targetStyle;
826                } else if (fallbackCache[sourceStyle.ordinal()] != targetStyle) {
827                    throw new ICUException(
828                            "Inconsistent style fallback for style " + sourceStyle + " to " + targetStyle);
829                }
830            }
831        }
832
833        @Override
834        public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
835            // Get base unit and style from the key value.
836            style = styleFromKey(key);
837            int limit = key.length() - styleSuffixLength(style);
838            String unitString = key.substring(0, limit);
839
840            // Process only if unitString is in the white list.
841            unit = DateTimeUnit.orNullFromString(unitString);
842            if (unit == null) {
843                return null;
844            }
845            return unitSink;  // Continue parsing this path.
846        }
847
848        // Sinks for additional levels under /fields/*/relative/ and /fields/*/relativeTime/
849
850        // Sets values under relativeTime paths, e.g., "hour/relativeTime/future/one"
851        class RelativeTimeDetailSink extends UResource.TableSink {
852            @Override
853            public void put(UResource.Key key, UResource.Value value) {
854                /* Make two lists of simplePatternFmtList, one for past and one for future.
855                 *  Set a SimplePatternFormatter for the <style, relative unit, plurality>
856                 *
857                 * Fill in values for the particular plural given, e.g., ONE, FEW, OTHER, etc.
858                 */
859                EnumMap<RelativeUnit, String[][]> unitPatterns  =
860                        styleRelUnitPatterns.get(style);
861                if (unitPatterns == null) {
862                    unitPatterns = new EnumMap<RelativeUnit, String[][]>(RelativeUnit.class);
863                    styleRelUnitPatterns.put(style, unitPatterns);
864                }
865                String[][] patterns = unitPatterns.get(unit.relUnit);
866                if (patterns == null) {
867                    patterns = new String[2][StandardPlural.COUNT];
868                    unitPatterns.put(unit.relUnit, patterns);
869                }
870                int pluralIndex = StandardPlural.indexFromString(key.toString());
871                if (patterns[pastFutureIndex][pluralIndex] == null) {
872                    patterns[pastFutureIndex][pluralIndex] =
873                            SimplePatternFormatter.compileToStringMinMaxPlaceholders(value.getString(),
874                                    sb, 0, 1);
875                }
876            }
877        }
878        RelativeTimeDetailSink relativeTimeDetailSink = new RelativeTimeDetailSink();
879
880        // Handles "relativeTime" entries, e.g., under "day", "hour", "minute", "minute-short", etc.
881        class RelativeTimeSink extends UResource.TableSink {
882            @Override
883            public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
884                if (key.contentEquals("past")) {
885                    pastFutureIndex = 0;
886                } else if (key.contentEquals("future")) {
887                    pastFutureIndex = 1;
888                } else {
889                    return null;
890                }
891                if (unit.relUnit == null) {
892                    return null;
893                }
894                return relativeTimeDetailSink;
895            }
896        }
897        RelativeTimeSink relativeTimeSink = new RelativeTimeSink();
898
899        // Handles "relative" entries, e.g., under "day", "day-short", "fri", "fri-narrow", "fri-short", etc.
900        class RelativeSink extends UResource.TableSink {
901            @Override
902            public void put(UResource.Key key, UResource.Value value) {
903
904                EnumMap<AbsoluteUnit, EnumMap<Direction, String>> absMap = qualitativeUnitMap.get(style);
905
906                if (unit.relUnit == RelativeUnit.SECONDS) {
907                    if (key.contentEquals("0")) {
908                        // Handle Zero seconds for "now".
909                        EnumMap<Direction, String> unitStrings = absMap.get(AbsoluteUnit.NOW);
910                        if (unitStrings == null) {
911                            unitStrings = new EnumMap<Direction, String>(Direction.class);
912                            absMap.put(AbsoluteUnit.NOW, unitStrings);
913                        }
914                        if (unitStrings.get(Direction.PLAIN) == null) {
915                            unitStrings.put(Direction.PLAIN, value.getString());
916                        }
917                        return;
918                    }
919                }
920                Direction keyDirection = keyToDirection(key);
921                if (keyDirection == null) {
922                    return;
923                }
924                AbsoluteUnit absUnit = unit.absUnit;
925                if (absUnit == null) {
926                    return;
927                }
928
929                if (absMap == null) {
930                    absMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class);
931                    qualitativeUnitMap.put(style, absMap);
932                }
933                EnumMap<Direction, String> dirMap = absMap.get(absUnit);
934                if (dirMap == null) {
935                    dirMap = new EnumMap<Direction, String>(Direction.class);
936                    absMap.put(absUnit, dirMap);
937                }
938                if (dirMap.get(keyDirection) == null) {
939                    // Do not override values already entered.
940                    dirMap.put(keyDirection, value.getString());
941                }
942            }
943        }
944        RelativeSink relativeSink = new RelativeSink();
945
946        // Handles entries under units, recognizing "relative" and "relativeTime" entries.
947        class UnitSink extends UResource.TableSink {
948            @Override
949            public void put(UResource.Key key, UResource.Value value) {
950                if (key.contentEquals("dn")) {
951                    // Handle Display Name for PLAIN direction for some units.
952                    AbsoluteUnit absUnit = unit.absUnit;
953                    if (absUnit == null) {
954                        return;  // Not interesting.
955                    }
956                    EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap =
957                            qualitativeUnitMap.get(style);
958                    if (unitMap == null) {
959                        unitMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class);
960                        qualitativeUnitMap.put(style, unitMap);
961                    }
962                    EnumMap<Direction,String> dirMap = unitMap.get(absUnit);
963                    if (dirMap == null) {
964                        dirMap = new EnumMap<Direction,String>(Direction.class);
965                        unitMap.put(absUnit, dirMap);
966                    }
967                    if (dirMap.get(Direction.PLAIN) == null) {
968                        String displayName = value.toString();
969                        // TODO(Travis Keep): This is a hack to get around CLDR bug 6818.
970                        if (ulocale.getLanguage().equals("en")) {
971                            displayName = displayName.toLowerCase(Locale.ROOT);
972                        }
973                        dirMap.put(Direction.PLAIN, displayName);
974                    }
975                }
976            }
977
978            @Override
979            public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
980                if (key.contentEquals("relative")) {
981                    return relativeSink;
982                } else if (key.contentEquals("relativeTime")) {
983                    return relativeTimeSink;
984                }
985                return null;
986            }
987        }
988        UnitSink unitSink = new UnitSink();
989    }
990
991    private static class Loader {
992        private final ULocale ulocale;
993
994        public Loader(ULocale ulocale) {
995            this.ulocale = ulocale;
996        }
997
998        public RelativeDateTimeFormatterData load() {
999            // Sink for traversing data.
1000            RelDateTimeFmtDataSink sink = new RelDateTimeFmtDataSink(ulocale);
1001            ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
1002                    getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
1003
1004            // Use sink mechanism to traverse data structure.
1005            r.getAllTableItemsWithFallback("fields", sink);
1006
1007            // Check fallbacks array for loops or too many levels.
1008            for (Style testStyle : Style.values()) {
1009                Style newStyle1 = fallbackCache[testStyle.ordinal()];
1010                // Data loading guaranteed newStyle1 != testStyle.
1011                if (newStyle1 != null) {
1012                    Style newStyle2 = fallbackCache[newStyle1.ordinal()];
1013                    if (newStyle2 != null) {
1014                        // No fallback should take more than 2 steps.
1015                        if (fallbackCache[newStyle2.ordinal()] != null) {
1016                            throw new IllegalStateException("Style fallback too deep");
1017                        }
1018                    }
1019                }
1020            }
1021
1022            // TODO: Replace this use of CalendarData.
1023            CalendarData calData = new CalendarData(
1024                    ulocale, r.getStringWithFallback("calendar/default"));
1025
1026            return new RelativeDateTimeFormatterData(
1027                    sink.qualitativeUnitMap, sink.styleRelUnitPatterns,
1028                    calData.getDateTimePattern());
1029        }
1030    }
1031
1032    private static final Cache cache = new Cache();
1033}
1034