]> granicus.if.org Git - icu/commitdiff
ICU-12018 Overhaul RelativeDateTimeFormatter data loading
authorCraig Cornelius <ccornelius@google.com>
Fri, 15 Jan 2016 23:50:25 +0000 (23:50 +0000)
committerCraig Cornelius <ccornelius@google.com>
Fri, 15 Jan 2016 23:50:25 +0000 (23:50 +0000)
X-SVN-Rev: 38176

icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java

index d928ca50a5b221c259eecaa6b17da1a5b4942b3e..6120b3cd7646b544f0088b5d213f71a8f0828fa2 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 2013-2015, International Business Machines Corporation and
+ * Copyright (C) 2013-2016, International Business Machines Corporation and
  * others. All Rights Reserved.
  *******************************************************************************
  */
@@ -10,11 +10,17 @@ import java.util.EnumMap;
 import java.util.Locale;
 
 import com.ibm.icu.impl.CalendarData;
+import com.ibm.icu.impl.DontCareFieldPosition;
 import com.ibm.icu.impl.ICUCache;
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.SimpleCache;
+import com.ibm.icu.impl.SimplePatternFormatter;
+import com.ibm.icu.impl.StandardPlural;
 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.impl.UResource;
 import com.ibm.icu.util.UResourceBundle;
 
 
@@ -88,9 +94,11 @@ public final class RelativeDateTimeFormatter {
          * Use single letters when possible.
          * @stable ICU 54
          */
-        NARROW,
+        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"
@@ -139,6 +147,14 @@ public final class RelativeDateTimeFormatter {
          * @stable ICU 53
          */
         YEARS,
+
+        /**
+         * Quarters
+         * @internal TODO: propose for addition in ICU 57
+         * @deprecated This API is ICU internal only.
+         */
+        @Deprecated
+        QUARTERS,
     }
     
     /**
@@ -218,6 +234,14 @@ public final class RelativeDateTimeFormatter {
          * @stable ICU 53
          */
         NOW,
+
+        /*
+         * Quarter
+         * @internal TODO: propose for addition in ICU 57
+         * @deprecated This API is ICU internal only.
+         */
+        @Deprecated
+        QUARTER,
       }
 
       /**
@@ -226,7 +250,6 @@ public final class RelativeDateTimeFormatter {
        * @stable ICU 53
        */
       public static enum Direction {
-          
           /**
            * Two before. Not fully supported in every locale
            * @stable ICU 53
@@ -332,7 +355,7 @@ public final class RelativeDateTimeFormatter {
         }
         return new RelativeDateTimeFormatter(
                 data.qualitativeUnitMap,
-                data.quantitativeUnitMap,
+                data.relUnitPatternMap,
                 new MessageFormat(data.dateTimePattern),
                 PluralRules.forLocale(locale),
                 nf,
@@ -341,9 +364,8 @@ public final class RelativeDateTimeFormatter {
                 capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE ?
                     BreakIterator.getSentenceInstance(locale) : null,
                 locale);
-                
     }
-           
+
     /**
      * Returns a RelativeDateTimeFormatter for a particular JDK locale that uses a particular
      * NumberFormat object.
@@ -376,16 +398,26 @@ public final class RelativeDateTimeFormatter {
             throw new IllegalArgumentException("direction must be NEXT or LAST");
         }
         String result;
+        int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0);
+
         // 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) {
-            result = getQuantity(
-                    unit, direction == Direction.NEXT).format(
-                            quantity, numberFormat, pluralRules);
+            StringBuffer formatStr = new StringBuffer();
+            DontCareFieldPosition fieldPosition = DontCareFieldPosition.INSTANCE;
+            StandardPlural pluralForm = QuantityFormatter.selectPlural(quantity,
+                    numberFormat, pluralRules, formatStr, fieldPosition);
+
+            String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm);
+            result = SimplePatternFormatter.formatCompiledPattern(formatter, formatStr);
         }
         return adjustForContext(result);
+
     }
-    
+
+    private int[] styleToDateFormatSymbolsWidth = {
+                DateFormatSymbols.WIDE, DateFormatSymbols.SHORT, DateFormatSymbols.NARROW
+    };
 
     /**
      * Formats a relative date without a quantity.
@@ -402,10 +434,53 @@ public final class RelativeDateTimeFormatter {
         if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) {
             throw new IllegalArgumentException("NOW can only accept direction PLAIN.");
         }
-        String result = this.qualitativeUnitMap.get(style).get(unit).get(direction);
+        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 != null ? adjustForContext(result) : null;
     }
 
+    /**
+     * Gets the string value from qualitativeUnitMap with fallback based on style.
+     * @param style
+     * @param unit
+     * @param direction
+     * @return
+     */
+    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
@@ -420,7 +495,7 @@ public final class RelativeDateTimeFormatter {
         return this.combinedDateAndTime.format(
             new Object[]{timeString, relativeDateString}, new StringBuffer(), null).toString();
     }
-    
+
     /**
      * Returns a copy of the NumberFormat this object is using.
      * @return A copy of the NumberFormat.
@@ -433,7 +508,7 @@ public final class RelativeDateTimeFormatter {
             return (NumberFormat) numberFormat.clone();
         }
     }
-    
+
     /**
      * Return capitalization context.
      *
@@ -451,7 +526,7 @@ public final class RelativeDateTimeFormatter {
     public Style getFormatStyle() {
         return style;
     }
-    
+
     private String adjustForContext(String originalFormattedString) {
         if (breakIterator == null || originalFormattedString.length() == 0 
                 || !UCharacter.isLowerCase(UCharacter.codePointAt(originalFormattedString, 0))) {
@@ -465,45 +540,10 @@ public final class RelativeDateTimeFormatter {
                     UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT);
         }
     }
-    
-    private static void addQualitativeUnit(
-            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnits,
-            AbsoluteUnit unit,
-            String current) {
-        EnumMap<Direction, String> unitStrings =
-                new EnumMap<Direction, String>(Direction.class);
-        unitStrings.put(Direction.PLAIN, current);
-        qualitativeUnits.put(unit,  unitStrings);       
-    }
-
-    private static void addQualitativeUnit(
-            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnits,
-            AbsoluteUnit unit, ICUResourceBundle bundle, String plain) {
-        EnumMap<Direction, String> unitStrings =
-                new EnumMap<Direction, String>(Direction.class);
-        unitStrings.put(Direction.LAST, bundle.getStringWithFallback("-1"));
-        unitStrings.put(Direction.THIS, bundle.getStringWithFallback("0"));
-        unitStrings.put(Direction.NEXT, bundle.getStringWithFallback("1"));
-        addOptionalDirection(unitStrings, Direction.LAST_2, bundle, "-2");
-        addOptionalDirection(unitStrings, Direction.NEXT_2, bundle, "2");
-        unitStrings.put(Direction.PLAIN, plain);
-        qualitativeUnits.put(unit,  unitStrings);
-    }
-    private static void addOptionalDirection(
-            EnumMap<Direction, String> unitStrings,
-            Direction direction,
-            ICUResourceBundle bundle,
-            String key) {
-        String s = bundle.findStringWithFallback(key);
-        if (s != null) {
-            unitStrings.put(direction, s);
-        }
-    }
 
     private RelativeDateTimeFormatter(
             EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap,
-            EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap,
+            EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap, 
             MessageFormat combinedDateAndTime,
             PluralRules pluralRules,
             NumberFormat numberFormat,
@@ -512,7 +552,7 @@ public final class RelativeDateTimeFormatter {
             BreakIterator breakIterator,
             ULocale locale) {
         this.qualitativeUnitMap = qualitativeUnitMap;
-        this.quantitativeUnitMap = quantitativeUnitMap;
+        this.patternMap = patternMap;
         this.combinedDateAndTime = combinedDateAndTime;
         this.pluralRules = pluralRules;
         this.numberFormat = numberFormat;
@@ -523,38 +563,73 @@ public final class RelativeDateTimeFormatter {
         this.capitalizationContext = capitalizationContext;
         this.breakIterator = breakIterator;
         this.locale = locale;
+        this.dateFormatSymbols = new DateFormatSymbols(locale);
     }
-    
-    private QuantityFormatter getQuantity(RelativeUnit unit, boolean isFuture) {
-        QuantityFormatter[] quantities = quantitativeUnitMap.get(style).get(unit);
-        return isFuture ? quantities[1] : quantities[0];
+
+    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, QuantityFormatter[]>> quantitativeUnitMap;
+    private final EnumMap<Style, EnumMap<RelativeUnit, String[][]>> patternMap;
+
     private final MessageFormat combinedDateAndTime;
     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, QuantityFormatter[]>> quantitativeUnitMap,
+                EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap,
                 String dateTimePattern) {
             this.qualitativeUnitMap = qualitativeUnitMap;
-            this.quantitativeUnitMap = quantitativeUnitMap;
+            this.relUnitPatternMap = relUnitPatternMap;
+
             this.dateTimePattern = dateTimePattern;
         }
-        
+
         public final EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap;
-        public final EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap;
+        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> relUnitPatternMap;
         public final String dateTimePattern;  // Example: "{1}, {0}"
     }
-    
+
     private static class Cache {
         private final ICUCache<String, RelativeDateTimeFormatterData> cache =
             new SimpleCache<String, RelativeDateTimeFormatterData>();
@@ -569,292 +644,388 @@ public final class RelativeDateTimeFormatter {
             return result;
         }
     }
-    
-    private static class Loader {
-        private final ULocale ulocale;
-        
-        public Loader(ULocale ulocale) {
-            this.ulocale = ulocale;
+
+    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;
+    }
 
-        public RelativeDateTimeFormatterData load() {
-            EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap = 
-                    new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class);
-            
-            EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap =
-                    new EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>>(Style.class);
-            
-            for (Style style : Style.values()) {
-                qualitativeUnitMap.put(style, new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class));
-                quantitativeUnitMap.put(style, new EnumMap<RelativeUnit, QuantityFormatter[]>(RelativeUnit.class));                
+    /**
+     * Sink for enumerating all of the relative data time formatter names.
+     * Contains inner sink classes, each one corresponding to a type of resource table.
+     * The outer sink handles the top-level 'fields'.
+     *
+     * 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.
+     *
+     * C++: Each inner sink class has a reference to the main outer sink.
+     * Java: Use non-static inner classes instead.
+     */
+    private static final class RelDateTimeFmtDataSink extends UResource.TableSink {
+        // For white list of units to handle in RelativeDateTimeFormatter.
+        private static enum DateTimeUnit {
+            SECOND(RelativeUnit.SECONDS, null),
+            MINUTE(RelativeUnit.MINUTES, null),
+            HOUR(RelativeUnit.HOURS, null),
+            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(null, AbsoluteUnit.SUNDAY),
+            MONDAY(null, AbsoluteUnit.MONDAY),
+            TUESDAY(null, AbsoluteUnit.TUESDAY),
+            WEDNESDAY(null, AbsoluteUnit.WEDNESDAY),
+            THURSDAY(null, AbsoluteUnit.THURSDAY),
+            FRIDAY(null, AbsoluteUnit.FRIDAY),
+            SATURDAY(null, 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;  // TODO: Check @provisional
+                    }
+                    break;
+                default:
+                    break;
+                }
+                return null;
             }
-                    
-            ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
-                    getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
-            addTimeUnits(
-                    r,
-                    "fields/day", "fields/day-short", "fields/day-narrow",
-                    RelativeUnit.DAYS,
-                    AbsoluteUnit.DAY,
-                    quantitativeUnitMap,
-                    qualitativeUnitMap);
-            addTimeUnits(
-                    r,
-                    "fields/week", "fields/week-short", "fields/week-narrow",
-                    RelativeUnit.WEEKS,
-                    AbsoluteUnit.WEEK,
-                    quantitativeUnitMap,
-                    qualitativeUnitMap);
-            addTimeUnits(
-                    r,
-                    "fields/month", "fields/month-short", "fields/month-narrow",
-                    RelativeUnit.MONTHS,
-                    AbsoluteUnit.MONTH,
-                    quantitativeUnitMap,
-                    qualitativeUnitMap);
-            addTimeUnits(
-                    r,
-                    "fields/year", "fields/year-short", "fields/year-narrow",
-                    RelativeUnit.YEARS,
-                    AbsoluteUnit.YEAR,
-                    quantitativeUnitMap,
-                    qualitativeUnitMap);
-            initRelativeUnits(
-                    r,
-                    "fields/second", "fields/second-short", "fields/second-narrow",
-                    RelativeUnit.SECONDS,
-                    quantitativeUnitMap);
-            initRelativeUnits(
-                    r,
-                    "fields/minute", "fields/minute-short", "fields/minute-narrow",
-                    RelativeUnit.MINUTES,
-                    quantitativeUnitMap);
-            initRelativeUnits(
-                    r,
-                    "fields/hour", "fields/hour-short", "fields/hour-narrow",
-                    RelativeUnit.HOURS,
-                    quantitativeUnitMap);
-            
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.LONG),
-                    AbsoluteUnit.NOW,
-                    r.getStringWithFallback("fields/second/relative/0"));
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.SHORT),
-                    AbsoluteUnit.NOW,
-                    r.getStringWithFallback("fields/second-short/relative/0"));
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.NARROW),
-                    AbsoluteUnit.NOW,
-                    r.getStringWithFallback("fields/second-narrow/relative/0"));
-            
-            EnumMap<Style, EnumMap<AbsoluteUnit, String>> dayOfWeekMap = 
-                    new EnumMap<Style, EnumMap<AbsoluteUnit, String>>(Style.class);
-            dayOfWeekMap.put(Style.LONG, readDaysOfWeek(
-                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/wide")));
-            dayOfWeekMap.put(Style.SHORT, readDaysOfWeek(
-                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/short")));
-            dayOfWeekMap.put(Style.NARROW, readDaysOfWeek(
-                    r.getWithFallback("calendar/gregorian/dayNames/stand-alone/narrow")));
-            
-            addWeekDays(
-                    r,
-                    "fields/mon/relative",
-                    "fields/mon-short/relative",
-                    "fields/mon-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.MONDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/tue/relative",
-                    "fields/tue-short/relative",
-                    "fields/tue-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.TUESDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/wed/relative",
-                    "fields/wed-short/relative",
-                    "fields/wed-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.WEDNESDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/thu/relative",
-                    "fields/thu-short/relative",
-                    "fields/thu-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.THURSDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/fri/relative",
-                    "fields/fri-short/relative",
-                    "fields/fri-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.FRIDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/sat/relative",
-                    "fields/sat-short/relative",
-                    "fields/sat-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.SATURDAY,
-                    qualitativeUnitMap);
-            addWeekDays(
-                    r,
-                    "fields/sun/relative",
-                    "fields/sun-short/relative",
-                    "fields/sun-narrow/relative",
-                    dayOfWeekMap,
-                    AbsoluteUnit.SUNDAY,
-                    qualitativeUnitMap);   
-            CalendarData calData = new CalendarData(
-                    ulocale, r.getStringWithFallback("calendar/default"));  
-            return new RelativeDateTimeFormatterData(
-                    qualitativeUnitMap, quantitativeUnitMap, calData.getDateTimePattern());
         }
 
-        private void addTimeUnits(
-                ICUResourceBundle r,
-                String path, String pathShort, String pathNarrow,
-                RelativeUnit relativeUnit, 
-                AbsoluteUnit absoluteUnit,
-                EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap,
-                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap) {
-           addTimeUnit(
-                   r.getWithFallback(path),
-                   relativeUnit,
-                   absoluteUnit,
-                   quantitativeUnitMap.get(Style.LONG),
-                   qualitativeUnitMap.get(Style.LONG));
-           addTimeUnit(
-                   r.getWithFallback(pathShort),
-                   relativeUnit,
-                   absoluteUnit,
-                   quantitativeUnitMap.get(Style.SHORT),
-                   qualitativeUnitMap.get(Style.SHORT));
-           addTimeUnit(
-                   r.getWithFallback(pathNarrow),
-                   relativeUnit,
-                   absoluteUnit,
-                   quantitativeUnitMap.get(Style.NARROW),
-                   qualitativeUnitMap.get(Style.NARROW));
-            
+        EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap =
+                new EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>>(Style.class);
+        EnumMap<Style, EnumMap<RelativeUnit, String[][]>> styleRelUnitPatterns =
+                new EnumMap<Style, EnumMap<RelativeUnit, String[][]>>(Style.class);
+
+        private ULocale ulocale = null;
+
+        StringBuilder sb = new StringBuilder();
+
+        public RelDateTimeFmtDataSink(ULocale locale) {
+            ulocale = locale;
         }
 
-        private void addTimeUnit(
-                ICUResourceBundle timeUnitBundle,
-                RelativeUnit relativeUnit,
-                AbsoluteUnit absoluteUnit,
-                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap,
-                EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap) {
-            addTimeUnit(timeUnitBundle, relativeUnit, quantitativeUnitMap);
-            String unitName = timeUnitBundle.getStringWithFallback("dn");
-            // TODO(Travis Keep): This is a hack to get around CLDR bug 6818.
-            if (ulocale.getLanguage().equals("en")) {
-                unitName = unitName.toLowerCase();
+        // 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;
             }
-            timeUnitBundle = timeUnitBundle.getWithFallback("relative");
-            addQualitativeUnit(
-                    qualitativeUnitMap,
-                    absoluteUnit,
-                    timeUnitBundle,
-                    unitName);
         }
-        
-        private void initRelativeUnits(
-                ICUResourceBundle r, 
-                String path,
-                String pathShort,
-                String pathNarrow,
-                RelativeUnit relativeUnit,
-                EnumMap<Style, EnumMap<RelativeUnit, QuantityFormatter[]>> quantitativeUnitMap) {
-            addTimeUnit(
-                    r.getWithFallback(path),
-                    relativeUnit,
-                    quantitativeUnitMap.get(Style.LONG));
-            addTimeUnit(
-                    r.getWithFallback(pathShort),
-                    relativeUnit,
-                    quantitativeUnitMap.get(Style.SHORT));
-            addTimeUnit(
-                    r.getWithFallback(pathNarrow),
-                    relativeUnit,
-                    quantitativeUnitMap.get(Style.NARROW));
+
+        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 void addTimeUnit(
-                ICUResourceBundle timeUnitBundle,
-                RelativeUnit relativeUnit,
-                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap) {
-            QuantityFormatter future = new QuantityFormatter();
-            QuantityFormatter past = new QuantityFormatter();
-            timeUnitBundle = timeUnitBundle.getWithFallback("relativeTime");
-            addTimeUnit(
-                    timeUnitBundle.getWithFallback("future"),
-                    future);
-            addTimeUnit(
-                    timeUnitBundle.getWithFallback("past"),
-                    past);
-            quantitativeUnitMap.put(
-                    relativeUnit, new QuantityFormatter[] { past, future });
+        private static int styleSuffixLength(Style style) {
+            switch (style) {
+            case SHORT: return 6;
+            case NARROW: return 7;
+            default: return 0;
+            }
+        }
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value) {
+            // Parse and store aliases.
+            if (value.getType() != ICUResourceBundle.ALIAS) { return; }
+
+            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);
+                }
+            } 
         }
 
-        private static void addTimeUnit(
-                ICUResourceBundle pastOrFuture, QuantityFormatter qf) {
-            int size = pastOrFuture.getSize();
-            for (int i = 0; i < size; i++) {
-                UResourceBundle r = pastOrFuture.get(i);
-                qf.addIfAbsent(r.getKey(), r.getString());
+        @Override
+        public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
+            // Get base unit and style from the key value.
+            style = styleFromKey(key);
+            int limit = key.length() - styleSuffixLength(style);
+            String unitString = key.substring(0, limit);
+
+            // Process only if unitString is in the white list.
+            unit = DateTimeUnit.orNullFromString(unitString);
+            if (unit == null) {
+                return null;
             }
+            return unitSink;  // Continue parsing this path.
         }
-        
-        private void addWeekDays(
-                ICUResourceBundle r,
-                String path,
-                String pathShort,
-                String pathNarrow,
-                EnumMap<Style, EnumMap<AbsoluteUnit, String>> dayOfWeekMap,
-                AbsoluteUnit weekDay,
-                EnumMap<Style, EnumMap<AbsoluteUnit, EnumMap<Direction, String>>> qualitativeUnitMap) {
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.LONG),
-                    weekDay,
-                    r.findWithFallback(path),
-                    dayOfWeekMap.get(Style.LONG).get(weekDay)); 
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.SHORT),
-                    weekDay,
-                    r.findWithFallback(pathShort),
-                    dayOfWeekMap.get(Style.SHORT).get(weekDay)); 
-            addQualitativeUnit(
-                    qualitativeUnitMap.get(Style.NARROW),
-                    weekDay,
-                    r.findWithFallback(pathNarrow),
-                    dayOfWeekMap.get(Style.NARROW).get(weekDay)); 
-            
+
+        // Sinks for additional levels under /fields/*/relative/ and /fields/*/relativeTime/
+        // Sets values under relativeTime paths, e.g., "hour/relativeTime/future/one"
+        class RelativeTimeDetailSink extends UResource.TableSink {
+            @Override
+            public void put(UResource.Key key, UResource.Value value) {
+                /* Make two lists of simplePatternFmtList, one for past and one for future.
+                 *  Set a SimplePatternFormatter for the <style, relative unit, plurality>
+                 *
+                 * Fill in values for the particular plural given, e.g., ONE, FEW, OTHER, etc.
+                 */
+                EnumMap<RelativeUnit, String[][]> unitPatterns  =
+                        styleRelUnitPatterns.get(style);
+                if (unitPatterns == null) {
+                    unitPatterns = new EnumMap<RelativeUnit, String[][]>(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);
+                }
+                int pluralIndex = StandardPlural.indexFromString(key.toString());
+                if (patterns[pastFutureIndex][pluralIndex] == null) {
+                    patterns[pastFutureIndex][pluralIndex] = 
+                            SimplePatternFormatter.compileToStringMinMaxPlaceholders(value.getString(),
+                                    sb, 0, 1); 
+                }
+            }
         }
+        RelativeTimeDetailSink relativeTimeDetailSink = new RelativeTimeDetailSink();
 
-        private static EnumMap<AbsoluteUnit, String> readDaysOfWeek(ICUResourceBundle daysOfWeekBundle) {
-            EnumMap<AbsoluteUnit, String> dayOfWeekMap = new EnumMap<AbsoluteUnit, String>(AbsoluteUnit.class);
-            if (daysOfWeekBundle.getSize() != 7) {
-                throw new IllegalStateException(String.format("Expect 7 days in a week, got %d", daysOfWeekBundle.getSize()));
+        // Handles "relativeTime" entries, e.g., under "day", "hour", "minute", "minute-short", etc.
+        class RelativeTimeSink extends UResource.TableSink {
+            @Override
+            public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
+                if (key.contentEquals("past")) {
+                    pastFutureIndex = 0;
+                } else if (key.contentEquals("future")) {
+                    pastFutureIndex = 1;
+                } else {
+                    return null;
+                }
+                if (unit.relUnit == null) {
+                    return null;
+                }
+                return relativeTimeDetailSink;
             }
-            // Sunday always comes first in CLDR data.
-            int idx = 0;
-            dayOfWeekMap.put(AbsoluteUnit.SUNDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.MONDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.TUESDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.WEDNESDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.THURSDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.FRIDAY, daysOfWeekBundle.getString(idx++));
-            dayOfWeekMap.put(AbsoluteUnit.SATURDAY, daysOfWeekBundle.getString(idx++));
-            return dayOfWeekMap;
+        }
+        RelativeTimeSink relativeTimeSink = new RelativeTimeSink();
+        // Handles "relative" entries, e.g., under "day", "day-short", "fri", "fri-narrow", "fri-short", etc.
+        class RelativeSink extends UResource.TableSink {
+            @Override
+            public void put(UResource.Key key, UResource.Value value) {
+
+                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, String>(Direction.class);
+                            absMap.put(AbsoluteUnit.NOW, unitStrings);
+                        }
+                        if (unitStrings.get(Direction.PLAIN) == null) {
+                            unitStrings.put(Direction.PLAIN, value.getString());
+                        }
+                        return;
+                    }
+                }
+                Direction keyDirection = keyToDirection(key);
+                if (keyDirection == null) {
+                    return;
+                }
+                AbsoluteUnit absUnit = unit.absUnit;
+                if (absUnit == null) {
+                    return;
+                }
+
+                if (absMap == null) {
+                    absMap = new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class);
+                    qualitativeUnitMap.put(style, absMap);
+                }
+                EnumMap<Direction, String> dirMap = absMap.get(absUnit);
+                if (dirMap == null) {
+                    dirMap = new EnumMap<Direction, String>(Direction.class);
+                    absMap.put(absUnit, dirMap);
+                }
+                if (dirMap.get(keyDirection) == null) {
+                    // Do not override values already entered.
+                    dirMap.put(keyDirection, value.getString());
+                }
+            }
+        }
+        RelativeSink relativeSink = new RelativeSink();
+
+        // Handles entries under units, recognizing "relative" and "relativeTime" entries.
+        class UnitSink extends UResource.TableSink {
+            @Override
+            public void put(UResource.Key key, UResource.Value value) {
+                if (key.contentEquals("dn")) {
+                    // Handle Display Name for PLAIN direction for some units.
+                    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, EnumMap<Direction, String>>(AbsoluteUnit.class);
+                        qualitativeUnitMap.put(style, unitMap);
+                    }
+                    EnumMap<Direction,String> dirMap = unitMap.get(absUnit);
+                    if (dirMap == null) {
+                        dirMap = new EnumMap<Direction,String>(Direction.class);
+                        unitMap.put(absUnit, dirMap);
+                    }
+                    if (dirMap.get(Direction.PLAIN) == null) {
+                        String displayName = value.toString();
+                        // TODO(Travis Keep): This is a hack to get around CLDR bug 6818.
+                        if (ulocale.getLanguage().equals("en")) {
+                            displayName = displayName.toLowerCase(Locale.ROOT);
+                        }
+                        dirMap.put(Direction.PLAIN, displayName);
+                    }
+                }
+            }
+
+            @Override
+            public UResource.TableSink getOrCreateTableSink(UResource.Key key, int initialSize) {
+                if (key.contentEquals("relative")) {
+                    return relativeSink;
+                } else if (key.contentEquals("relativeTime")) {
+                    return relativeTimeSink;
+                }
+                return null;
+            }
+        }
+        UnitSink unitSink = new UnitSink();
+    }
+
+    private static class Loader {
+        private final ULocale ulocale;
+
+        public Loader(ULocale ulocale) {
+            this.ulocale = ulocale;
+        }
+
+        public RelativeDateTimeFormatterData load() {
+            // Sink for traversing data.
+            RelDateTimeFmtDataSink sink = new RelDateTimeFmtDataSink(ulocale);
+            ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
+                    getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
+
+            // Use sink mechanism to traverse data structure.
+            r.getAllTableItemsWithFallback("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");
+                        }
+                    }
+                }
+            }
+
+            // TODO: Replace this use of CalendarData.
+            CalendarData calData = new CalendarData(
+                    ulocale, r.getStringWithFallback("calendar/default"));
+
+            return new RelativeDateTimeFormatterData(
+                    sink.qualitativeUnitMap, sink.styleRelUnitPatterns,
+                    calData.getDateTimePattern());
         }
     }
 
index 59e0a64dee5caa1a54e19b8f703cc1ead5bb36d0..1c5ccc127e4a57de98c9288b7582aa69aa9fcbb9 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *******************************************************************************
- * Copyright (C) 2013-2015, International Business Machines Corporation and    *
- * others. All Rights Reserved.                                                *
+ * Copyright (C) 2013-2016, International Business Machines Corporation and
+ * others. All Rights Reserved.
  *******************************************************************************
  */
 package com.ibm.icu.dev.test.format;
@@ -749,5 +749,24 @@ public class RelativeDateTimeFormatterTest extends TestFmwk {
         } catch (IllegalArgumentException expected) {
         }
     }
-    
+
+    public void TestSidewaysDataLoading() {
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(
+                new ULocale("en_GB"),
+                null,
+                RelativeDateTimeFormatter.Style.NARROW,
+                DisplayContext.CAPITALIZATION_NONE);
+        String s = fmt.format(3.0, Direction.NEXT, RelativeUnit.MONTHS);
+        assertEquals("narrow: in 3 months", "in 3 mo", s);
+        String t = fmt.format(1.0, Direction.LAST, RelativeUnit.QUARTERS);
+        assertEquals("narrow: 1 qtr ago", "1 qtr ago", t);
+        // Check for fallback to SHORT
+        String u = fmt.format(3.0, Direction.LAST, RelativeUnit.QUARTERS);
+        assertEquals("narrow: 3 qtr ago", "3 qtr ago", u);
+        // Do not expect fall back to SHORT
+        String v = fmt.format(1.0, Direction.LAST, RelativeUnit.QUARTERS);
+        assertEquals("narrow: 1 qtr ago", "1 qtr ago", v);
+        String w = fmt.format(6.0, Direction.NEXT, RelativeUnit.QUARTERS);
+        assertEquals("narrow: in 6 qtr", "in 6 qtr", w);
+    }
 }