1/*
2 *******************************************************************************
3 * Copyright (C) 2013-2015, 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.ICUCache;
14import com.ibm.icu.impl.ICUResourceBundle;
15import com.ibm.icu.impl.SimpleCache;
16import com.ibm.icu.lang.UCharacter;
17import com.ibm.icu.util.ULocale;
18import com.ibm.icu.util.UResourceBundle;
19
20
21/**
22 * Formats simple relative dates. There are two types of relative dates that
23 * it handles:
24 * <ul>
25 *   <li>relative dates with a quantity e.g "in 5 days"</li>
26 *   <li>relative dates without a quantity e.g "next Tuesday"</li>
27 * </ul>
28 * <p>
29 * This API is very basic and is intended to be a building block for more
30 * fancy APIs. The caller tells it exactly what to display in a locale
31 * independent way. While this class automatically provides the correct plural
32 * forms, the grammatical form is otherwise as neutral as possible. It is the
33 * caller's responsibility to handle cut-off logic such as deciding between
34 * displaying "in 7 days" or "in 1 week." This API supports relative dates
35 * involving one single unit. This API does not support relative dates
36 * involving compound units.
37 * e.g "in 5 days and 4 hours" nor does it support parsing.
38 * This class is both immutable and thread-safe.
39 * <p>
40 * Here are some examples of use:
41 * <blockquote>
42 * <pre>
43 * RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance();
44 * fmt.format(1, Direction.NEXT, RelativeUnit.DAYS); // "in 1 day"
45 * fmt.format(3, Direction.NEXT, RelativeUnit.DAYS); // "in 3 days"
46 * fmt.format(3.2, Direction.LAST, RelativeUnit.YEARS); // "3.2 years ago"
47 *
48 * fmt.format(Direction.LAST, AbsoluteUnit.SUNDAY); // "last Sunday"
49 * fmt.format(Direction.THIS, AbsoluteUnit.SUNDAY); // "this Sunday"
50 * fmt.format(Direction.NEXT, AbsoluteUnit.SUNDAY); // "next Sunday"
51 * fmt.format(Direction.PLAIN, AbsoluteUnit.SUNDAY); // "Sunday"
52 *
53 * fmt.format(Direction.LAST, AbsoluteUnit.DAY); // "yesterday"
54 * fmt.format(Direction.THIS, AbsoluteUnit.DAY); // "today"
55 * fmt.format(Direction.NEXT, AbsoluteUnit.DAY); // "tomorrow"
56 *
57 * fmt.format(Direction.PLAIN, AbsoluteUnit.NOW); // "now"
58 * </pre>
59 * </blockquote>
60 * <p>
61 * In the future, we may add more forms, such as abbreviated/short forms
62 * (3 secs ago), and relative day periods ("yesterday afternoon"), etc.
63 *
64 * @stable ICU 53
65 */
66public final class RelativeDateTimeFormatter {
67
68    /**
69     * The formatting style
70     * @draft ICU 54
71     * @provisional This API might change or be removed in a future release.
72     *
73     */
74    public static enum Style {
75
76        /**
77         * Everything spelled out.
78         * @draft ICU 54
79         * @provisional This API might change or be removed in a future release.
80         */
81        LONG,
82
83        /**
84         * Abbreviations used when possible.
85         * @draft ICU 54
86         * @provisional This API might change or be removed in a future release.
87         */
88        SHORT,
89
90        /**
91         * Use single letters when possible.
92         * @draft ICU 54
93         * @provisional This API might change or be removed in a future release.
94         */
95        NARROW,
96    }
97
98    /**
99     * Represents the unit for formatting a relative date. e.g "in 5 days"
100     * or "in 3 months"
101     * @stable ICU 53
102     */
103    public static enum RelativeUnit {
104
105        /**
106         * Seconds
107         * @stable ICU 53
108         */
109        SECONDS,
110
111        /**
112         * Minutes
113         * @stable ICU 53
114         */
115        MINUTES,
116
117       /**
118        * Hours
119        * @stable ICU 53
120        */
121        HOURS,
122
123        /**
124         * Days
125         * @stable ICU 53
126         */
127        DAYS,
128
129        /**
130         * Weeks
131         * @stable ICU 53
132         */
133        WEEKS,
134
135        /**
136         * Months
137         * @stable ICU 53
138         */
139        MONTHS,
140
141        /**
142         * Years
143         * @stable ICU 53
144         */
145        YEARS,
146    }
147
148    /**
149     * Represents an absolute unit.
150     * @stable ICU 53
151     */
152    public static enum AbsoluteUnit {
153
154       /**
155        * Sunday
156        * @stable ICU 53
157        */
158        SUNDAY,
159
160        /**
161         * Monday
162         * @stable ICU 53
163         */
164        MONDAY,
165
166        /**
167         * Tuesday
168         * @stable ICU 53
169         */
170        TUESDAY,
171
172        /**
173         * Wednesday
174         * @stable ICU 53
175         */
176        WEDNESDAY,
177
178        /**
179         * Thursday
180         * @stable ICU 53
181         */
182        THURSDAY,
183
184        /**
185         * Friday
186         * @stable ICU 53
187         */
188        FRIDAY,
189
190        /**
191         * Saturday
192         * @stable ICU 53
193         */
194        SATURDAY,
195
196        /**
197         * Day
198         * @stable ICU 53
199         */
200        DAY,
201
202        /**
203         * Week
204         * @stable ICU 53
205         */
206        WEEK,
207
208        /**
209         * Month
210         * @stable ICU 53
211         */
212        MONTH,
213
214        /**
215         * Year
216         * @stable ICU 53
217         */
218        YEAR,
219
220        /**
221         * Now
222         * @stable ICU 53
223         */
224        NOW,
225      }
226
227      /**
228       * Represents a direction for an absolute unit e.g "Next Tuesday"
229       * or "Last Tuesday"
230       * @stable ICU 53
231       */
232      public static enum Direction {
233
234          /**
235           * Two before. Not fully supported in every locale
236           * @stable ICU 53
237           */
238          LAST_2,
239
240          /**
241           * Last
242           * @stable ICU 53
243           */
244          LAST,
245
246          /**
247           * This
248           * @stable ICU 53
249           */
250          THIS,
251
252          /**
253           * Next
254           * @stable ICU 53
255           */
256          NEXT,
257
258          /**
259           * Two after. Not fully supported in every locale
260           * @stable ICU 53
261           */
262          NEXT_2,
263
264          /**
265           * Plain, which means the absence of a qualifier
266           * @stable ICU 53
267           */
268          PLAIN;
269      }
270
271    /**
272     * Returns a RelativeDateTimeFormatter for the default locale.
273     * @stable ICU 53
274     */
275    public static RelativeDateTimeFormatter getInstance() {
276        return getInstance(ULocale.getDefault(), null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
277    }
278
279    /**
280     * Returns a RelativeDateTimeFormatter for a particular locale.
281     *
282     * @param locale the locale.
283     * @return An instance of RelativeDateTimeFormatter.
284     * @stable ICU 53
285     */
286    public static RelativeDateTimeFormatter getInstance(ULocale locale) {
287        return getInstance(locale, null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
288    }
289
290    /**
291     * Returns a RelativeDateTimeFormatter for a particular JDK locale.
292     *
293     * @param locale the JDK locale.
294     * @return An instance of RelativeDateTimeFormatter.
295     * @draft ICU 54
296     * @provisional This API might change or be removed in a future release.
297     */
298    public static RelativeDateTimeFormatter getInstance(Locale locale) {
299        return getInstance(ULocale.forLocale(locale));
300    }
301
302    /**
303     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
304     * NumberFormat object.
305     *
306     * @param locale the locale
307     * @param nf the number format object. It is defensively copied to ensure thread-safety
308     * and immutability of this class.
309     * @return An instance of RelativeDateTimeFormatter.
310     * @stable ICU 53
311     */
312    public static RelativeDateTimeFormatter getInstance(ULocale locale, NumberFormat nf) {
313        return getInstance(locale, nf, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
314    }
315
316    /**
317     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
318     * NumberFormat object, style, and capitalization context
319     *
320     * @param locale the locale
321     * @param nf the number format object. It is defensively copied to ensure thread-safety
322     * and immutability of this class. May be null.
323     * @param style the style.
324     * @param capitalizationContext the capitalization context.
325     * @draft ICU 54
326     * @provisional This API might change or be removed in a future release.
327     */
328    public static RelativeDateTimeFormatter getInstance(
329            ULocale locale,
330            NumberFormat nf,
331            Style style,
332            DisplayContext capitalizationContext) {
333        RelativeDateTimeFormatterData data = cache.get(locale);
334        if (nf == null) {
335            nf = NumberFormat.getInstance(locale);
336        } else {
337            nf = (NumberFormat) nf.clone();
338        }
339        return new RelativeDateTimeFormatter(
340                data.qualitativeUnitMap,
341                data.quantitativeUnitMap,
342                new MessageFormat(data.dateTimePattern),
343                PluralRules.forLocale(locale),
344                nf,
345                style,
346                capitalizationContext,
347                capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ?
348                    BreakIterator.getSentenceInstance(locale) : null,
349                locale);
350
351    }
352
353    /**
354     * Returns a RelativeDateTimeFormatter for a particular JDK locale that uses a particular
355     * NumberFormat object.
356     *
357     * @param locale the JDK locale
358     * @param nf the number format object. It is defensively copied to ensure thread-safety
359     * and immutability of this class.
360     * @return An instance of RelativeDateTimeFormatter.
361     * @draft ICU 54
362     * @provisional This API might change or be removed in a future release.
363     */
364    public static RelativeDateTimeFormatter getInstance(Locale locale, NumberFormat nf) {
365        return getInstance(ULocale.forLocale(locale), nf);
366    }
367
368    /**
369     * Formats a relative date with a quantity such as "in 5 days" or
370     * "3 months ago"
371     * @param quantity The numerical amount e.g 5. This value is formatted
372     * according to this object's {@link NumberFormat} object.
373     * @param direction NEXT means a future relative date; LAST means a past
374     * relative date.
375     * @param unit the unit e.g day? month? year?
376     * @return the formatted string
377     * @throws IllegalArgumentException if direction is something other than
378     * NEXT or LAST.
379     * @stable ICU 53
380     */
381    public String format(double quantity, Direction direction, RelativeUnit unit) {
382        if (direction != Direction.LAST && direction != Direction.NEXT) {
383            throw new IllegalArgumentException("direction must be NEXT or LAST");
384        }
385        String result;
386        // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this
387        // class we must guarantee that only one thread at a time uses our numberFormat.
388        synchronized (numberFormat) {
389            result = getQuantity(
390                    unit, direction == Direction.NEXT).format(
391                            quantity, numberFormat, pluralRules);
392        }
393        return adjustForContext(result);
394    }
395
396
397    /**
398     * Formats a relative date without a quantity.
399     * @param direction NEXT, LAST, THIS, etc.
400     * @param unit e.g SATURDAY, DAY, MONTH
401     * @return the formatted string. If direction has a value that is documented as not being
402     *  fully supported in every locale (for example NEXT_2 or LAST_2) then this function may
403     *  return null to signal that no formatted string is available.
404     * @throws IllegalArgumentException if the direction is incompatible with
405     * unit this can occur with NOW which can only take PLAIN.
406     * @stable ICU 53
407     */
408    public String format(Direction direction, AbsoluteUnit unit) {
409        if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) {
410            throw new IllegalArgumentException("NOW can only accept direction PLAIN.");
411        }
412        String result = this.qualitativeUnitMap.get(style).get(unit).get(direction);
413        return result != null ? adjustForContext(result) : null;
414    }
415
416    /**
417     * Combines a relative date string and a time string in this object's
418     * locale. This is done with the same date-time separator used for the
419     * default calendar in this locale.
420     * @param relativeDateString the relative date e.g 'yesterday'
421     * @param timeString the time e.g '3:45'
422     * @return the date and time concatenated according to the default
423     * calendar in this locale e.g 'yesterday, 3:45'
424     * @stable ICU 53
425     */
426    public String combineDateAndTime(String relativeDateString, String timeString) {
427        return this.combinedDateAndTime.format(
428            new Object[]{timeString, relativeDateString}, new StringBuffer(), null).toString();
429    }
430
431    /**
432     * Returns a copy of the NumberFormat this object is using.
433     * @return A copy of the NumberFormat.
434     * @stable ICU 53
435     */
436    public NumberFormat getNumberFormat() {
437        // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this
438        // class we must guarantee that only one thread at a time uses our numberFormat.
439        synchronized (numberFormat) {
440            return (NumberFormat) numberFormat.clone();
441        }
442    }
443
444    /**
445     * Return capitalization context.
446     *
447     * @draft ICU 54
448     * @provisional This API might change or be removed in a future release.
449     */
450    public DisplayContext getCapitalizationContext() {
451        return capitalizationContext;
452    }
453
454    /**
455     * Return style
456     *
457     * @draft ICU 54
458     * @provisional This API might change or be removed in a future release.
459     */
460    public Style getFormatStyle() {
461        return style;
462    }
463
464    private String adjustForContext(String originalFormattedString) {
465        if (breakIterator == null || originalFormattedString.length() == 0
466                || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) {
467            return originalFormattedString;
468        }
469        synchronized (breakIterator) {
470            return UCharacter.toTitleCase(
471                    locale,
472                    originalFormattedString,
473                    breakIterator,
474                    UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT);
475        }
476    }
477
478    private static void addQualitativeUnit(
479            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnits,
480            AbsoluteUnit unit,
481            String current) {
482        EnumMap<Direction, String> unitStrings =
483                new EnumMap<Direction, String>(Direction.class);
484        unitStrings.put(Direction.PLAIN, current);
485        qualitativeUnits.put(unit,  unitStrings);
486    }
487
488    private static void addQualitativeUnit(
489            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnits,
490            AbsoluteUnit unit, ICUResourceBundle bundle, String plain) {
491        EnumMap<Direction, String> unitStrings =
492                new EnumMap<Direction, String>(Direction.class);
493        unitStrings.put(Direction.LAST, bundle.getStringWithFallback("-1"));
494        unitStrings.put(Direction.THIS, bundle.getStringWithFallback("0"));
495        unitStrings.put(Direction.NEXT, bundle.getStringWithFallback("1"));
496        addOptionalDirection(unitStrings, Direction.LAST_2, bundle, "-2");
497        addOptionalDirection(unitStrings, Direction.NEXT_2, bundle, "2");
498        unitStrings.put(Direction.PLAIN, plain);
499        qualitativeUnits.put(unit,  unitStrings);
500    }
501
502    private static void addOptionalDirection(
503            EnumMap<Direction, String> unitStrings,
504            Direction direction,
505            ICUResourceBundle bundle,
506            String key) {
507        String s = bundle.findStringWithFallback(key);
508        if (s != null) {
509            unitStrings.put(direction, s);
510        }
511    }
512
513    private RelativeDateTimeFormatter(
514            EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
515            EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap,
516            MessageFormat combinedDateAndTime,
517            PluralRules pluralRules,
518            NumberFormat numberFormat,
519            Style style,
520            DisplayContext capitalizationContext,
521            BreakIterator breakIterator,
522            ULocale locale) {
523        this.qualitativeUnitMap = qualitativeUnitMap;
524        this.quantitativeUnitMap = quantitativeUnitMap;
525        this.combinedDateAndTime = combinedDateAndTime;
526        this.pluralRules = pluralRules;
527        this.numberFormat = numberFormat;
528        this.style = style;
529        if (capitalizationContext.type() != DisplayContext.Type.CAPITALIZATION) {
530            throw new IllegalArgumentException(capitalizationContext.toString());
531        }
532        this.capitalizationContext = capitalizationContext;
533        this.breakIterator = breakIterator;
534        this.locale = locale;
535    }
536
537    private QuantityFormatter getQuantity(RelativeUnit unit, boolean isFuture) {
538        QuantityFormatter[] quantities = quantitativeUnitMap.get(style).get(unit);
539        return isFuture ? quantities[1] : quantities[0];
540    }
541
542    private final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap;
543    private final EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap;
544    private final MessageFormat combinedDateAndTime;
545    private final PluralRules pluralRules;
546    private final NumberFormat numberFormat;
547    private final Style style;
548    private final DisplayContext capitalizationContext;
549    private final BreakIterator breakIterator;
550    private final ULocale locale;
551
552    private static class RelativeDateTimeFormatterData {
553        public RelativeDateTimeFormatterData(
554                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
555                EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap,
556                String dateTimePattern) {
557            this.qualitativeUnitMap = qualitativeUnitMap;
558            this.quantitativeUnitMap = quantitativeUnitMap;
559            this.dateTimePattern = dateTimePattern;
560        }
561
562        public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap;
563        public final EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap;
564        public final String dateTimePattern;  // Example: "{1}, {0}"
565    }
566
567    private static class Cache {
568        private final ICUCache<String, RelativeDateTimeFormatterData> cache =
569            new SimpleCache<String, RelativeDateTimeFormatterData>();
570
571        public RelativeDateTimeFormatterData get(ULocale locale) {
572            String key = locale.toString();
573            RelativeDateTimeFormatterData result = cache.get(key);
574            if (result == null) {
575                result = new Loader(locale).load();
576                cache.put(key, result);
577            }
578            return result;
579        }
580    }
581
582    private static class Loader {
583        private final ULocale ulocale;
584
585        public Loader(ULocale ulocale) {
586            this.ulocale = ulocale;
587        }
588
589        public RelativeDateTimeFormatterData load() {
590            EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap =
591                    new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class);
592
593            EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap =
594                    new EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>>(Style.class);
595
596            for (Style style : Style.values()) {
597                qualitativeUnitMap.put(style, new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class));
598                quantitativeUnitMap.put(style, new EnumMap<RelativeUnit, QuantityFormatter[]>(RelativeUnit.class));
599            }
600
601            ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
602                    getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
603            addTimeUnits(
604                    r,
605                    "fields/day", "fields/day-short", "fields/day-narrow",
606                    RelativeUnit.DAYS,
607                    AbsoluteUnit.DAY,
608                    quantitativeUnitMap,
609                    qualitativeUnitMap);
610            addTimeUnits(
611                    r,
612                    "fields/week", "fields/week-short", "fields/week-narrow",
613                    RelativeUnit.WEEKS,
614                    AbsoluteUnit.WEEK,
615                    quantitativeUnitMap,
616                    qualitativeUnitMap);
617            addTimeUnits(
618                    r,
619                    "fields/month", "fields/month-short", "fields/month-narrow",
620                    RelativeUnit.MONTHS,
621                    AbsoluteUnit.MONTH,
622                    quantitativeUnitMap,
623                    qualitativeUnitMap);
624            addTimeUnits(
625                    r,
626                    "fields/year", "fields/year-short", "fields/year-narrow",
627                    RelativeUnit.YEARS,
628                    AbsoluteUnit.YEAR,
629                    quantitativeUnitMap,
630                    qualitativeUnitMap);
631            initRelativeUnits(
632                    r,
633                    "fields/second", "fields/second-short", "fields/second-narrow",
634                    RelativeUnit.SECONDS,
635                    quantitativeUnitMap);
636            initRelativeUnits(
637                    r,
638                    "fields/minute", "fields/minute-short", "fields/minute-narrow",
639                    RelativeUnit.MINUTES,
640                    quantitativeUnitMap);
641            initRelativeUnits(
642                    r,
643                    "fields/hour", "fields/hour-short", "fields/hour-narrow",
644                    RelativeUnit.HOURS,
645                    quantitativeUnitMap);
646
647            addQualitativeUnit(
648                    qualitativeUnitMap.get(Style.LONG),
649                    AbsoluteUnit.NOW,
650                    r.getStringWithFallback("fields/second/relative/0"));
651            addQualitativeUnit(
652                    qualitativeUnitMap.get(Style.SHORT),
653                    AbsoluteUnit.NOW,
654                    r.getStringWithFallback("fields/second-short/relative/0"));
655            addQualitativeUnit(
656                    qualitativeUnitMap.get(Style.NARROW),
657                    AbsoluteUnit.NOW,
658                    r.getStringWithFallback("fields/second-narrow/relative/0"));
659
660            EnumMap<Style, EnumMap<AbsoluteUnit, String>> dayOfWeekMap =
661                    new EnumMap<Style, EnumMap<AbsoluteUnit, String>>(Style.class);
662            dayOfWeekMap.put(Style.LONG, readDaysOfWeek(
663                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/wide")));
664            dayOfWeekMap.put(Style.SHORT, readDaysOfWeek(
665                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/short")));
666            dayOfWeekMap.put(Style.NARROW, readDaysOfWeek(
667                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/narrow")));
668
669            addWeekDays(
670                    r,
671                    "fields/mon/relative",
672                    "fields/mon-short/relative",
673                    "fields/mon-narrow/relative",
674                    dayOfWeekMap,
675                    AbsoluteUnit.MONDAY,
676                    qualitativeUnitMap);
677            addWeekDays(
678                    r,
679                    "fields/tue/relative",
680                    "fields/tue-short/relative",
681                    "fields/tue-narrow/relative",
682                    dayOfWeekMap,
683                    AbsoluteUnit.TUESDAY,
684                    qualitativeUnitMap);
685            addWeekDays(
686                    r,
687                    "fields/wed/relative",
688                    "fields/wed-short/relative",
689                    "fields/wed-narrow/relative",
690                    dayOfWeekMap,
691                    AbsoluteUnit.WEDNESDAY,
692                    qualitativeUnitMap);
693            addWeekDays(
694                    r,
695                    "fields/thu/relative",
696                    "fields/thu-short/relative",
697                    "fields/thu-narrow/relative",
698                    dayOfWeekMap,
699                    AbsoluteUnit.THURSDAY,
700                    qualitativeUnitMap);
701            addWeekDays(
702                    r,
703                    "fields/fri/relative",
704                    "fields/fri-short/relative",
705                    "fields/fri-narrow/relative",
706                    dayOfWeekMap,
707                    AbsoluteUnit.FRIDAY,
708                    qualitativeUnitMap);
709            addWeekDays(
710                    r,
711                    "fields/sat/relative",
712                    "fields/sat-short/relative",
713                    "fields/sat-narrow/relative",
714                    dayOfWeekMap,
715                    AbsoluteUnit.SATURDAY,
716                    qualitativeUnitMap);
717            addWeekDays(
718                    r,
719                    "fields/sun/relative",
720                    "fields/sun-short/relative",
721                    "fields/sun-narrow/relative",
722                    dayOfWeekMap,
723                    AbsoluteUnit.SUNDAY,
724                    qualitativeUnitMap);
725            CalendarData calData = new CalendarData(
726                    ulocale, r.getStringWithFallback("calendar/default"));
727            return new RelativeDateTimeFormatterData(
728                    qualitativeUnitMap, quantitativeUnitMap, calData.getDateTimePattern());
729        }
730
731        private void addTimeUnits(
732                ICUResourceBundle r,
733                String path, String pathShort, String pathNarrow,
734                RelativeUnit relativeUnit,
735                AbsoluteUnit absoluteUnit,
736                EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap,
737                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap) {
738           addTimeUnit(
739                   r.getWithFallback(path),
740                   relativeUnit,
741                   absoluteUnit,
742                   quantitativeUnitMap.get(Style.LONG),
743                   qualitativeUnitMap.get(Style.LONG));
744           addTimeUnit(
745                   r.getWithFallback(pathShort),
746                   relativeUnit,
747                   absoluteUnit,
748                   quantitativeUnitMap.get(Style.SHORT),
749                   qualitativeUnitMap.get(Style.SHORT));
750           addTimeUnit(
751                   r.getWithFallback(pathNarrow),
752                   relativeUnit,
753                   absoluteUnit,
754                   quantitativeUnitMap.get(Style.NARROW),
755                   qualitativeUnitMap.get(Style.NARROW));
756
757        }
758
759        private void addTimeUnit(
760                ICUResourceBundle timeUnitBundle,
761                RelativeUnit relativeUnit,
762                AbsoluteUnit absoluteUnit,
763                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap,
764                EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap) {
765            addTimeUnit(timeUnitBundle, relativeUnit, quantitativeUnitMap);
766            String unitName = timeUnitBundle.getStringWithFallback("dn");
767            // TODO(Travis Keep): This is a hack to get around CLDR bug 6818.
768            if (ulocale.getLanguage().equals("en")) {
769                unitName = unitName.toLowerCase();
770            }
771            timeUnitBundle = timeUnitBundle.getWithFallback("relative");
772            addQualitativeUnit(
773                    qualitativeUnitMap,
774                    absoluteUnit,
775                    timeUnitBundle,
776                    unitName);
777        }
778
779        private void initRelativeUnits(
780                ICUResourceBundle r,
781                String path,
782                String pathShort,
783                String pathNarrow,
784                RelativeUnit relativeUnit,
785                EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap) {
786            addTimeUnit(
787                    r.getWithFallback(path),
788                    relativeUnit,
789                    quantitativeUnitMap.get(Style.LONG));
790            addTimeUnit(
791                    r.getWithFallback(pathShort),
792                    relativeUnit,
793                    quantitativeUnitMap.get(Style.SHORT));
794            addTimeUnit(
795                    r.getWithFallback(pathNarrow),
796                    relativeUnit,
797                    quantitativeUnitMap.get(Style.NARROW));
798        }
799
800        private static void addTimeUnit(
801                ICUResourceBundle timeUnitBundle,
802                RelativeUnit relativeUnit,
803                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap) {
804            QuantityFormatter.Builder future = new QuantityFormatter.Builder();
805            QuantityFormatter.Builder past = new QuantityFormatter.Builder();
806            timeUnitBundle = timeUnitBundle.getWithFallback("relativeTime");
807            addTimeUnit(
808                    timeUnitBundle.getWithFallback("future"),
809                    future);
810            addTimeUnit(
811                    timeUnitBundle.getWithFallback("past"),
812                    past);
813            quantitativeUnitMap.put(
814                    relativeUnit, new QuantityFormatter[] { past.build(), future.build() });
815        }
816
817        private static void addTimeUnit(
818                ICUResourceBundle pastOrFuture, QuantityFormatter.Builder builder) {
819            int size = pastOrFuture.getSize();
820            for (int i = 0; i < size; i++) {
821                UResourceBundle r = pastOrFuture.get(i);
822                builder.add(r.getKey(), r.getString());
823            }
824        }
825
826        private void addWeekDays(
827                ICUResourceBundle r,
828                String path,
829                String pathShort,
830                String pathNarrow,
831                EnumMap<Style, EnumMap<AbsoluteUnit, String>> dayOfWeekMap,
832                AbsoluteUnit weekDay,
833                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap) {
834            addQualitativeUnit(
835                    qualitativeUnitMap.get(Style.LONG),
836                    weekDay,
837                    r.findWithFallback(path),
838                    dayOfWeekMap.get(Style.LONG).get(weekDay));
839            addQualitativeUnit(
840                    qualitativeUnitMap.get(Style.SHORT),
841                    weekDay,
842                    r.findWithFallback(pathShort),
843                    dayOfWeekMap.get(Style.SHORT).get(weekDay));
844            addQualitativeUnit(
845                    qualitativeUnitMap.get(Style.NARROW),
846                    weekDay,
847                    r.findWithFallback(pathNarrow),
848                    dayOfWeekMap.get(Style.NARROW).get(weekDay));
849
850        }
851
852        private static EnumMap<AbsoluteUnit, String> readDaysOfWeek(ICUResourceBundle daysOfWeekBundle) {
853            EnumMap<AbsoluteUnit, String> dayOfWeekMap = new EnumMap<AbsoluteUnit, String>(AbsoluteUnit.class);
854            if (daysOfWeekBundle.getSize() != 7) {
855                throw new IllegalStateException(String.format("Expect 7 days in a week, got %d", daysOfWeekBundle.getSize()));
856            }
857            // Sunday always comes first in CLDR data.
858            int idx = 0;
859            dayOfWeekMap.put(AbsoluteUnit.SUNDAY, daysOfWeekBundle.getString(idx++));
860            dayOfWeekMap.put(AbsoluteUnit.MONDAY, daysOfWeekBundle.getString(idx++));
861            dayOfWeekMap.put(AbsoluteUnit.TUESDAY, daysOfWeekBundle.getString(idx++));
862            dayOfWeekMap.put(AbsoluteUnit.WEDNESDAY, daysOfWeekBundle.getString(idx++));
863            dayOfWeekMap.put(AbsoluteUnit.THURSDAY, daysOfWeekBundle.getString(idx++));
864            dayOfWeekMap.put(AbsoluteUnit.FRIDAY, daysOfWeekBundle.getString(idx++));
865            dayOfWeekMap.put(AbsoluteUnit.SATURDAY, daysOfWeekBundle.getString(idx++));
866            return dayOfWeekMap;
867        }
868    }
869
870    private static final Cache cache = new Cache();
871}
872