]> granicus.if.org Git - icu/commitdiff
ICU-8464 Add Relative Date Formatting for JAVA.
authorTravis Keep <keep94@gmail.com>
Thu, 14 Nov 2013 19:50:19 +0000 (19:50 +0000)
committerTravis Keep <keep94@gmail.com>
Thu, 14 Nov 2013 19:50:19 +0000 (19:50 +0000)
X-SVN-Rev: 34663

.gitattributes
icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java
icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java

index c2758f2669a611e0907bac7e8ebba3f3f364a0b8..a99b97fce9319440f991649c92d1f197a1097df2 100644 (file)
@@ -260,6 +260,8 @@ icu4j/main/classes/core/.settings/edu.umd.cs.findbugs.core.prefs -text
 icu4j/main/classes/core/.settings/org.eclipse.core.resources.prefs -text
 icu4j/main/classes/core/.settings/org.eclipse.jdt.core.prefs -text
 icu4j/main/classes/core/manifest.stub -text
+icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java -text
+icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java -text
 icu4j/main/classes/currdata/.externalToolBuilders/copy-data-currdata.launch -text
 icu4j/main/classes/currdata/.settings/org.eclipse.core.resources.prefs -text
 icu4j/main/classes/currdata/.settings/org.eclipse.jdt.core.prefs -text
@@ -320,6 +322,7 @@ icu4j/main/tests/core/manifest.stub -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/data/rbbi/english.dict -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/data/resources/testmessages.properties -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/data/thai6.ucs -text
+icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_3.6/com.ibm.icu.impl.OlsonTimeZone.dat -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_3.6/com.ibm.icu.impl.TimeZoneAdapter.dat -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_3.6/com.ibm.icu.math.BigDecimal.dat -text
index 3e59dd9377ef66c825b9a55b2872637917b41593..031a206a14c8fb9914f5825d07fd5b04d7114656 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 2004-2012, International Business Machines Corporation and    *
+ * Copyright (C) 2004-2013, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
@@ -9,6 +9,7 @@ package com.ibm.icu.impl;
 import java.util.ArrayList;
 import java.util.MissingResourceException;
 
+import com.ibm.icu.text.DateFormat;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 import com.ibm.icu.util.UResourceBundleIterator;
@@ -159,7 +160,50 @@ public class CalendarData {
 
         return list.toArray(new String[list.size()]);
     }
-
+    
+    /**
+     * Returns the default date-time pattern such as <code>{1}, {0}</code>.
+     * {1} is always the date and {0} is always the time.
+     */
+    public String getDateTimePattern() {
+        // this is a hack to get offset 8 from the dateTimePatterns array.
+        return _getDateTimePattern(-1);
+    }
+    
+    /**
+     * Returns the date-time pattern by style where style is one of the style fields defined
+     * in DateFormat. If date-time patterns by style are not available, it returns what
+     * {@link #getDateTimePattern()} would return.
+     * @param style the style e.g DateFormat.LONG.
+     * @return the pattern, e.g {1}, {0}.
+     */
+    public String getDateTimePattern(int style) {
+        // mask away high order bits such as the DateFormat.RELATIVE bit.
+        // We do it this way to avoid making this class depend on DateFormat. It makes this
+        // code more brittle, but it is no more brittle than how we access patterns by style.
+        return _getDateTimePattern(style & 7);
+    }
+    
+    private String _getDateTimePattern(int offset) {
+        String[] patterns = null;
+        try {
+            patterns = getDateTimePatterns();
+        } catch (MissingResourceException ignored) {
+            // ignore. patterns remains null.
+        }
+        if (patterns == null || patterns.length < 9) {
+            // Return hard-coded default. patterns array not available or it has too few
+            // elements.
+            return "{1} {0}";
+        }
+        if (patterns.length < 13) {
+            // Offset 8 contains default pattern if we don't have per style patterns.
+            return patterns[8];
+        }
+        // DateTimePatterns start at index 9 in the array.
+        return patterns[9 + offset];
+    }
+        
     public String[] getOverrides(){
         ICUResourceBundle bundle = get("DateTimePatterns");
         ArrayList<String> list = new ArrayList<String>();
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java
new file mode 100644 (file)
index 0000000..1e8ad7d
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2013, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.text;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * QuantityFormatter represents an unknown quantity of something and formats a known quantity
+ * in terms of that something. For example, a QuantityFormatter that represents X apples may
+ * format 1 as "1 apple" and 3 as "3 apples" 
+ * <p>
+ * QuanitityFormatter appears here instead of in com.ibm.icu.impl because it depends on
+ * PluralRules and DecimalFormat. It is package-protected as it is not meant for public use.
+ * @author rocketman
+ */
+class QuantityFormatter {
+    
+    private static final Map<String, Integer> INDEX_MAP = new HashMap<String, Integer>();
+    private static final int MAX_INDEX;
+    
+    static {
+        int idx = 0;
+        INDEX_MAP.put("other", idx++);
+        INDEX_MAP.put("zero", idx++);
+        INDEX_MAP.put("one", idx++);
+        INDEX_MAP.put("two", idx++);
+        INDEX_MAP.put("few", idx++);
+        INDEX_MAP.put("many", idx++);
+        
+        MAX_INDEX = idx;
+    }
+
+    /**
+     * Builder builds a QuantityFormatter.
+     * 
+     * @author rocketman
+     */
+    static class Builder {
+        
+        private String[] templates;
+
+        /**
+         * Adds a template.
+         * @param variant the plural variant, e.g "zero", "one", "two", "few", "many", "other"
+         * @param template the text for that plural variant with "{0}" as the quantity. For
+         * example, in English, the template for the "one" variant may be "{0} apple" while the
+         * template for the "other" variant may be "{0} apples"
+         * @return a reference to this Builder for chaining.
+         */
+        public Builder add(String variant, String template) {
+            ensureCapacity();
+            templates[INDEX_MAP.get(variant)] = template;
+            return this;
+        }
+
+        private void ensureCapacity() {
+            if (templates == null) {
+                templates = new String[MAX_INDEX];
+            }
+        }
+
+        /**
+         * Builds the new QuantityFormatter and resets this Builder to its initial state.
+         * @return the new QuantityFormatter object.
+         * @throws IllegalStateException if no template is specified for the "other" variant.
+         *   When throwing this exception, build() still resets this Builder to its initial
+         *   state.
+         */
+        public QuantityFormatter build() {
+            if (templates == null || templates[0] == null) {
+                templates = null;
+                throw new IllegalStateException("At least other variant must be set.");
+            }
+            QuantityFormatter result = new QuantityFormatter(templates);
+            templates = null;
+            return result;          
+        }
+
+    }
+
+    private final String[] templates;
+
+    private QuantityFormatter(String[] templates) {
+        this.templates = templates;
+    }
+
+    /**
+     * Format formats a quantity with this object.
+     * @param quantity the quantity to be formatted
+     * @param numberFormat used to actually format the quantity.
+     * @param pluralRules uses the quantity and the numberFormat to determine what plural
+     *  variant to use for fetching the formatting template.
+     * @return the formatted string e.g '3 apples'
+     */
+    public String format(double quantity, NumberFormat numberFormat, PluralRules pluralRules) {
+        String formatStr = numberFormat.format(quantity);
+        String variant;
+        if (numberFormat instanceof DecimalFormat) {
+            variant = pluralRules.select(((DecimalFormat) numberFormat).getFixedDecimal(quantity));            
+        } else {
+            variant = pluralRules.select(quantity);
+        }
+        return getByVariant(variant).replace("{0}", formatStr);
+    }
+
+    private String getByVariant(String variant) {
+        String template = templates[INDEX_MAP.get(variant)];
+        return template == null ? templates[0] : template;
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java
new file mode 100644 (file)
index 0000000..740f7c6
--- /dev/null
@@ -0,0 +1,634 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2013, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.text;
+
+import java.util.EnumMap;
+
+import com.ibm.icu.impl.CalendarData;
+import com.ibm.icu.impl.ICUCache;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.SimpleCache;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+
+/**
+ * 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>
+ *   <li>relative dates without a quantity e.g "next Tuesday"</li>
+ * </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 NOT 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>
+ * In the future, we may add more forms, such as abbreviated/short forms
+ * (3 secs ago), and relative day periods ("yesterday afternoon"), etc.
+ * 
+ * @draft ICU 53
+ * @provisional
+ */
+public final class RelativeDateTimeFormatter {
+    
+    /**
+     * Represents the unit for formatting a relative date. e.g "in 5 days"
+     * or "in 3 months"
+     * @draft ICU 53
+     * @provisional
+     */
+    public static enum RelativeUnit {
+        
+        /**
+         * Seconds
+         * @draft ICU 53
+         * @provisional
+         */
+        SECONDS,
+        
+        /**
+         * Minutes
+         * @draft ICU 53
+         * @provisional
+         */
+        MINUTES,
+        
+       /**
+        * Hours
+        * @draft ICU 53
+        * @provisional
+        */
+        HOURS,
+        
+        /**
+         * Days
+         * @draft ICU 53
+         * @provisional
+         */
+        DAYS,
+        
+        /**
+         * Weeks
+         * @draft ICU 53
+         * @provisional
+         */
+        WEEKS,
+        
+        /**
+         * Months
+         * @draft ICU 53
+         * @provisional
+         */
+        MONTHS,
+        
+        /**
+         * Years
+         * @draft ICU 53
+         * @provisional
+         */
+        YEARS,
+    }
+    
+    /**
+     * Represents an absolute unit.
+     * @draft ICU 53
+     * @provisional
+     */
+    public static enum AbsoluteUnit {
+        
+       /**
+        * Sunday
+        * @draft ICU 53
+        * @provisional
+        */
+        SUNDAY,
+        
+        /**
+         * Monday
+         * @draft ICU 53
+         * @provisional
+         */
+        MONDAY,
+        
+        /**
+         * Tuesday
+         * @draft ICU 53
+         * @provisional
+         */
+        TUESDAY,
+        
+        /**
+         * Wednesday
+         * @draft ICU 53
+         * @provisional
+         */
+        WEDNESDAY,
+        
+        /**
+         * Thursday
+         * @draft ICU 53
+         * @provisional
+         */
+        THURSDAY,
+        
+        /**
+         * Friday
+         * @draft ICU 53
+         * @provisional
+         */
+        FRIDAY,
+        
+        /**
+         * Saturday
+         * @draft ICU 53
+         * @provisional
+         */
+        SATURDAY,
+        
+        /**
+         * Day
+         * @draft ICU 53
+         * @provisional
+         */
+        DAY,
+        
+        /**
+         * Week
+         * @draft ICU 53
+         * @provisional
+         */
+        WEEK,
+        
+        /**
+         * Month
+         * @draft ICU 53
+         * @provisional
+         */
+        MONTH,
+        
+        /**
+         * Year
+         * @draft ICU 53
+         * @provisional
+         */
+        YEAR,
+        
+        /**
+         * Now
+         * @draft ICU 53
+         * @provisional
+         */
+        NOW,
+      }
+
+      /**
+       * Represents a direction for an absolute unit e.g "Next Tuesday"
+       * or "Last Tuesday"
+       * @draft ICU 53
+       * @provisional
+       */
+      public static enum Direction {
+          
+          /**
+           * Two before. Not fully supported in every locale
+           * @draft ICU 53
+           * @provisional
+           */
+          LAST_2,
+
+          
+          /**
+           * Last
+           * @draft ICU 53
+           * @provisional
+           */  
+        LAST,
+        
+        /**
+         * This
+         * @draft ICU 53
+         * @provisional
+         */
+        THIS,
+        
+        /**
+         * Next
+         * @draft ICU 53
+         * @provisional
+         */
+        NEXT,
+        
+        /**
+         * Two after. Not fully supported in every locale
+         * @draft ICU 53
+         * @provisional
+         */
+        NEXT_2,
+        
+        /**
+         * Plain, which means the absence of a qualifier
+         * @draft ICU 53
+         * @provisional
+         */
+        PLAIN;
+      }
+    
+    /**
+     * Returns a RelativeDateTimeFormatter for the default locale.
+     * @draft ICU 53
+     * @provisional
+     */
+    public static RelativeDateTimeFormatter getInstance() {
+        return getInstance(ULocale.getDefault());
+    }
+    
+    /**
+     * Returns a RelativeDateTimeFormatter for a particular locale.
+     * @draft ICU 53
+     * @provisional
+     */
+    public static RelativeDateTimeFormatter getInstance(ULocale locale) {
+        CalendarData calData = new CalendarData(locale, null);
+        RelativeDateTimeFormatterData data = cache.get(locale);
+        return new RelativeDateTimeFormatter(
+                data.qualitativeUnitMap,
+                data.quantitativeUnitMap,
+                new MessageFormat(calData.getDateTimePattern()),
+                PluralRules.forLocale(locale),
+                NumberFormat.getInstance(locale));
+    }
+    
+           
+    /**
+     * Formats a relative date with a quantity such as "in 5 days" or
+     * "3 months ago"
+     * @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.
+     * @draft ICU 53
+     * @provisional
+     */
+    public String format(double quantity, Direction direction, RelativeUnit unit) {
+        if (direction != Direction.LAST && direction != Direction.NEXT) {
+            throw new IllegalArgumentException("direction must be NEXT or LAST");
+        }
+        return getQuantity(unit, direction == Direction.NEXT).format(quantity, numberFormat, pluralRules);
+    }
+    
+    /**
+     * Formats a relative date without a quantity.
+     * @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.
+     * @draft ICU 53
+     * @provisional
+     */
+    public String format(Direction direction, AbsoluteUnit unit) {
+        if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) {
+            throw new IllegalArgumentException("NOW can only accept direction PLAIN.");
+        }
+        return this.qualitativeUnitMap.get(unit).get(direction);
+    }
+    
+    /**
+     * Specify which NumberFormat object this object should use for
+     * formatting numbers. By default this object uses the default
+     * NumberFormat object for this object's locale.
+     * @param nf the NumberFormat object to use. This method makes
+     *  its own defensive copy of nf so that subsequent
+     *  changes to nf will not affect the operation of this object.
+     * @see #format(double, Direction, RelativeUnit)
+     * @draft ICU 53
+     * @provisional
+     */
+    public void setNumberFormat(NumberFormat nf) {
+        this.numberFormat = (NumberFormat) nf.clone();
+    }
+
+    /**
+     * 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'
+     * @draft ICU 53
+     * @provisional
+     */
+    public String combineDateAndTime(String relativeDateString, String timeString) {
+        return this.combinedDateAndTime.format(
+            new Object[]{timeString, relativeDateString}, new StringBuffer(), null).toString();
+    }
+    
+    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.LAST, current);
+        unitStrings.put(Direction.THIS, current);
+        unitStrings.put(Direction.NEXT, current);
+        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) {
+        bundle = bundle.findWithFallback(key);
+        if (bundle != null) {
+            unitStrings.put(direction, bundle.getString());
+        }
+    }
+
+    private RelativeDateTimeFormatter(
+            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap,
+            EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap,
+            MessageFormat combinedDateAndTime,
+            PluralRules pluralRules,
+            NumberFormat numberFormat) {
+        this.qualitativeUnitMap = qualitativeUnitMap;
+        this.quantitativeUnitMap = quantitativeUnitMap;
+        this.combinedDateAndTime = combinedDateAndTime;
+        this.pluralRules = pluralRules;
+        this.numberFormat = numberFormat;
+    }
+    
+    private QuantityFormatter getQuantity(RelativeUnit unit, boolean isFuture) {
+        QuantityFormatter[] quantities = quantitativeUnitMap.get(unit);
+        return isFuture ? quantities[1] : quantities[0];
+    }
+    
+    private final EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap;
+    private final EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap;
+    private final MessageFormat combinedDateAndTime;
+    private final PluralRules pluralRules;
+    private NumberFormat numberFormat;
+    
+    private static class RelativeDateTimeFormatterData {
+        public RelativeDateTimeFormatterData(
+                EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap,
+                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap) {
+            this.qualitativeUnitMap = qualitativeUnitMap;
+            this.quantitativeUnitMap = quantitativeUnitMap;
+        }
+        
+        public final EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap;
+        public final EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap;
+    }
+    
+    private static class Cache {
+        private final ICUCache<String, RelativeDateTimeFormatterData> cache =
+            new SimpleCache<String, RelativeDateTimeFormatterData>();
+
+        public RelativeDateTimeFormatterData get(ULocale locale) {
+            String key = locale.toString();
+            RelativeDateTimeFormatterData result = cache.get(key);
+            if (result == null) {
+                result = new Loader(locale).load();
+                cache.put(key, result);
+            }
+            return result;
+        }
+    }
+    
+    private static class Loader {
+        private final ULocale ulocale;
+        
+        public Loader(ULocale ulocale) {
+            this.ulocale = ulocale;
+        }
+
+        public RelativeDateTimeFormatterData load() {
+            EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap = 
+                    new EnumMap<AbsoluteUnit, EnumMap<Direction, String>>(AbsoluteUnit.class);
+            
+            EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap =
+                    new EnumMap<RelativeUnit, QuantityFormatter[]>(RelativeUnit.class);
+                    
+            ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
+                    getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
+            addTimeUnit(
+                    r.getWithFallback("fields/day"),
+                    RelativeUnit.DAYS,
+                    AbsoluteUnit.DAY,
+                    quantitativeUnitMap,
+                    qualitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/week"),
+                    RelativeUnit.WEEKS,
+                    AbsoluteUnit.WEEK,
+                    quantitativeUnitMap,
+                    qualitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/month"),
+                    RelativeUnit.MONTHS,
+                    AbsoluteUnit.MONTH,
+                    quantitativeUnitMap,
+                    qualitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/year"),
+                    RelativeUnit.YEARS,
+                    AbsoluteUnit.YEAR,
+                    quantitativeUnitMap,
+                    qualitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/second"),
+                    RelativeUnit.SECONDS,
+                    quantitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/minute"),
+                    RelativeUnit.MINUTES,
+                    quantitativeUnitMap);
+            addTimeUnit(
+                    r.getWithFallback("fields/hour"),
+                    RelativeUnit.HOURS,
+                    quantitativeUnitMap);
+            addQualitativeUnit(
+                    qualitativeUnitMap,
+                    AbsoluteUnit.NOW,
+                    r.getStringWithFallback("fields/second/relative/0"));
+            
+            EnumMap<AbsoluteUnit, String> dayOfWeekMap = readDaysOfWeek(
+                r.getWithFallback("calendar/gregorian/dayNames/stand-alone/wide")
+            );
+            
+            addWeekDay(
+                    r.getWithFallback("fields/mon"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.MONDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/tue"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.TUESDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/wed"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.WEDNESDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/thu"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.THURSDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/fri"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.FRIDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/sat"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.SATURDAY,
+                    qualitativeUnitMap);
+            addWeekDay(
+                    r.getWithFallback("fields/sun"),
+                    dayOfWeekMap,
+                    AbsoluteUnit.SUNDAY,
+                    qualitativeUnitMap);          
+            return new RelativeDateTimeFormatterData(qualitativeUnitMap, quantitativeUnitMap);
+        }
+
+        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();
+            }
+            timeUnitBundle = timeUnitBundle.getWithFallback("relative");
+            addQualitativeUnit(
+                    qualitativeUnitMap,
+                    absoluteUnit,
+                    timeUnitBundle,
+                    unitName);
+        }
+
+        private static void addTimeUnit(
+                ICUResourceBundle timeUnitBundle,
+                RelativeUnit relativeUnit,
+                EnumMap<RelativeUnit, QuantityFormatter[]> quantitativeUnitMap) {
+            QuantityFormatter.Builder future = new QuantityFormatter.Builder();
+            QuantityFormatter.Builder past = new QuantityFormatter.Builder();
+            timeUnitBundle = timeUnitBundle.getWithFallback("relativeTime");
+            addTimeUnit(
+                    timeUnitBundle.getWithFallback("future"),
+                    future);
+            addTimeUnit(
+                    timeUnitBundle.getWithFallback("past"),
+                    past);
+            quantitativeUnitMap.put(
+                    relativeUnit, new QuantityFormatter[] { past.build(), future.build() });
+        }
+
+        private static void addTimeUnit(
+                ICUResourceBundle pastOrFuture, QuantityFormatter.Builder builder) {
+            int size = pastOrFuture.getSize();
+            for (int i = 0; i < size; i++) {
+                UResourceBundle r = pastOrFuture.get(i);
+                builder.add(r.getKey(), r.getString());
+            }
+        }
+
+        private static void addWeekDay(
+                ICUResourceBundle weekdayBundle,
+                EnumMap<AbsoluteUnit, String> dayOfWeekMap,
+                AbsoluteUnit weekDay,
+                EnumMap<AbsoluteUnit, EnumMap<Direction, String>> qualitativeUnitMap) {
+            weekdayBundle = weekdayBundle.findWithFallback("relative");
+            addQualitativeUnit(
+                    qualitativeUnitMap,
+                    weekDay,
+                    weekdayBundle,
+                    dayOfWeekMap.get(weekDay));
+        }
+
+        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()));
+            }
+            // Assuming that days of week are always listed from Sunday.
+            // TODO(tkeep): Is this always true?
+            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;
+        }
+    }
+    
+
+    private static final Cache cache = new Cache();
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java
new file mode 100644 (file)
index 0000000..aa449bd
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2013, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.format;
+
+
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.RelativeDateTimeFormatter;
+import com.ibm.icu.text.RelativeDateTimeFormatter.AbsoluteUnit;
+import com.ibm.icu.text.RelativeDateTimeFormatter.Direction;
+import com.ibm.icu.text.RelativeDateTimeFormatter.RelativeUnit;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * @author rocketman
+ *
+ */
+public class RelativeDateTimeFormatterTest extends TestFmwk {
+    
+    public static void main(String[] args) throws Exception {
+        new RelativeDateTimeFormatterTest().run(args);
+    }
+    
+    public void TestRelativeDateWithQuantity() {
+        Object[][] data = {
+                {0.0, Direction.NEXT, RelativeUnit.SECONDS, "in 0 seconds"},
+                {0.5, Direction.NEXT, RelativeUnit.SECONDS, "in 0.5 seconds"},
+                {1.0, Direction.NEXT, RelativeUnit.SECONDS, "in 1 second"},
+                {2.0, Direction.NEXT, RelativeUnit.SECONDS, "in 2 seconds"},
+                {0.0, Direction.NEXT, RelativeUnit.MINUTES, "in 0 minutes"},
+                {0.5, Direction.NEXT, RelativeUnit.MINUTES, "in 0.5 minutes"},
+                {1.0, Direction.NEXT, RelativeUnit.MINUTES, "in 1 minute"},
+                {2.0, Direction.NEXT, RelativeUnit.MINUTES, "in 2 minutes"},
+                {0.0, Direction.NEXT, RelativeUnit.HOURS, "in 0 hours"},
+                {0.5, Direction.NEXT, RelativeUnit.HOURS, "in 0.5 hours"},
+                {1.0, Direction.NEXT, RelativeUnit.HOURS, "in 1 hour"},
+                {2.0, Direction.NEXT, RelativeUnit.HOURS, "in 2 hours"},
+                {0.0, Direction.NEXT, RelativeUnit.DAYS, "in 0 days"},
+                {0.5, Direction.NEXT, RelativeUnit.DAYS, "in 0.5 days"},
+                {1.0, Direction.NEXT, RelativeUnit.DAYS, "in 1 day"},
+                {2.0, Direction.NEXT, RelativeUnit.DAYS, "in 2 days"},
+                {0.0, Direction.NEXT, RelativeUnit.WEEKS, "in 0 weeks"},
+                {0.5, Direction.NEXT, RelativeUnit.WEEKS, "in 0.5 weeks"},
+                {1.0, Direction.NEXT, RelativeUnit.WEEKS, "in 1 week"},
+                {2.0, Direction.NEXT, RelativeUnit.WEEKS, "in 2 weeks"},
+                {0.0, Direction.NEXT, RelativeUnit.MONTHS, "in 0 months"},
+                {0.5, Direction.NEXT, RelativeUnit.MONTHS, "in 0.5 months"},
+                {1.0, Direction.NEXT, RelativeUnit.MONTHS, "in 1 month"},
+                {2.0, Direction.NEXT, RelativeUnit.MONTHS, "in 2 months"},
+                {0.0, Direction.NEXT, RelativeUnit.YEARS, "in 0 years"},
+                {0.5, Direction.NEXT, RelativeUnit.YEARS, "in 0.5 years"},
+                {1.0, Direction.NEXT, RelativeUnit.YEARS, "in 1 year"},
+                {2.0, Direction.NEXT, RelativeUnit.YEARS, "in 2 years"},
+                
+                {0.0, Direction.LAST, RelativeUnit.SECONDS, "0 seconds ago"},
+                {0.5, Direction.LAST, RelativeUnit.SECONDS, "0.5 seconds ago"},
+                {1.0, Direction.LAST, RelativeUnit.SECONDS, "1 second ago"},
+                {2.0, Direction.LAST, RelativeUnit.SECONDS, "2 seconds ago"},
+                {0.0, Direction.LAST, RelativeUnit.MINUTES, "0 minutes ago"},
+                {0.5, Direction.LAST, RelativeUnit.MINUTES, "0.5 minutes ago"},
+                {1.0, Direction.LAST, RelativeUnit.MINUTES, "1 minute ago"},
+                {2.0, Direction.LAST, RelativeUnit.MINUTES, "2 minutes ago"},
+                {0.0, Direction.LAST, RelativeUnit.HOURS, "0 hours ago"},
+                {0.5, Direction.LAST, RelativeUnit.HOURS, "0.5 hours ago"},
+                {1.0, Direction.LAST, RelativeUnit.HOURS, "1 hour ago"},
+                {2.0, Direction.LAST, RelativeUnit.HOURS, "2 hours ago"},
+                {0.0, Direction.LAST, RelativeUnit.DAYS, "0 days ago"},
+                {0.5, Direction.LAST, RelativeUnit.DAYS, "0.5 days ago"},
+                {1.0, Direction.LAST, RelativeUnit.DAYS, "1 day ago"},
+                {2.0, Direction.LAST, RelativeUnit.DAYS, "2 days ago"},
+                {0.0, Direction.LAST, RelativeUnit.WEEKS, "0 weeks ago"},
+                {0.5, Direction.LAST, RelativeUnit.WEEKS, "0.5 weeks ago"},
+                {1.0, Direction.LAST, RelativeUnit.WEEKS, "1 week ago"},
+                {2.0, Direction.LAST, RelativeUnit.WEEKS, "2 weeks ago"},
+                {0.0, Direction.LAST, RelativeUnit.MONTHS, "0 months ago"},
+                {0.5, Direction.LAST, RelativeUnit.MONTHS, "0.5 months ago"},
+                {1.0, Direction.LAST, RelativeUnit.MONTHS, "1 month ago"},
+                {2.0, Direction.LAST, RelativeUnit.MONTHS, "2 months ago"},
+                {0.0, Direction.LAST, RelativeUnit.YEARS, "0 years ago"},
+                {0.5, Direction.LAST, RelativeUnit.YEARS, "0.5 years ago"},
+                {1.0, Direction.LAST, RelativeUnit.YEARS, "1 year ago"},
+                {2.0, Direction.LAST, RelativeUnit.YEARS, "2 years ago"},      
+        };
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("en_US"));
+        for (Object[] row : data) {
+            String actual = fmt.format(
+                    ((Double) row[0]).doubleValue(), (Direction) row[1], (RelativeUnit) row[2]);
+            assertEquals("Relative date with quantity", row[3], actual);
+        }
+    }
+    
+    public void TestRelativeDateWithQuantitySr() {
+        Object[][] data = {
+                {0.0, Direction.NEXT, RelativeUnit.MONTHS, "за 0 месеци"},
+                {1.2, Direction.NEXT, RelativeUnit.MONTHS, "за 1,2 месеца"},
+                {21.0, Direction.NEXT, RelativeUnit.MONTHS, "за 21 месец"},      
+        };
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("sr"));
+        for (Object[] row : data) {
+            String actual = fmt.format(
+                    ((Double) row[0]).doubleValue(), (Direction) row[1], (RelativeUnit) row[2]);
+            assertEquals("Relative date with quantity", row[3], actual);
+        }
+    }
+    
+    public void TestRelativeDateWithoutQuantity() {
+        Object[][] data = {
+                {Direction.NEXT_2, AbsoluteUnit.DAY, null},
+                
+                {Direction.NEXT, AbsoluteUnit.DAY, "tomorrow"},
+                {Direction.NEXT, AbsoluteUnit.WEEK, "next week"},
+                {Direction.NEXT, AbsoluteUnit.MONTH, "next month"},
+                {Direction.NEXT, AbsoluteUnit.YEAR, "next year"},
+                {Direction.NEXT, AbsoluteUnit.MONDAY, "next Monday"},
+                {Direction.NEXT, AbsoluteUnit.TUESDAY, "next Tuesday"},
+                {Direction.NEXT, AbsoluteUnit.WEDNESDAY, "next Wednesday"},
+                {Direction.NEXT, AbsoluteUnit.THURSDAY, "next Thursday"},
+                {Direction.NEXT, AbsoluteUnit.FRIDAY, "next Friday"},
+                {Direction.NEXT, AbsoluteUnit.SATURDAY, "next Saturday"},
+                {Direction.NEXT, AbsoluteUnit.SUNDAY, "next Sunday"},
+                
+                {Direction.LAST_2, AbsoluteUnit.DAY, null},
+                
+                {Direction.LAST, AbsoluteUnit.DAY, "yesterday"},
+                {Direction.LAST, AbsoluteUnit.WEEK, "last week"},
+                {Direction.LAST, AbsoluteUnit.MONTH, "last month"},
+                {Direction.LAST, AbsoluteUnit.YEAR, "last year"},
+                {Direction.LAST, AbsoluteUnit.MONDAY, "last Monday"},
+                {Direction.LAST, AbsoluteUnit.TUESDAY, "last Tuesday"},
+                {Direction.LAST, AbsoluteUnit.WEDNESDAY, "last Wednesday"},
+                {Direction.LAST, AbsoluteUnit.THURSDAY, "last Thursday"},
+                {Direction.LAST, AbsoluteUnit.FRIDAY, "last Friday"},
+                {Direction.LAST, AbsoluteUnit.SATURDAY, "last Saturday"},
+                {Direction.LAST, AbsoluteUnit.SUNDAY, "last Sunday"},
+                 
+                {Direction.THIS, AbsoluteUnit.DAY, "today"},
+                {Direction.THIS, AbsoluteUnit.WEEK, "this week"},
+                {Direction.THIS, AbsoluteUnit.MONTH, "this month"},
+                {Direction.THIS, AbsoluteUnit.YEAR, "this year"},
+                {Direction.THIS, AbsoluteUnit.MONDAY, "this Monday"},
+                {Direction.THIS, AbsoluteUnit.TUESDAY, "this Tuesday"},
+                {Direction.THIS, AbsoluteUnit.WEDNESDAY, "this Wednesday"},
+                {Direction.THIS, AbsoluteUnit.THURSDAY, "this Thursday"},
+                {Direction.THIS, AbsoluteUnit.FRIDAY, "this Friday"},
+                {Direction.THIS, AbsoluteUnit.SATURDAY, "this Saturday"},
+                {Direction.THIS, AbsoluteUnit.SUNDAY, "this Sunday"},
+                
+                {Direction.PLAIN, AbsoluteUnit.DAY, "day"},
+                {Direction.PLAIN, AbsoluteUnit.WEEK, "week"},
+                {Direction.PLAIN, AbsoluteUnit.MONTH, "month"},
+                {Direction.PLAIN, AbsoluteUnit.YEAR, "year"},
+                {Direction.PLAIN, AbsoluteUnit.MONDAY, "Monday"},
+                {Direction.PLAIN, AbsoluteUnit.TUESDAY, "Tuesday"},
+                {Direction.PLAIN, AbsoluteUnit.WEDNESDAY, "Wednesday"},
+                {Direction.PLAIN, AbsoluteUnit.THURSDAY, "Thursday"},
+                {Direction.PLAIN, AbsoluteUnit.FRIDAY, "Friday"},
+                {Direction.PLAIN, AbsoluteUnit.SATURDAY, "Saturday"},
+                {Direction.PLAIN, AbsoluteUnit.SUNDAY, "Sunday"},
+                
+                {Direction.PLAIN, AbsoluteUnit.NOW, "now"},
+        };
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("en_US"));
+        for (Object[] row : data) {
+            String actual = fmt.format((Direction) row[0], (AbsoluteUnit) row[1]);
+            assertEquals("Relative date without quantity", row[2], actual);
+        }
+    }
+    
+    public void TestTwoBeforeTwoAfter() {
+        Object[][] data = {
+                {Direction.NEXT_2, AbsoluteUnit.DAY, "pasado ma\u00F1ana"},
+                {Direction.LAST_2, AbsoluteUnit.DAY, "antes de ayer"},
+        };
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("es"));
+        for (Object[] row : data) {
+            String actual = fmt.format((Direction) row[0], (AbsoluteUnit) row[1]);
+            assertEquals("Two before two after", row[2], actual);
+        }
+    }
+    
+    public void TestFormatWithQuantityIllegalArgument() {
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("en_US"));
+        try {
+            fmt.format(1.0, Direction.PLAIN, RelativeUnit.DAYS);
+            fail("Expected IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            fmt.format(1.0, Direction.THIS, RelativeUnit.DAYS);
+            fail("Expected IllegalArgumentException."); 
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    public void TestFormatWithoutQuantityIllegalArgument() {
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("en_US"));
+        try {
+            fmt.format(Direction.LAST, AbsoluteUnit.NOW);
+            fail("Expected IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            fmt.format(Direction.NEXT, AbsoluteUnit.NOW);
+            fail("Expected IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            fmt.format(Direction.THIS, AbsoluteUnit.NOW);
+            fail("Expected IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    public void TestSetNumberFormat() {
+        ULocale loc = new ULocale("en_US");
+        NumberFormat nf = NumberFormat.getInstance(loc);
+        nf.setMinimumFractionDigits(1);
+        nf.setMaximumFractionDigits(1);
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(loc);
+        fmt.setNumberFormat(nf);
+        
+        // Change nf after the fact to prove that we made a defensive copy
+        nf.setMinimumFractionDigits(3);
+        nf.setMaximumFractionDigits(3);
+        
+        Object[][] data = {
+            {0.0, Direction.NEXT, RelativeUnit.SECONDS, "in 0.0 seconds"},
+            {0.5, Direction.NEXT, RelativeUnit.SECONDS, "in 0.5 seconds"},
+            {1.0, Direction.NEXT, RelativeUnit.SECONDS, "in 1.0 seconds"},
+            {2.0, Direction.NEXT, RelativeUnit.SECONDS, "in 2.0 seconds"},
+        };
+        for (Object[] row : data) {
+            String actual = fmt.format(
+                    ((Double) row[0]).doubleValue(), (Direction) row[1], (RelativeUnit) row[2]);
+            assertEquals("Relative date with quantity special NumberFormat", row[3], actual);
+        }
+    }
+    
+    public void TestCombineDateAndTime() {
+        RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(new ULocale("en_US"));
+        assertEquals("TestcombineDateAndTime", "yesterday, 3:50", fmt.combineDateAndTime("yesterday", "3:50"));
+    }
+    
+}
index ffea011ec565bdc4fdd5bf3c2a675c6feff341fb..dfe0326aa8b25d20d36a2744315c1f27fc3c2f89 100644 (file)
@@ -80,7 +80,8 @@ public class TestAll extends TestGroup {
                 "DateTimeGeneratorTest",
                 "IntlTestSimpleDateFormatAPI",
                 "DateFormatRegressionTestJ",
-                "TimeZoneFormatTest"
+                "TimeZoneFormatTest",
+                "RelativeDateTimeFormatterTest"
             });
         }
     }