From b90cbad09cdd4ff637f93f6b579054b69b60588f Mon Sep 17 00:00:00 2001 From: Travis Keep Date: Thu, 14 Nov 2013 19:50:19 +0000 Subject: [PATCH] ICU-8464 Add Relative Date Formatting for JAVA. X-SVN-Rev: 34663 --- .gitattributes | 3 + .../src/com/ibm/icu/impl/CalendarData.java | 48 +- .../com/ibm/icu/text/QuantityFormatter.java | 115 ++++ .../icu/text/RelativeDateTimeFormatter.java | 634 ++++++++++++++++++ .../format/RelativeDateTimeFormatterTest.java | 254 +++++++ .../com/ibm/icu/dev/test/format/TestAll.java | 3 +- 6 files changed, 1054 insertions(+), 3 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java diff --git a/.gitattributes b/.gitattributes index c2758f2669a..a99b97fce93 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java index 3e59dd9377e..031a206a14c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java @@ -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 {1}, {0}. + * {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 list = new ArrayList(); 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 index 00000000000..1e8ad7da075 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java @@ -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" + *

+ * 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 INDEX_MAP = new HashMap(); + 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 index 00000000000..740f7c68df3 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java @@ -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: + *

    + *
  • relative dates with a quantity e.g "in 5 days"
  • + *
  • relative dates without a quantity e.g "next Tuesday"
  • + *
+ *

+ * 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. + *

+ * Here are some examples of use: + *

+ *
+ * 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"
+ * 
+ *
+ *

+ * 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> qualitativeUnits, + AbsoluteUnit unit, + String current) { + EnumMap unitStrings = + new EnumMap(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> qualitativeUnits, + AbsoluteUnit unit, ICUResourceBundle bundle, String plain) { + EnumMap unitStrings = + new EnumMap(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 unitStrings, + Direction direction, + ICUResourceBundle bundle, + String key) { + bundle = bundle.findWithFallback(key); + if (bundle != null) { + unitStrings.put(direction, bundle.getString()); + } + } + + private RelativeDateTimeFormatter( + EnumMap> qualitativeUnitMap, + EnumMap 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> qualitativeUnitMap; + private final EnumMap quantitativeUnitMap; + private final MessageFormat combinedDateAndTime; + private final PluralRules pluralRules; + private NumberFormat numberFormat; + + private static class RelativeDateTimeFormatterData { + public RelativeDateTimeFormatterData( + EnumMap> qualitativeUnitMap, + EnumMap quantitativeUnitMap) { + this.qualitativeUnitMap = qualitativeUnitMap; + this.quantitativeUnitMap = quantitativeUnitMap; + } + + public final EnumMap> qualitativeUnitMap; + public final EnumMap quantitativeUnitMap; + } + + private static class Cache { + private final ICUCache cache = + new SimpleCache(); + + 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> qualitativeUnitMap = + new EnumMap>(AbsoluteUnit.class); + + EnumMap quantitativeUnitMap = + new EnumMap(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 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 quantitativeUnitMap, + EnumMap> 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 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 dayOfWeekMap, + AbsoluteUnit weekDay, + EnumMap> qualitativeUnitMap) { + weekdayBundle = weekdayBundle.findWithFallback("relative"); + addQualitativeUnit( + qualitativeUnitMap, + weekDay, + weekdayBundle, + dayOfWeekMap.get(weekDay)); + } + + private static EnumMap readDaysOfWeek(ICUResourceBundle daysOfWeekBundle) { + EnumMap dayOfWeekMap = new EnumMap(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 index 00000000000..aa449bd7ac4 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java @@ -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")); + } + +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java index ffea011ec56..dfe0326aa8b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java @@ -80,7 +80,8 @@ public class TestAll extends TestGroup { "DateTimeGeneratorTest", "IntlTestSimpleDateFormatAPI", "DateFormatRegressionTestJ", - "TimeZoneFormatTest" + "TimeZoneFormatTest", + "RelativeDateTimeFormatterTest" }); } }