/* ************************************************************************** * Copyright (C) 2008-2010, Google, International Business Machines * Corporationand others. All Rights Reserved. ************************************************************************** */ package com.ibm.icu.text; import java.text.FieldPosition; import java.text.ParsePosition; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import java.util.TreeMap; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.util.TimeUnit; import com.ibm.icu.util.TimeUnitAmount; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; /** * Format or parse a TimeUnitAmount, using plural rules for the units where available. * *

* Code Sample: *

 *   // create a time unit instance.
 *   // only SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, and YEAR are supported
 *   TimeUnit timeUnit = TimeUnit.SECOND;
 *   // create time unit amount instance - a combination of Number and time unit
 *   TimeUnitAmount source = new TimeUnitAmount(2, timeUnit);
 *   // create time unit format instance
 *   TimeUnitFormat format = new TimeUnitFormat();
 *   // set the locale of time unit format
 *   format.setLocale(new ULocale("en"));
 *   // format a time unit amount
 *   String formatted = format.format(source);
 *   System.out.println(formatted);
 *   try {
 *       // parse a string into time unit amount
 *       TimeUnitAmount result = (TimeUnitAmount) format.parseObject(formatted);
 *       // result should equal to source 
 *   } catch (ParseException e) {
 *   }
 * 
* *

* @see TimeUnitAmount * @see TimeUnitFormat * @author markdavis * @stable ICU 4.0 */ public class TimeUnitFormat extends MeasureFormat { /** * Constant for full name style format. * For example, the full name for "hour" in English is "hour" or "hours". * @stable ICU 4.2 */ public static final int FULL_NAME = 0; /** * Constant for abbreviated name style format. * For example, the abbreviated name for "hour" in English is "hr" or "hrs". * @stable ICU 4.2 */ public static final int ABBREVIATED_NAME = 1; private static final int TOTAL_STYLES = 2; private static final long serialVersionUID = -3707773153184971529L; private static final String DEFAULT_PATTERN_FOR_SECOND = "{0} s"; private static final String DEFAULT_PATTERN_FOR_MINUTE = "{0} min"; private static final String DEFAULT_PATTERN_FOR_HOUR = "{0} h"; private static final String DEFAULT_PATTERN_FOR_DAY = "{0} d"; private static final String DEFAULT_PATTERN_FOR_WEEK = "{0} w"; private static final String DEFAULT_PATTERN_FOR_MONTH = "{0} m"; private static final String DEFAULT_PATTERN_FOR_YEAR = "{0} y"; private NumberFormat format; private ULocale locale; private transient Map> timeUnitToCountToPatterns; private transient PluralRules pluralRules; private transient boolean isReady; private int style; /** * Create empty format using full name style, for example, "hours". * Use setLocale and/or setFormat to modify. * @stable ICU 4.0 */ public TimeUnitFormat() { isReady = false; style = FULL_NAME; } /** * Create TimeUnitFormat given a ULocale, and using full name style. * @param locale locale of this time unit formatter. * @stable ICU 4.0 */ public TimeUnitFormat(ULocale locale) { this(locale, FULL_NAME); } /** * Create TimeUnitFormat given a Locale, and using full name style. * @param locale locale of this time unit formatter. * @stable ICU 4.0 */ public TimeUnitFormat(Locale locale) { this(locale, FULL_NAME); } /** * Create TimeUnitFormat given a ULocale and a formatting style: full or * abbreviated. * @param locale locale of this time unit formatter. * @param style format style, either FULL_NAME or ABBREVIATED_NAME style. * @throws IllegalArgumentException if the style is not FULL_NAME or * ABBREVIATED_NAME style. * @stable ICU 4.2 */ public TimeUnitFormat(ULocale locale, int style) { if (style < FULL_NAME || style >= TOTAL_STYLES) { throw new IllegalArgumentException("style should be either FULL_NAME or ABBREVIATED_NAME style"); } this.style = style; this.locale = locale; isReady = false; } /** * Create TimeUnitFormat given a Locale and a formatting style: full or * abbreviated. * @stable ICU 4.2 */ public TimeUnitFormat(Locale locale, int style) { this(ULocale.forLocale(locale), style); } /** * Set the locale used for formatting or parsing. * @param locale locale of this time unit formatter. * @return this, for chaining. * @stable ICU 4.0 */ public TimeUnitFormat setLocale(ULocale locale) { if ( locale != this.locale ) { this.locale = locale; isReady = false; } return this; } /** * Set the locale used for formatting or parsing. * @param locale locale of this time unit formatter. * @return this, for chaining. * @stable ICU 4.0 */ public TimeUnitFormat setLocale(Locale locale) { return setLocale(ULocale.forLocale(locale)); } /** * Set the format used for formatting or parsing. If null or not available, use the getNumberInstance(locale). * @param format the number formatter. * @return this, for chaining. * @stable ICU 4.0 */ public TimeUnitFormat setNumberFormat(NumberFormat format) { if (format == this.format) { return this; } if ( format == null ) { if ( locale == null ) { isReady = false; return this; } else { this.format = NumberFormat.getNumberInstance(locale); } } else { this.format = format; } // reset the number formatter in the timeUnitToCountToPatterns map if (isReady == false) { return this; } for (TimeUnit timeUnit : timeUnitToCountToPatterns.keySet()) { Map countToPattern = timeUnitToCountToPatterns.get(timeUnit); for (String count : countToPattern.keySet()) { Object[] pair = countToPattern.get(count); MessageFormat pattern = (MessageFormat)pair[FULL_NAME]; pattern.setFormatByArgumentIndex(0, format); pattern = (MessageFormat)pair[ABBREVIATED_NAME]; pattern.setFormatByArgumentIndex(0, format); } } return this; } /** * Format a TimeUnitAmount. * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition) * @stable ICU 4.0 */ public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { if ( !(obj instanceof TimeUnitAmount) ) { throw new IllegalArgumentException("can not format non TimeUnitAmount object"); } if (!isReady) { setup(); } TimeUnitAmount amount = (TimeUnitAmount) obj; Map countToPattern = timeUnitToCountToPatterns.get(amount.getTimeUnit()); double number = amount.getNumber().doubleValue(); String count = pluralRules.select(number); MessageFormat pattern = (MessageFormat)(countToPattern.get(count))[style]; return pattern.format(new Object[]{amount.getNumber()}, toAppendTo, pos); } /** * Parse a TimeUnitAmount. * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition) * @stable ICU 4.0 */ public Object parseObject(String source, ParsePosition pos) { if (!isReady) { setup(); } Number resultNumber = null; TimeUnit resultTimeUnit = null; int oldPos = pos.getIndex(); int newPos = -1; int longestParseDistance = 0; String countOfLongestMatch = null; // we don't worry too much about speed on parsing, but this can be optimized later if needed. // Parse by iterating through all available patterns // and looking for the longest match. for (TimeUnit timeUnit : timeUnitToCountToPatterns.keySet()) { Map countToPattern = timeUnitToCountToPatterns.get(timeUnit); for (String count : countToPattern.keySet()) { for (int styl = FULL_NAME; styl < TOTAL_STYLES; ++styl) { MessageFormat pattern = (MessageFormat)(countToPattern.get(count))[styl]; pos.setErrorIndex(-1); pos.setIndex(oldPos); // see if we can parse Object parsed = pattern.parseObject(source, pos); if ( pos.getErrorIndex() != -1 || pos.getIndex() == oldPos ) { // nothing parsed continue; } Number temp = null; if ( ((Object[])parsed).length != 0 ) { // pattern with Number as beginning, // such as "{0} d". // check to make sure that the timeUnit is consistent temp = (Number)((Object[])parsed)[0]; String select = pluralRules.select(temp.doubleValue()); if (!count.equals(select)) { continue; } } int parseDistance = pos.getIndex() - oldPos; if ( parseDistance > longestParseDistance ) { resultNumber = temp; resultTimeUnit = timeUnit; newPos = pos.getIndex(); longestParseDistance = parseDistance; countOfLongestMatch = count; } } } } /* After find the longest match, parse the number. * Result number could be null for the pattern without number pattern. * such as unit pattern in Arabic. * When result number is null, use plural rule to set the number. */ if (resultNumber == null && longestParseDistance != 0) { // set the number using plurrual count if ( countOfLongestMatch.equals("zero") ) { resultNumber = new Integer(0); } else if ( countOfLongestMatch.equals("one") ) { resultNumber = new Integer(1); } else if ( countOfLongestMatch.equals("two") ) { resultNumber = new Integer(2); } else { // should not happen. // TODO: how to handle? resultNumber = new Integer(3); } } if (longestParseDistance == 0) { pos.setIndex(oldPos); pos.setErrorIndex(0); return null; } else { pos.setIndex(newPos); pos.setErrorIndex(-1); return new TimeUnitAmount(resultNumber, resultTimeUnit); } } /* * Initialize locale, number formatter, plural rules, and * time units patterns. * Initially, we are storing all of these as MessageFormats. * I think it might actually be simpler to make them Decimal Formats later. */ private void setup() { if (locale == null) { if (format != null) { locale = format.getLocale(null); } else { locale = ULocale.getDefault(); } } if (format == null) { format = NumberFormat.getNumberInstance(locale); } pluralRules = PluralRules.forLocale(locale); timeUnitToCountToPatterns = new HashMap>(); setup("units", timeUnitToCountToPatterns, FULL_NAME); setup("unitsShort", timeUnitToCountToPatterns, ABBREVIATED_NAME); isReady = true; } private void setup(String resourceKey, Map> timeUnitToCountToPatterns, int style) { // fill timeUnitToCountToPatterns from resource file try { ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale); ICUResourceBundle unitsRes = resource.getWithFallback(resourceKey); int size = unitsRes.getSize(); for ( int index = 0; index < size; ++index) { String timeUnitName = unitsRes.get(index).getKey(); TimeUnit timeUnit = null; if ( timeUnitName.equals("year") ) { timeUnit = TimeUnit.YEAR; } else if ( timeUnitName.equals("month") ) { timeUnit = TimeUnit.MONTH; } else if ( timeUnitName.equals("day") ) { timeUnit = TimeUnit.DAY; } else if ( timeUnitName.equals("hour") ) { timeUnit = TimeUnit.HOUR; } else if ( timeUnitName.equals("minute") ) { timeUnit = TimeUnit.MINUTE; } else if ( timeUnitName.equals("second") ) { timeUnit = TimeUnit.SECOND; } else if ( timeUnitName.equals("week") ) { timeUnit = TimeUnit.WEEK; } else { continue; } ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(timeUnitName); int count = oneUnitRes.getSize(); Map countToPatterns = timeUnitToCountToPatterns.get(timeUnit); if (countToPatterns == null) { countToPatterns = new TreeMap(); timeUnitToCountToPatterns.put(timeUnit, countToPatterns); } for ( int pluralIndex = 0; pluralIndex < count; ++pluralIndex) { String pluralCount = oneUnitRes.get(pluralIndex).getKey(); String pattern = oneUnitRes.get(pluralIndex).getString(); final MessageFormat messageFormat = new MessageFormat(pattern, locale); if (format != null) { messageFormat.setFormatByArgumentIndex(0, format); } // save both full name and abbreviated name in one table // is good space-wise, but it degrades performance, // since it needs to check whether the needed space // is already allocated or not. Object[] pair = countToPatterns.get(pluralCount); if (pair == null) { pair = new Object[2]; countToPatterns.put(pluralCount, pair); } pair[style] = messageFormat; } } } catch ( MissingResourceException e ) { } // there should be patterns for each plural rule in each time unit. // For each time unit, // for each plural rule, following is unit pattern fall-back rule: // ( for example: "one" hour ) // look for its unit pattern in its locale tree. // if pattern is not found in its own locale, such as de_DE, // look for the pattern in its parent, such as de, // keep looking till found or till root. // if the pattern is not found in root either, // fallback to plural count "other", // look for the pattern of "other" in the locale tree: // "de_DE" to "de" to "root". // If not found, fall back to value of // static variable DEFAULT_PATTERN_FOR_xxx, such as "{0} h". // // Following is consistency check to create pattern for each // plural rule in each time unit using above fall-back rule. // final TimeUnit[] timeUnits = TimeUnit.values(); Set keywords = pluralRules.getKeywords(); for ( int i = 0; i < timeUnits.length; ++i ) { // for each time unit, // get all the patterns for each plural rule in this locale. final TimeUnit timeUnit = timeUnits[i]; Map countToPatterns = timeUnitToCountToPatterns.get(timeUnit); if (countToPatterns == null) { countToPatterns = new TreeMap(); timeUnitToCountToPatterns.put(timeUnit, countToPatterns); } for (String pluralCount : keywords) { if ( countToPatterns.get(pluralCount) == null || countToPatterns.get(pluralCount)[style] == null ) { // look through parents searchInTree(resourceKey, style, timeUnit, pluralCount, pluralCount, countToPatterns); } } } } // srcPluralCount is the original plural count on which the pattern is // searched for. // searchPluralCount is the fallback plural count. // For example, to search for pattern for ""one" hour", // "one" is the srcPluralCount, // if the pattern is not found even in root, fallback to // using patterns of plural count "other", // then, "other" is the searchPluralCount. private void searchInTree(String resourceKey, int styl, TimeUnit timeUnit, String srcPluralCount, String searchPluralCount, Map countToPatterns) { ULocale parentLocale=locale; String srcTimeUnitName = timeUnit.toString(); while ( parentLocale != null ) { try { // look for pattern for srcPluralCount in locale tree ICUResourceBundle unitsRes = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, parentLocale); unitsRes = unitsRes.getWithFallback(resourceKey); ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(srcTimeUnitName); String pattern = oneUnitRes.getStringWithFallback(searchPluralCount); final MessageFormat messageFormat = new MessageFormat(pattern, locale); if (format != null) { messageFormat.setFormatByArgumentIndex(0, format); } Object[] pair = countToPatterns.get(srcPluralCount); if (pair == null) { pair = new Object[2]; countToPatterns.put(srcPluralCount, pair); } pair[styl] = messageFormat; return; } catch ( MissingResourceException e ) { } parentLocale=parentLocale.getFallback(); } // if not found the pattern for this plural count at all, // fall-back to plural count "other" if ( searchPluralCount.equals("other") ) { // set default fall back the same as the resource in root MessageFormat messageFormat = null; if ( timeUnit == TimeUnit.SECOND ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_SECOND, locale); } else if ( timeUnit == TimeUnit.MINUTE ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MINUTE, locale); } else if ( timeUnit == TimeUnit.HOUR ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_HOUR, locale); } else if ( timeUnit == TimeUnit.WEEK ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_WEEK, locale); } else if ( timeUnit == TimeUnit.DAY ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_DAY, locale); } else if ( timeUnit == TimeUnit.MONTH ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MONTH, locale); } else if ( timeUnit == TimeUnit.YEAR ) { messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_YEAR, locale); } if (format != null && messageFormat != null) { messageFormat.setFormatByArgumentIndex(0, format); } Object[] pair = countToPatterns.get(srcPluralCount); if (pair == null) { pair = new Object[2]; countToPatterns.put(srcPluralCount, pair); } pair[styl] = messageFormat; } else { // fall back to rule "other", and search in parents searchInTree(resourceKey, styl, timeUnit, srcPluralCount, "other", countToPatterns); } } }