RelativeDateTimeFormatter.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2013-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.text;

import com.ibm.icu.impl.CacheBase;
import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.impl.FormattedValueStringBuilderImpl;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.SoftCache;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import java.io.InvalidObjectException;
import java.text.AttributedCharacterIterator;
import java.text.Format;
import java.util.EnumMap;
import java.util.Locale;

/**
 * Formats simple relative dates. There are two types of relative dates that it handles:
 *
 * <ul>
 *   <li>relative dates with a quantity e.g "in 5 days"
 *   <li>relative dates without a quantity e.g "next Tuesday"
 * </ul>
 *
 * <p>This API is very basic and is intended to be a building block for more fancy APIs. The caller
 * tells it exactly what to display in a locale independent way. While this class automatically
 * provides the correct plural forms, the grammatical form is otherwise as neutral as possible. It
 * is the caller's responsibility to handle cut-off logic such as deciding between displaying "in 7
 * days" or "in 1 week." This API supports relative dates involving one single unit. This API does
 * not support relative dates involving compound units. e.g "in 5 days and 4 hours" nor does it
 * support parsing. This class is both immutable and thread-safe.
 *
 * <p>Here are some examples of use:
 *
 * <blockquote>
 *
 * <pre>
 * RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance();
 * fmt.format(1, Direction.NEXT, RelativeUnit.DAYS); // "in 1 day"
 * fmt.format(3, Direction.NEXT, RelativeUnit.DAYS); // "in 3 days"
 * fmt.format(3.2, Direction.LAST, RelativeUnit.YEARS); // "3.2 years ago"
 *
 * fmt.format(Direction.LAST, AbsoluteUnit.SUNDAY); // "last Sunday"
 * fmt.format(Direction.THIS, AbsoluteUnit.SUNDAY); // "this Sunday"
 * fmt.format(Direction.NEXT, AbsoluteUnit.SUNDAY); // "next Sunday"
 * fmt.format(Direction.PLAIN, AbsoluteUnit.SUNDAY); // "Sunday"
 *
 * fmt.format(Direction.LAST, AbsoluteUnit.DAY); // "yesterday"
 * fmt.format(Direction.THIS, AbsoluteUnit.DAY); // "today"
 * fmt.format(Direction.NEXT, AbsoluteUnit.DAY); // "tomorrow"
 *
 * fmt.format(Direction.PLAIN, AbsoluteUnit.NOW); // "now"
 * </pre>
 *
 * </blockquote>
 *
 * <p>The Style parameter allows selection of different length styles: LONG ("3 seconds ago"), SHORT
 * ("3 sec. ago"), NARROW ("3s ago"). In the future, we may add more forms, such as relative day
 * periods ("yesterday afternoon"), etc.
 *
 * @stable ICU 53
 */
public final class RelativeDateTimeFormatter {

    /**
     * The formatting style
     *
     * @stable ICU 54
     */
    public static enum Style {

        /**
         * Everything spelled out.
         *
         * @stable ICU 54
         */
        LONG,

        /**
         * Abbreviations used when possible.
         *
         * @stable ICU 54
         */
        SHORT,

        /**
         * Use single letters when possible.
         *
         * @stable ICU 54
         */
        NARROW;

        private static final int INDEX_COUNT = 3; // NARROW.ordinal() + 1
    }

    /**
     * Represents the unit for formatting a relative date. e.g "in 5 days" or "in 3 months"
     *
     * @stable ICU 53
     */
    public static enum RelativeUnit {

        /**
         * Seconds
         *
         * @stable ICU 53
         */
        SECONDS,

        /**
         * Minutes
         *
         * @stable ICU 53
         */
        MINUTES,

        /**
         * Hours
         *
         * @stable ICU 53
         */
        HOURS,

        /**
         * Days
         *
         * @stable ICU 53
         */
        DAYS,

        /**
         * Weeks
         *
         * @stable ICU 53
         */
        WEEKS,

        /**
         * Months
         *
         * @stable ICU 53
         */
        MONTHS,

        /**
         * Years
         *
         * @stable ICU 53
         */
        YEARS,

        /**
         * Quarters
         *
         * @stable ICU 76
         */
        QUARTERS,

        /**
         * Sundays
         *
         * @stable ICU 76
         */
        SUNDAYS,

        /**
         * Mondays
         *
         * @stable ICU 76
         */
        MONDAYS,

        /**
         * Tuesdays
         *
         * @stable ICU 76
         */
        TUESDAYS,

        /**
         * Wednesdays
         *
         * @stable ICU 76
         */
        WEDNESDAYS,

        /**
         * Thursdays
         *
         * @stable ICU 76
         */
        THURSDAYS,

        /**
         * Fridays
         *
         * @stable ICU 76
         */
        FRIDAYS,

        /**
         * Saturdays
         *
         * @stable ICU 76
         */
        SATURDAYS,
    }

    /**
     * Represents an absolute unit.
     *
     * @stable ICU 53
     */
    public static enum AbsoluteUnit {

        /**
         * Sunday
         *
         * @stable ICU 53
         */
        SUNDAY,

        /**
         * Monday
         *
         * @stable ICU 53
         */
        MONDAY,

        /**
         * Tuesday
         *
         * @stable ICU 53
         */
        TUESDAY,

        /**
         * Wednesday
         *
         * @stable ICU 53
         */
        WEDNESDAY,

        /**
         * Thursday
         *
         * @stable ICU 53
         */
        THURSDAY,

        /**
         * Friday
         *
         * @stable ICU 53
         */
        FRIDAY,

        /**
         * Saturday
         *
         * @stable ICU 53
         */
        SATURDAY,

        /**
         * Day
         *
         * @stable ICU 53
         */
        DAY,

        /**
         * Week
         *
         * @stable ICU 53
         */
        WEEK,

        /**
         * Month
         *
         * @stable ICU 53
         */
        MONTH,

        /**
         * Year
         *
         * @stable ICU 53
         */
        YEAR,

        /**
         * Now
         *
         * @stable ICU 53
         */
        NOW,

        /**
         * Quarter
         *
         * @stable ICU 64
         */
        QUARTER,

        /**
         * Hour
         *
         * @stable ICU 65
         */
        HOUR,

        /**
         * Minute
         *
         * @stable ICU 65
         */
        MINUTE,
    }

    /**
     * Represents a direction for an absolute unit e.g "Next Tuesday" or "Last Tuesday"
     *
     * @stable ICU 53
     */
    public static enum Direction {
        /**
         * Two before. Not fully supported in every locale
         *
         * @stable ICU 53
         */
        LAST_2,

        /**
         * Last
         *
         * @stable ICU 53
         */
        LAST,

        /**
         * This
         *
         * @stable ICU 53
         */
        THIS,

        /**
         * Next
         *
         * @stable ICU 53
         */
        NEXT,

        /**
         * Two after. Not fully supported in every locale
         *
         * @stable ICU 53
         */
        NEXT_2,

        /**
         * Plain, which means the absence of a qualifier
         *
         * @stable ICU 53
         */
        PLAIN,
    }

    /**
     * Represents the unit for formatting a relative date. e.g "in 5 days" or "next year"
     *
     * @stable ICU 57
     */
    public static enum RelativeDateTimeUnit {
        /**
         * Specifies that relative unit is year, e.g. "last year", "in 5 years".
         *
         * @stable ICU 57
         */
        YEAR,
        /**
         * Specifies that relative unit is quarter, e.g. "last quarter", "in 5 quarters".
         *
         * @stable ICU 57
         */
        QUARTER,
        /**
         * Specifies that relative unit is month, e.g. "last month", "in 5 months".
         *
         * @stable ICU 57
         */
        MONTH,
        /**
         * Specifies that relative unit is week, e.g. "last week", "in 5 weeks".
         *
         * @stable ICU 57
         */
        WEEK,
        /**
         * Specifies that relative unit is day, e.g. "yesterday", "in 5 days".
         *
         * @stable ICU 57
         */
        DAY,
        /**
         * Specifies that relative unit is hour, e.g. "1 hour ago", "in 5 hours".
         *
         * @stable ICU 57
         */
        HOUR,
        /**
         * Specifies that relative unit is minute, e.g. "1 minute ago", "in 5 minutes".
         *
         * @stable ICU 57
         */
        MINUTE,
        /**
         * Specifies that relative unit is second, e.g. "1 second ago", "in 5 seconds".
         *
         * @stable ICU 57
         */
        SECOND,
        /**
         * Specifies that relative unit is Sunday, e.g. "last Sunday", "this Sunday", "next Sunday",
         * "in 5 Sundays".
         *
         * @stable ICU 57
         */
        SUNDAY,
        /**
         * Specifies that relative unit is Monday, e.g. "last Monday", "this Monday", "next Monday",
         * "in 5 Mondays".
         *
         * @stable ICU 57
         */
        MONDAY,
        /**
         * Specifies that relative unit is Tuesday, e.g. "last Tuesday", "this Tuesday", "next
         * Tuesday", "in 5 Tuesdays".
         *
         * @stable ICU 57
         */
        TUESDAY,
        /**
         * Specifies that relative unit is Wednesday, e.g. "last Wednesday", "this Wednesday", "next
         * Wednesday", "in 5 Wednesdays".
         *
         * @stable ICU 57
         */
        WEDNESDAY,
        /**
         * Specifies that relative unit is Thursday, e.g. "last Thursday", "this Thursday", "next
         * Thursday", "in 5 Thursdays".
         *
         * @stable ICU 57
         */
        THURSDAY,
        /**
         * Specifies that relative unit is Friday, e.g. "last Friday", "this Friday", "next Friday",
         * "in 5 Fridays".
         *
         * @stable ICU 57
         */
        FRIDAY,
        /**
         * Specifies that relative unit is Saturday, e.g. "last Saturday", "this Saturday", "next
         * Saturday", "in 5 Saturdays".
         *
         * @stable ICU 57
         */
        SATURDAY,
    }

    /**
     * Field constants used when accessing field information for relative datetime strings in
     * FormattedValue.
     *
     * <p>There is no public constructor to this class; the only instances are the constants defined
     * here.
     *
     * <p>
     *
     * @stable ICU 64
     */
    public static class Field extends Format.Field {
        private static final long serialVersionUID = -5327685528663492325L;

        /**
         * Represents a literal text string, like "tomorrow" or "days ago".
         *
         * @stable ICU 64
         */
        public static final Field LITERAL = new Field("literal");

        /**
         * Represents a number quantity, like "3" in "3 days ago".
         *
         * @stable ICU 64
         */
        public static final Field NUMERIC = new Field("numeric");

        private Field(String fieldName) {
            super(fieldName);
        }

        /**
         * Serizalization method resolve instances to the constant Field values
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        @Override
        protected Object readResolve() throws InvalidObjectException {
            if (this.getName().equals(LITERAL.getName())) return LITERAL;
            if (this.getName().equals(NUMERIC.getName())) return NUMERIC;

            throw new InvalidObjectException("An invalid object.");
        }
    }

    /**
     * Represents the result of a formatting operation of a relative datetime. Access the string
     * value or field information.
     *
     * <p>Instances of this class are immutable and thread-safe.
     *
     * <p>Not intended for public subclassing.
     *
     * @author sffc
     * @stable ICU 64
     */
    public static class FormattedRelativeDateTime implements FormattedValue {

        private final FormattedStringBuilder string;

        private FormattedRelativeDateTime(FormattedStringBuilder string) {
            this.string = string;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public String toString() {
            return string.toString();
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public int length() {
            return string.length();
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public char charAt(int index) {
            return string.charAt(index);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public CharSequence subSequence(int start, int end) {
            return string.subString(start, end);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public <A extends Appendable> A appendTo(A appendable) {
            return Utility.appendTo(string, appendable);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public boolean nextPosition(ConstrainedFieldPosition cfpos) {
            return FormattedValueStringBuilderImpl.nextPosition(string, cfpos, Field.NUMERIC);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 64
         */
        @Override
        public AttributedCharacterIterator toCharacterIterator() {
            return FormattedValueStringBuilderImpl.toCharacterIterator(string, Field.NUMERIC);
        }
    }

    /**
     * Returns a RelativeDateTimeFormatter for the default locale.
     *
     * @stable ICU 53
     */
    public static RelativeDateTimeFormatter getInstance() {
        return getInstance(
                ULocale.getDefault(), null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
    }

    /**
     * Returns a RelativeDateTimeFormatter for a particular locale.
     *
     * @param locale the locale.
     * @return An instance of RelativeDateTimeFormatter.
     * @stable ICU 53
     */
    public static RelativeDateTimeFormatter getInstance(ULocale locale) {
        return getInstance(locale, null, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
    }

    /**
     * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale}.
     *
     * @param locale the {@link java.util.Locale}.
     * @return An instance of RelativeDateTimeFormatter.
     * @stable ICU 54
     */
    public static RelativeDateTimeFormatter getInstance(Locale locale) {
        return getInstance(ULocale.forLocale(locale));
    }

    /**
     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
     * NumberFormat object.
     *
     * @param locale the locale
     * @param nf the number format object. It is defensively copied to ensure thread-safety and
     *     immutability of this class.
     * @return An instance of RelativeDateTimeFormatter.
     * @stable ICU 53
     */
    public static RelativeDateTimeFormatter getInstance(ULocale locale, NumberFormat nf) {
        return getInstance(locale, nf, Style.LONG, DisplayContext.CAPITALIZATION_NONE);
    }

    /**
     * Returns a RelativeDateTimeFormatter for a particular locale that uses a particular
     * NumberFormat object, style, and capitalization context
     *
     * @param locale the locale
     * @param nf the number format object. It is defensively copied to ensure thread-safety and
     *     immutability of this class. May be null.
     * @param style the style.
     * @param capitalizationContext the capitalization context.
     * @stable ICU 54
     */
    public static RelativeDateTimeFormatter getInstance(
            ULocale locale, NumberFormat nf, Style style, DisplayContext capitalizationContext) {
        RelativeDateTimeFormatterData data = cache.get(locale);
        if (nf == null) {
            nf = NumberFormat.getInstance(locale);
        } else {
            nf = nf.clone();
        }
        return new RelativeDateTimeFormatter(
                data.qualitativeUnitMap,
                data.relUnitPatternMap,
                SimpleFormatterImpl.compileToStringMinMaxArguments(
                        data.dateTimePattern, new StringBuilder(), 2, 2),
                PluralRules.forLocale(locale),
                nf,
                style,
                capitalizationContext,
                capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE
                        ? BreakIterator.getSentenceInstance(locale)
                        : null,
                locale);
    }

    /**
     * Returns a RelativeDateTimeFormatter for a particular {@link java.util.Locale} that uses a
     * particular NumberFormat object.
     *
     * @param locale the {@link java.util.Locale}
     * @param nf the number format object. It is defensively copied to ensure thread-safety and
     *     immutability of this class.
     * @return An instance of RelativeDateTimeFormatter.
     * @stable ICU 54
     */
    public static RelativeDateTimeFormatter getInstance(Locale locale, NumberFormat nf) {
        return getInstance(ULocale.forLocale(locale), nf);
    }

    /**
     * Formats a relative date with a quantity such as "in 5 days" or "3 months ago".
     *
     * <p>This method returns a String. To get more information about the formatting result, use
     * formatToValue().
     *
     * @param quantity The numerical amount e.g 5. This value is formatted according to this
     *     object's {@link NumberFormat} object.
     * @param direction NEXT means a future relative date; LAST means a past relative date.
     * @param unit the unit e.g day? month? year?
     * @return the formatted string
     * @throws IllegalArgumentException if direction is something other than NEXT or LAST.
     * @stable ICU 53
     */
    public String format(double quantity, Direction direction, RelativeUnit unit) {
        FormattedStringBuilder output = formatImpl(quantity, direction, unit);
        return adjustForContext(output.toString());
    }

    /**
     * Formats a relative date with a quantity such as "in 5 days" or "3 months ago".
     *
     * <p>This method returns a FormattedRelativeDateTime, which exposes more information than the
     * String returned by format().
     *
     * @param quantity The numerical amount e.g 5. This value is formatted according to this
     *     object's {@link NumberFormat} object.
     * @param direction NEXT means a future relative date; LAST means a past relative date.
     * @param unit the unit e.g day? month? year?
     * @return the formatted relative datetime
     * @throws IllegalArgumentException if direction is something other than NEXT or LAST.
     * @stable ICU 64
     */
    public FormattedRelativeDateTime formatToValue(
            double quantity, Direction direction, RelativeUnit unit) {
        checkNoAdjustForContext();
        return new FormattedRelativeDateTime(formatImpl(quantity, direction, unit));
    }

    /** Implementation method for format and formatToValue with RelativeUnit */
    private FormattedStringBuilder formatImpl(
            double quantity, Direction direction, RelativeUnit unit) {
        if (direction != Direction.LAST && direction != Direction.NEXT) {
            throw new IllegalArgumentException("direction must be NEXT or LAST");
        }
        int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0);

        FormattedStringBuilder output = new FormattedStringBuilder();
        String pluralKeyword;
        if (numberFormat instanceof DecimalFormat) {
            DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(quantity);
            ((DecimalFormat) numberFormat).toNumberFormatter().formatImpl(dq, output);
            pluralKeyword = pluralRules.select(dq);
        } else {
            String result = numberFormat.format(quantity);
            output.append(result, null);
            pluralKeyword = pluralRules.select(quantity);
        }
        StandardPlural pluralForm = StandardPlural.orOtherFromString(pluralKeyword);

        String compiledPattern =
                getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm);
        SimpleFormatterImpl.formatPrefixSuffix(
                compiledPattern, Field.LITERAL, 0, output.length(), output);
        return output;
    }

    /**
     * Format a combination of RelativeDateTimeUnit and numeric offset using a numeric style, e.g.
     * "1 week ago", "in 1 week", "5 weeks ago", "in 5 weeks".
     *
     * <p>This method returns a String. To get more information about the formatting result, use
     * formatNumericToValue().
     *
     * @param offset The signed offset for the specified unit. This will be formatted according to
     *     this object's NumberFormat object.
     * @param unit The unit to use when formatting the relative date, e.g.
     *     RelativeDateTimeUnit.WEEK, RelativeDateTimeUnit.FRIDAY.
     * @return The formatted string (may be empty in case of error)
     * @stable ICU 57
     */
    public String formatNumeric(double offset, RelativeDateTimeUnit unit) {
        FormattedStringBuilder output = formatNumericImpl(offset, unit);
        return adjustForContext(output.toString());
    }

    /**
     * Format a combination of RelativeDateTimeUnit and numeric offset using a numeric style, e.g.
     * "1 week ago", "in 1 week", "5 weeks ago", "in 5 weeks".
     *
     * <p>This method returns a FormattedRelativeDateTime, which exposes more information than the
     * String returned by formatNumeric().
     *
     * @param offset The signed offset for the specified unit. This will be formatted according to
     *     this object's NumberFormat object.
     * @param unit The unit to use when formatting the relative date, e.g.
     *     RelativeDateTimeUnit.WEEK, RelativeDateTimeUnit.FRIDAY.
     * @return The formatted string (may be empty in case of error)
     * @stable ICU 64
     */
    public FormattedRelativeDateTime formatNumericToValue(
            double offset, RelativeDateTimeUnit unit) {
        checkNoAdjustForContext();
        return new FormattedRelativeDateTime(formatNumericImpl(offset, unit));
    }

    /** Implementation method for formatNumeric and formatNumericToValue */
    private FormattedStringBuilder formatNumericImpl(double offset, RelativeDateTimeUnit unit) {
        // TODO:
        // The full implementation of this depends on CLDR data that is not yet available,
        // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data.
        // In the meantime do a quick bring-up by calling the old format method. When the
        // new CLDR data is available, update the data storage accordingly, rewrite this
        // to use it directly, and rewrite the old format method to call this new one;
        // that is covered by https://unicode-org.atlassian.net/browse/ICU-12171.
        RelativeUnit relunit = RelativeUnit.SECONDS;
        switch (unit) {
            case YEAR:
                relunit = RelativeUnit.YEARS;
                break;
            case QUARTER:
                relunit = RelativeUnit.QUARTERS;
                break;
            case MONTH:
                relunit = RelativeUnit.MONTHS;
                break;
            case WEEK:
                relunit = RelativeUnit.WEEKS;
                break;
            case DAY:
                relunit = RelativeUnit.DAYS;
                break;
            case HOUR:
                relunit = RelativeUnit.HOURS;
                break;
            case MINUTE:
                relunit = RelativeUnit.MINUTES;
                break;
            case SECOND:
                break; // set above
            case SUNDAY:
                relunit = RelativeUnit.SUNDAYS;
                break;
            case MONDAY:
                relunit = RelativeUnit.MONDAYS;
                break;
            case TUESDAY:
                relunit = RelativeUnit.TUESDAYS;
                break;
            case WEDNESDAY:
                relunit = RelativeUnit.WEDNESDAYS;
                break;
            case THURSDAY:
                relunit = RelativeUnit.THURSDAYS;
                break;
            case FRIDAY:
                relunit = RelativeUnit.FRIDAYS;
                break;
            case SATURDAY:
                relunit = RelativeUnit.SATURDAYS;
                break;
        }
        Direction direction = Direction.NEXT;
        if (Double.compare(offset, 0.0) < 0) { // needed to handle -0.0
            direction = Direction.LAST;
            offset = -offset;
        }
        return formatImpl(offset, direction, relunit);
    }

    private int[] styleToDateFormatSymbolsWidth = {
        DateFormatSymbols.WIDE, DateFormatSymbols.SHORT, DateFormatSymbols.NARROW
    };

    /**
     * Formats a relative date without a quantity.
     *
     * <p>This method returns a String. To get more information about the formatting result, use
     * formatToValue().
     *
     * @param direction NEXT, LAST, THIS, etc.
     * @param unit e.g SATURDAY, DAY, MONTH
     * @return the formatted string. If direction has a value that is documented as not being fully
     *     supported in every locale (for example NEXT_2 or LAST_2) then this function may return
     *     null to signal that no formatted string is available.
     * @throws IllegalArgumentException if the direction is incompatible with unit this can occur
     *     with NOW which can only take PLAIN.
     * @stable ICU 53
     */
    public String format(Direction direction, AbsoluteUnit unit) {
        String result = formatAbsoluteImpl(direction, unit);
        return result != null ? adjustForContext(result) : null;
    }

    /**
     * Formats a relative date without a quantity.
     *
     * <p>This method returns a FormattedRelativeDateTime, which exposes more information than the
     * String returned by format().
     *
     * @param direction NEXT, LAST, THIS, etc.
     * @param unit e.g SATURDAY, DAY, MONTH
     * @return the formatted string. If direction has a value that is documented as not being fully
     *     supported in every locale (for example NEXT_2 or LAST_2) then this function may return
     *     null to signal that no formatted string is available.
     * @throws IllegalArgumentException if the direction is incompatible with unit this can occur
     *     with NOW which can only take PLAIN.
     * @stable ICU 64
     */
    public FormattedRelativeDateTime formatToValue(Direction direction, AbsoluteUnit unit) {
        checkNoAdjustForContext();
        String string = formatAbsoluteImpl(direction, unit);
        if (string == null) {
            return null;
        }
        FormattedStringBuilder nsb = new FormattedStringBuilder();
        nsb.append(string, Field.LITERAL);
        return new FormattedRelativeDateTime(nsb);
    }

    /** Implementation method for format and formatToValue with AbsoluteUnit */
    private String formatAbsoluteImpl(Direction direction, AbsoluteUnit unit) {
        if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) {
            throw new IllegalArgumentException("NOW can only accept direction PLAIN.");
        }
        String result;
        // Get plain day of week names from DateFormatSymbols.
        if ((direction == Direction.PLAIN)
                && (AbsoluteUnit.SUNDAY.ordinal() <= unit.ordinal()
                        && unit.ordinal() <= AbsoluteUnit.SATURDAY.ordinal())) {
            // Convert from AbsoluteUnit days to Calendar class indexing.
            int dateSymbolsDayOrdinal =
                    (unit.ordinal() - AbsoluteUnit.SUNDAY.ordinal()) + Calendar.SUNDAY;
            String[] dayNames =
                    dateFormatSymbols.getWeekdays(
                            DateFormatSymbols.STANDALONE,
                            styleToDateFormatSymbolsWidth[style.ordinal()]);
            result = dayNames[dateSymbolsDayOrdinal];
        } else {
            // Not PLAIN, or not a weekday.
            result = getAbsoluteUnitString(style, unit, direction);
        }
        return result;
    }

    /**
     * Format a combination of RelativeDateTimeUnit and numeric offset using a text style if
     * possible, e.g. "last week", "this week", "next week", "yesterday", "tomorrow". Falls back to
     * numeric style if no appropriate text term is available for the specified offset in the
     * object’s locale.
     *
     * <p>This method returns a String. To get more information about the formatting result, use
     * formatToValue().
     *
     * @param offset The signed offset for the specified field.
     * @param unit The unit to use when formatting the relative date, e.g.
     *     RelativeDateTimeUnit.WEEK, RelativeDateTimeUnit.FRIDAY.
     * @return The formatted string (may be empty in case of error)
     * @stable ICU 57
     */
    public String format(double offset, RelativeDateTimeUnit unit) {
        return adjustForContext(formatRelativeImpl(offset, unit).toString());
    }

    /**
     * Format a combination of RelativeDateTimeUnit and numeric offset using a text style if
     * possible, e.g. "last week", "this week", "next week", "yesterday", "tomorrow". Falls back to
     * numeric style if no appropriate text term is available for the specified offset in the
     * object’s locale.
     *
     * <p>This method returns a FormattedRelativeDateTime, which exposes more information than the
     * String returned by format().
     *
     * @param offset The signed offset for the specified field.
     * @param unit The unit to use when formatting the relative date, e.g.
     *     RelativeDateTimeUnit.WEEK, RelativeDateTimeUnit.FRIDAY.
     * @return The formatted string (may be empty in case of error)
     * @stable ICU 64
     */
    public FormattedRelativeDateTime formatToValue(double offset, RelativeDateTimeUnit unit) {
        checkNoAdjustForContext();
        CharSequence cs = formatRelativeImpl(offset, unit);
        FormattedStringBuilder nsb;
        if (cs instanceof FormattedStringBuilder) {
            nsb = (FormattedStringBuilder) cs;
        } else {
            nsb = new FormattedStringBuilder();
            nsb.append(cs, Field.LITERAL);
        }
        return new FormattedRelativeDateTime(nsb);
    }

    /** Implementation method for format and formatToValue with RelativeDateTimeUnit. */
    private CharSequence formatRelativeImpl(double offset, RelativeDateTimeUnit unit) {
        // TODO:
        // The full implementation of this depends on CLDR data that is not yet available,
        // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data.
        // In the meantime do a quick bring-up by calling the old format method. When the
        // new CLDR data is available, update the data storage accordingly, rewrite this
        // to use it directly, and rewrite the old format method to call this new one;
        // that is covered by https://unicode-org.atlassian.net/browse/ICU-12171.
        boolean useNumeric = true;
        Direction direction = Direction.THIS;
        if (offset > -2.1 && offset < 2.1) {
            // Allow a 1% epsilon, so offsets in -1.01..-0.99 map to LAST
            double offsetx100 = offset * 100.0;
            int intoffsetx100 =
                    (offsetx100 < 0) ? (int) (offsetx100 - 0.5) : (int) (offsetx100 + 0.5);
            switch (intoffsetx100) {
                case -200 /*-2*/:
                    direction = Direction.LAST_2;
                    useNumeric = false;
                    break;
                case -100 /*-1*/:
                    direction = Direction.LAST;
                    useNumeric = false;
                    break;
                case 0 /* 0*/:
                    useNumeric = false;
                    break; // direction = Direction.THIS was set above
                case 100 /* 1*/:
                    direction = Direction.NEXT;
                    useNumeric = false;
                    break;
                case 200 /* 2*/:
                    direction = Direction.NEXT_2;
                    useNumeric = false;
                    break;
                default:
                    break;
            }
        }
        AbsoluteUnit absunit = AbsoluteUnit.NOW;
        switch (unit) {
            case YEAR:
                absunit = AbsoluteUnit.YEAR;
                break;
            case QUARTER:
                absunit = AbsoluteUnit.QUARTER;
                break;
            case MONTH:
                absunit = AbsoluteUnit.MONTH;
                break;
            case WEEK:
                absunit = AbsoluteUnit.WEEK;
                break;
            case DAY:
                absunit = AbsoluteUnit.DAY;
                break;
            case SUNDAY:
                absunit = AbsoluteUnit.SUNDAY;
                break;
            case MONDAY:
                absunit = AbsoluteUnit.MONDAY;
                break;
            case TUESDAY:
                absunit = AbsoluteUnit.TUESDAY;
                break;
            case WEDNESDAY:
                absunit = AbsoluteUnit.WEDNESDAY;
                break;
            case THURSDAY:
                absunit = AbsoluteUnit.THURSDAY;
                break;
            case FRIDAY:
                absunit = AbsoluteUnit.FRIDAY;
                break;
            case SATURDAY:
                absunit = AbsoluteUnit.SATURDAY;
                break;
            case HOUR:
                absunit = AbsoluteUnit.HOUR;
                break;
            case MINUTE:
                absunit = AbsoluteUnit.MINUTE;
                break;
            case SECOND:
                if (direction == Direction.THIS) {
                    // absunit = AbsoluteUnit.NOW was set above
                    direction = Direction.PLAIN;
                    break;
                }
                // could just fall through here but that produces warnings
                useNumeric = true;
                break;
            default:
                useNumeric = true;
                break;
        }
        if (!useNumeric) {
            String result = formatAbsoluteImpl(direction, absunit);
            if (result != null && result.length() > 0) {
                return result;
            }
        }
        // otherwise fallback to formatNumeric
        return formatNumericImpl(offset, unit);
    }

    /** Gets the string value from qualitativeUnitMap with fallback based on style. */
    private String getAbsoluteUnitString(Style style, AbsoluteUnit unit, Direction direction) {
        EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap;
        EnumMap<Direction, String> dirMap;

        do {
            unitMap = qualitativeUnitMap.get(style);
            if (unitMap != null) {
                dirMap = unitMap.get(unit);
                if (dirMap != null) {
                    String result = dirMap.get(direction);
                    if (result != null) {
                        return result;
                    }
                }
            }

            // Consider other styles from alias fallback.
            // Data loading guaranteed no endless loops.
        } while ((style = fallbackCache[style.ordinal()]) != null);
        return null;
    }

    /**
     * Combines a relative date string and a time string in this object's locale. This is done with
     * the same date-time separator used for the default calendar in this locale.
     *
     * @param relativeDateString the relative date e.g 'yesterday'
     * @param timeString the time e.g '3:45'
     * @return the date and time concatenated according to the default calendar in this locale e.g
     *     'yesterday, 3:45'
     * @stable ICU 53
     */
    public String combineDateAndTime(String relativeDateString, String timeString) {
        return SimpleFormatterImpl.formatCompiledPattern(
                combinedDateAndTime, timeString, relativeDateString);
    }

    /**
     * Returns a copy of the NumberFormat this object is using.
     *
     * @return A copy of the NumberFormat.
     * @stable ICU 53
     */
    public NumberFormat getNumberFormat() {
        // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this
        // class we must guarantee that only one thread at a time uses our numberFormat.
        synchronized (numberFormat) {
            return numberFormat.clone();
        }
    }

    /**
     * Return capitalization context.
     *
     * @return The capitalization context.
     * @stable ICU 54
     */
    public DisplayContext getCapitalizationContext() {
        return capitalizationContext;
    }

    /**
     * Return style
     *
     * @return The formatting style.
     * @stable ICU 54
     */
    public Style getFormatStyle() {
        return style;
    }

    private String adjustForContext(String originalFormattedString) {
        if (breakIterator == null
                || originalFormattedString.length() == 0
                || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) {
            return originalFormattedString;
        }
        synchronized (breakIterator) {
            return UCharacter.toTitleCase(
                    locale,
                    originalFormattedString,
                    breakIterator,
                    UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT);
        }
    }

    private void checkNoAdjustForContext() {
        if (breakIterator != null) {
            throw new UnsupportedOperationException(
                    "Capitalization context is not supported in formatV");
        }
    }

    private RelativeDateTimeFormatter(
            EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
            EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap,
            String combinedDateAndTime,
            PluralRules pluralRules,
            NumberFormat numberFormat,
            Style style,
            DisplayContext capitalizationContext,
            BreakIterator breakIterator,
            ULocale locale) {
        this.qualitativeUnitMap = qualitativeUnitMap;
        this.patternMap = patternMap;
        this.combinedDateAndTime = combinedDateAndTime;
        this.pluralRules = pluralRules;
        this.numberFormat = numberFormat;
        this.style = style;
        if (capitalizationContext.type() != DisplayContext.Type.CAPITALIZATION) {
            throw new IllegalArgumentException(capitalizationContext.toString());
        }
        this.capitalizationContext = capitalizationContext;
        this.breakIterator = breakIterator;
        this.locale = locale;
        this.dateFormatSymbols = new DateFormatSymbols(locale);
    }

    private String getRelativeUnitPluralPattern(
            Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) {
        if (pluralForm != StandardPlural.OTHER) {
            String formatter = getRelativeUnitPattern(style, unit, pastFutureIndex, pluralForm);
            if (formatter != null) {
                return formatter;
            }
        }
        return getRelativeUnitPattern(style, unit, pastFutureIndex, StandardPlural.OTHER);
    }

    private String getRelativeUnitPattern(
            Style style, RelativeUnit unit, int pastFutureIndex, StandardPlural pluralForm) {
        int pluralIndex = pluralForm.ordinal();
        do {
            EnumMap<RelativeUnit, String[][]> unitMap = patternMap.get(style);
            if (unitMap != null) {
                String[][] spfCompiledPatterns = unitMap.get(unit);
                if (spfCompiledPatterns != null) {
                    if (spfCompiledPatterns[pastFutureIndex][pluralIndex] != null) {
                        return spfCompiledPatterns[pastFutureIndex][pluralIndex];
                    }
                }
            }

            // Consider other styles from alias fallback.
            // Data loading guaranteed no endless loops.
        } while ((style = fallbackCache[style.ordinal()]) != null);
        return null;
    }

    private final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>
            qualitativeUnitMap;
    private final EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap;

    private final String combinedDateAndTime; // compiled SimpleFormatter pattern
    private final PluralRules pluralRules;
    private final NumberFormat numberFormat;

    private final Style style;
    private final DisplayContext capitalizationContext;
    private final BreakIterator breakIterator;
    private final ULocale locale;

    private final DateFormatSymbols dateFormatSymbols;

    private static final Style fallbackCache[] = new Style[Style.INDEX_COUNT];

    private static class RelativeDateTimeFormatterData {
        public RelativeDateTimeFormatterData(
                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>
                        qualitativeUnitMap,
                EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap,
                String dateTimePattern) {
            this.qualitativeUnitMap = qualitativeUnitMap;
            this.relUnitPatternMap = relUnitPatternMap;

            this.dateTimePattern = dateTimePattern;
        }

        public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>
                qualitativeUnitMap;
        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap;
        public final String dateTimePattern; // Example: "{1}, {0}"
    }

    private static class Cache {
        private final CacheBase<String, RelativeDateTimeFormatterData, ULocale> cache =
                new SoftCache<String, RelativeDateTimeFormatterData, ULocale>() {
                    @Override
                    protected RelativeDateTimeFormatterData createInstance(
                            String key, ULocale locale) {
                        return new Loader(locale).load();
                    }
                };

        public RelativeDateTimeFormatterData get(ULocale locale) {
            String key = locale.toString();
            return cache.getInstance(key, locale);
        }
    }

    private static Direction keyToDirection(UResource.Key key) {
        if (key.contentEquals("-2")) {
            return Direction.LAST_2;
        }
        if (key.contentEquals("-1")) {
            return Direction.LAST;
        }
        if (key.contentEquals("0")) {
            return Direction.THIS;
        }
        if (key.contentEquals("1")) {
            return Direction.NEXT;
        }
        if (key.contentEquals("2")) {
            return Direction.NEXT_2;
        }
        return null;
    }

    /**
     * Sink for enumerating all of the relative data time formatter names.
     *
     * <p>More specific bundles (en_GB) are enumerated before their parents (en_001, en, root): Only
     * store a value if it is still missing, that is, it has not been overridden.
     */
    private static final class RelDateTimeDataSink extends UResource.Sink {

        // For white list of units to handle in RelativeDateTimeFormatter.
        private enum DateTimeUnit {
            SECOND(RelativeUnit.SECONDS, null),
            MINUTE(RelativeUnit.MINUTES, AbsoluteUnit.MINUTE),
            HOUR(RelativeUnit.HOURS, AbsoluteUnit.HOUR),
            DAY(RelativeUnit.DAYS, AbsoluteUnit.DAY),
            WEEK(RelativeUnit.WEEKS, AbsoluteUnit.WEEK),
            MONTH(RelativeUnit.MONTHS, AbsoluteUnit.MONTH),
            QUARTER(RelativeUnit.QUARTERS, AbsoluteUnit.QUARTER),
            YEAR(RelativeUnit.YEARS, AbsoluteUnit.YEAR),
            SUNDAY(RelativeUnit.SUNDAYS, AbsoluteUnit.SUNDAY),
            MONDAY(RelativeUnit.MONDAYS, AbsoluteUnit.MONDAY),
            TUESDAY(RelativeUnit.TUESDAYS, AbsoluteUnit.TUESDAY),
            WEDNESDAY(RelativeUnit.WEDNESDAYS, AbsoluteUnit.WEDNESDAY),
            THURSDAY(RelativeUnit.THURSDAYS, AbsoluteUnit.THURSDAY),
            FRIDAY(RelativeUnit.FRIDAYS, AbsoluteUnit.FRIDAY),
            SATURDAY(RelativeUnit.SATURDAYS, AbsoluteUnit.SATURDAY);

            RelativeUnit relUnit;
            AbsoluteUnit absUnit;

            DateTimeUnit(RelativeUnit relUnit, AbsoluteUnit absUnit) {
                this.relUnit = relUnit;
                this.absUnit = absUnit;
            }

            private static final DateTimeUnit orNullFromString(CharSequence keyword) {
                // Quick check from string to enum.
                switch (keyword.length()) {
                    case 3:
                        if ("day".contentEquals(keyword)) {
                            return DAY;
                        } else if ("sun".contentEquals(keyword)) {
                            return SUNDAY;
                        } else if ("mon".contentEquals(keyword)) {
                            return MONDAY;
                        } else if ("tue".contentEquals(keyword)) {
                            return TUESDAY;
                        } else if ("wed".contentEquals(keyword)) {
                            return WEDNESDAY;
                        } else if ("thu".contentEquals(keyword)) {
                            return THURSDAY;
                        } else if ("fri".contentEquals(keyword)) {
                            return FRIDAY;
                        } else if ("sat".contentEquals(keyword)) {
                            return SATURDAY;
                        }
                        break;
                    case 4:
                        if ("hour".contentEquals(keyword)) {
                            return HOUR;
                        } else if ("week".contentEquals(keyword)) {
                            return WEEK;
                        } else if ("year".contentEquals(keyword)) {
                            return YEAR;
                        }
                        break;
                    case 5:
                        if ("month".contentEquals(keyword)) {
                            return MONTH;
                        }
                        break;
                    case 6:
                        if ("minute".contentEquals(keyword)) {
                            return MINUTE;
                        } else if ("second".contentEquals(keyword)) {
                            return SECOND;
                        }
                        break;
                    case 7:
                        if ("quarter".contentEquals(keyword)) {
                            return QUARTER; // RelativeUnit.QUARTERS is deprecated
                        }
                        break;
                    default:
                        break;
                }
                return null;
            }
        }

        EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap =
                new EnumMap<>(Style.class);
        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> styleRelUnitPatterns =
                new EnumMap<>(Style.class);

        StringBuilder sb = new StringBuilder();

        // Values keep between levels of parsing the CLDR data.
        int pastFutureIndex;
        Style style; // {LONG, SHORT, NARROW} Derived from unit key string.
        DateTimeUnit
                unit; // From the unit key string, with the style (e.g., "-short") separated out.

        private Style styleFromKey(UResource.Key key) {
            if (key.endsWith("-short")) {
                return Style.SHORT;
            } else if (key.endsWith("-narrow")) {
                return Style.NARROW;
            } else {
                return Style.LONG;
            }
        }

        private Style styleFromAlias(UResource.Value value) {
            String s = value.getAliasString();
            if (s.endsWith("-short")) {
                return Style.SHORT;
            } else if (s.endsWith("-narrow")) {
                return Style.NARROW;
            } else {
                return Style.LONG;
            }
        }

        private static int styleSuffixLength(Style style) {
            switch (style) {
                case SHORT:
                    return 6;
                case NARROW:
                    return 7;
                default:
                    return 0;
            }
        }

        public void consumeTableRelative(UResource.Key key, UResource.Value value) {
            UResource.Table unitTypesTable = value.getTable();
            for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) {
                if (value.getType() == ICUResourceBundle.STRING) {
                    String valueString = value.getString();

                    EnumMap<AbsoluteUnit, EnumMap<Direction, String>> absMap =
                            qualitativeUnitMap.get(style);

                    if (unit.relUnit == RelativeUnit.SECONDS) {
                        if (key.contentEquals("0")) {
                            // Handle Zero seconds for "now".
                            EnumMap<Direction, String> unitStrings = absMap.get(AbsoluteUnit.NOW);
                            if (unitStrings == null) {
                                unitStrings = new EnumMap<>(Direction.class);
                                absMap.put(AbsoluteUnit.NOW, unitStrings);
                            }
                            if (unitStrings.get(Direction.PLAIN) == null) {
                                unitStrings.put(Direction.PLAIN, valueString);
                            }
                            continue;
                        }
                    }
                    Direction keyDirection = keyToDirection(key);
                    if (keyDirection == null) {
                        continue;
                    }
                    AbsoluteUnit absUnit = unit.absUnit;
                    if (absUnit == null) {
                        continue;
                    }

                    if (absMap == null) {
                        absMap = new EnumMap<>(AbsoluteUnit.class);
                        qualitativeUnitMap.put(style, absMap);
                    }
                    EnumMap<Direction, String> dirMap = absMap.get(absUnit);
                    if (dirMap == null) {
                        dirMap = new EnumMap<>(Direction.class);
                        absMap.put(absUnit, dirMap);
                    }
                    if (dirMap.get(keyDirection) == null) {
                        // Do not override values already entered.
                        dirMap.put(keyDirection, value.getString());
                    }
                }
            }
        }

        // Record past or future and
        public void consumeTableRelativeTime(UResource.Key key, UResource.Value value) {
            if (unit.relUnit == null) {
                return;
            }
            UResource.Table unitTypesTable = value.getTable();
            for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) {
                if (key.contentEquals("past")) {
                    pastFutureIndex = 0;
                } else if (key.contentEquals("future")) {
                    pastFutureIndex = 1;
                } else {
                    continue;
                }
                // Get the details of the relative time.
                consumeTimeDetail(key, value);
            }
        }

        public void consumeTimeDetail(UResource.Key key, UResource.Value value) {
            UResource.Table unitTypesTable = value.getTable();

            EnumMap<RelativeUnit, String[][]> unitPatterns = styleRelUnitPatterns.get(style);
            if (unitPatterns == null) {
                unitPatterns = new EnumMap<>(RelativeUnit.class);
                styleRelUnitPatterns.put(style, unitPatterns);
            }
            String[][] patterns = unitPatterns.get(unit.relUnit);
            if (patterns == null) {
                patterns = new String[2][StandardPlural.COUNT];
                unitPatterns.put(unit.relUnit, patterns);
            }

            // Stuff the pattern for the correct plural index with a simple formatter.
            for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) {
                if (value.getType() == ICUResourceBundle.STRING) {
                    int pluralIndex = StandardPlural.indexFromString(key.toString());
                    if (patterns[pastFutureIndex][pluralIndex] == null) {
                        patterns[pastFutureIndex][pluralIndex] =
                                SimpleFormatterImpl.compileToStringMinMaxArguments(
                                        value.getString(), sb, 0, 1);
                    }
                }
            }
        }

        private void handlePlainDirection(UResource.Key key, UResource.Value value) {
            AbsoluteUnit absUnit = unit.absUnit;
            if (absUnit == null) {
                return; // Not interesting.
            }
            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> unitMap =
                    qualitativeUnitMap.get(style);
            if (unitMap == null) {
                unitMap = new EnumMap<>(AbsoluteUnit.class);
                qualitativeUnitMap.put(style, unitMap);
            }
            EnumMap<Direction, String> dirMap = unitMap.get(absUnit);
            if (dirMap == null) {
                dirMap = new EnumMap<>(Direction.class);
                unitMap.put(absUnit, dirMap);
            }
            if (dirMap.get(Direction.PLAIN) == null) {
                dirMap.put(Direction.PLAIN, value.toString());
            }
        }

        // Handle at the Unit level,
        public void consumeTimeUnit(UResource.Key key, UResource.Value value) {
            UResource.Table unitTypesTable = value.getTable();
            for (int i = 0; unitTypesTable.getKeyAndValue(i, key, value); i++) {
                if (key.contentEquals("dn") && value.getType() == ICUResourceBundle.STRING) {
                    handlePlainDirection(key, value);
                }
                if (value.getType() == ICUResourceBundle.TABLE) {
                    if (key.contentEquals("relative")) {
                        consumeTableRelative(key, value);
                    } else if (key.contentEquals("relativeTime")) {
                        consumeTableRelativeTime(key, value);
                    }
                }
            }
        }

        private void handleAlias(UResource.Key key, UResource.Value value, boolean noFallback) {
            Style sourceStyle = styleFromKey(key);
            int limit = key.length() - styleSuffixLength(sourceStyle);
            DateTimeUnit unit = DateTimeUnit.orNullFromString(key.substring(0, limit));
            if (unit != null) {
                // Record the fallback chain for the values.
                // At formatting time, limit to 2 levels of fallback.
                Style targetStyle = styleFromAlias(value);
                if (sourceStyle == targetStyle) {
                    throw new ICUException(
                            "Invalid style fallback from " + sourceStyle + " to itself");
                }

                // Check for inconsistent fallbacks.
                if (fallbackCache[sourceStyle.ordinal()] == null) {
                    fallbackCache[sourceStyle.ordinal()] = targetStyle;
                } else if (fallbackCache[sourceStyle.ordinal()] != targetStyle) {
                    throw new ICUException(
                            "Inconsistent style fallback for style "
                                    + sourceStyle
                                    + " to "
                                    + targetStyle);
                }
                return;
            }
        }

        @Override
        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
            // Main entry point to sink
            if (value.getType() == ICUResourceBundle.ALIAS) {
                return;
            }

            UResource.Table table = value.getTable();
            // Process each key / value in this table.
            for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
                if (value.getType() == ICUResourceBundle.ALIAS) {
                    handleAlias(key, value, noFallback);
                } else {
                    // Remember style and unit for deeper levels.
                    style = styleFromKey(key);
                    int limit = key.length() - styleSuffixLength(style);
                    unit = DateTimeUnit.orNullFromString(key.substring(0, limit));
                    if (unit != null) {
                        // Process only if unitString is in the white list.
                        consumeTimeUnit(key, value);
                    }
                }
            }
        }

        RelDateTimeDataSink() {}
    }

    private static class Loader {
        private final ULocale ulocale;

        public Loader(ULocale ulocale) {
            this.ulocale = ulocale;
        }

        private String getDateTimePattern() {
            Calendar cal = Calendar.getInstance(ulocale);
            return Calendar.getDateAtTimePattern(cal, ulocale, DateFormat.MEDIUM);
        }

        public RelativeDateTimeFormatterData load() {
            // Sink for traversing data.
            RelDateTimeDataSink sink = new RelDateTimeDataSink();

            ICUResourceBundle r =
                    (ICUResourceBundle)
                            UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
            r.getAllItemsWithFallback("fields", sink);

            // Check fallbacks array for loops or too many levels.
            for (Style testStyle : Style.values()) {
                Style newStyle1 = fallbackCache[testStyle.ordinal()];
                // Data loading guaranteed newStyle1 != testStyle.
                if (newStyle1 != null) {
                    Style newStyle2 = fallbackCache[newStyle1.ordinal()];
                    if (newStyle2 != null) {
                        // No fallback should take more than 2 steps.
                        if (fallbackCache[newStyle2.ordinal()] != null) {
                            throw new IllegalStateException("Style fallback too deep");
                        }
                    }
                }
            }

            return new RelativeDateTimeFormatterData(
                    sink.qualitativeUnitMap, sink.styleRelUnitPatterns, getDateTimePattern());
        }
    }

    private static final Cache cache = new Cache();
}