From 0013251fcb0dd9af6ad0060dac0a7173f6ab1a55 Mon Sep 17 00:00:00 2001 From: Craig Cornelius Date: Fri, 15 Jan 2016 23:50:25 +0000 Subject: [PATCH] ICU-12018 Overhaul RelativeDateTimeFormatter data loading X-SVN-Rev: 38176 --- .../icu/text/RelativeDateTimeFormatter.java | 835 +++++++++++------- .../format/RelativeDateTimeFormatterTest.java | 25 +- 2 files changed, 525 insertions(+), 335 deletions(-) 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 index d928ca50a5b..6120b3cd764 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java @@ -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> unitMap; + EnumMap 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> qualitativeUnits, - AbsoluteUnit unit, - String current) { - EnumMap unitStrings = - new EnumMap(Direction.class); - 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) { - String s = bundle.findStringWithFallback(key); - if (s != null) { - unitStrings.put(direction, s); - } - } private RelativeDateTimeFormatter( EnumMap>> qualitativeUnitMap, - EnumMap> quantitativeUnitMap, + EnumMap> 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 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>> qualitativeUnitMap; - private final EnumMap> quantitativeUnitMap; + private final EnumMap> 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>> qualitativeUnitMap, - EnumMap> quantitativeUnitMap, + EnumMap> relUnitPatternMap, String dateTimePattern) { this.qualitativeUnitMap = qualitativeUnitMap; - this.quantitativeUnitMap = quantitativeUnitMap; + this.relUnitPatternMap = relUnitPatternMap; + this.dateTimePattern = dateTimePattern; } - + public final EnumMap>> qualitativeUnitMap; - public final EnumMap> quantitativeUnitMap; + EnumMap> relUnitPatternMap; public final String dateTimePattern; // Example: "{1}, {0}" } - + private static class Cache { private final ICUCache cache = new SimpleCache(); @@ -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>> qualitativeUnitMap = - new EnumMap>>(Style.class); - - EnumMap> quantitativeUnitMap = - new EnumMap>(Style.class); - - for (Style style : Style.values()) { - qualitativeUnitMap.put(style, new EnumMap>(AbsoluteUnit.class)); - quantitativeUnitMap.put(style, new EnumMap(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> dayOfWeekMap = - new EnumMap>(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> quantitativeUnitMap, - EnumMap>> 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>> qualitativeUnitMap = + new EnumMap>>(Style.class); + EnumMap> styleRelUnitPatterns = + new EnumMap>(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 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(); + // 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> 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 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> dayOfWeekMap, - AbsoluteUnit weekDay, - EnumMap>> 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 + * + * Fill in values for the particular plural given, e.g., ONE, FEW, OTHER, etc. + */ + EnumMap unitPatterns = + styleRelUnitPatterns.get(style); + if (unitPatterns == null) { + unitPatterns = new EnumMap(RelativeUnit.class); + styleRelUnitPatterns.put(style, unitPatterns); + } + String[][] patterns = unitPatterns.get(unit.relUnit); + if (patterns == null) { + patterns = new String[2][StandardPlural.COUNT]; + unitPatterns.put(unit.relUnit, patterns); + } + 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 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())); + // 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> absMap = qualitativeUnitMap.get(style); + + if (unit.relUnit == RelativeUnit.SECONDS) { + if (key.contentEquals("0")) { + // Handle Zero seconds for "now". + EnumMap unitStrings = absMap.get(AbsoluteUnit.NOW); + if (unitStrings == null) { + unitStrings = new EnumMap(Direction.class); + absMap.put(AbsoluteUnit.NOW, unitStrings); + } + if (unitStrings.get(Direction.PLAIN) == null) { + unitStrings.put(Direction.PLAIN, 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.class); + qualitativeUnitMap.put(style, absMap); + } + EnumMap dirMap = absMap.get(absUnit); + if (dirMap == null) { + dirMap = new EnumMap(Direction.class); + absMap.put(absUnit, dirMap); + } + if (dirMap.get(keyDirection) == null) { + // Do not override values already entered. + dirMap.put(keyDirection, value.getString()); + } + } + } + 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> unitMap = + qualitativeUnitMap.get(style); + if (unitMap == null) { + unitMap = new EnumMap>(AbsoluteUnit.class); + qualitativeUnitMap.put(style, unitMap); + } + EnumMap dirMap = unitMap.get(absUnit); + if (dirMap == null) { + dirMap = new EnumMap(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()); } } 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 index 59e0a64dee5..1c5ccc127e4 100644 --- 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 @@ -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); + } } -- 2.40.0