| /* |
| ************************************************************************** |
| * Copyright (C) 2008-2012, Google, International Business Machines |
| * Corporation and 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.Map.Entry; |
| 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.ULocale.Category; |
| import com.ibm.icu.util.UResourceBundle; |
| |
| |
| /** |
| * Format or parse a TimeUnitAmount, using plural rules for the units where available. |
| * |
| * <P> |
| * Code Sample: |
| * <pre> |
| * // 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) { |
| * } |
| * </pre> |
| * |
| * <P> |
| * @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<TimeUnit, Map<String, Object[]>> 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 (Map<String, Object[]> countToPattern : timeUnitToCountToPatterns.values()) { |
| for (Object[] pair : countToPattern.values()) { |
| 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<String, Object[]> 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<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(timeUnit); |
| for (Entry<String, Object[]> patternEntry : countToPattern.entrySet()) { |
| String count = patternEntry.getKey(); |
| for (int styl = FULL_NAME; styl < TOTAL_STYLES; ++styl) { |
| MessageFormat pattern = (MessageFormat)(patternEntry.getValue())[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 = Integer.valueOf(0); |
| } else if ( countOfLongestMatch.equals("one") ) { |
| resultNumber = Integer.valueOf(1); |
| } else if ( countOfLongestMatch.equals("two") ) { |
| resultNumber = Integer.valueOf(2); |
| } else { |
| // should not happen. |
| // TODO: how to handle? |
| resultNumber = Integer.valueOf(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(Category.FORMAT); |
| } |
| } |
| if (format == null) { |
| format = NumberFormat.getNumberInstance(locale); |
| } |
| pluralRules = PluralRules.forLocale(locale); |
| timeUnitToCountToPatterns = new HashMap<TimeUnit, Map<String, Object[]>>(); |
| |
| Set<String> pluralKeywords = pluralRules.getKeywords(); |
| setup("units", timeUnitToCountToPatterns, FULL_NAME, pluralKeywords); |
| setup("unitsShort", timeUnitToCountToPatterns, ABBREVIATED_NAME, pluralKeywords); |
| isReady = true; |
| } |
| |
| |
| private void setup(String resourceKey, Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns, |
| int style, Set<String> pluralKeywords) { |
| // 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<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit); |
| if (countToPatterns == null) { |
| countToPatterns = new TreeMap<String, Object[]>(); |
| timeUnitToCountToPatterns.put(timeUnit, countToPatterns); |
| } |
| for ( int pluralIndex = 0; pluralIndex < count; ++pluralIndex) { |
| String pluralCount = oneUnitRes.get(pluralIndex).getKey(); |
| if (!pluralKeywords.contains(pluralCount)) |
| continue; |
| 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<String> 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<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit); |
| if (countToPatterns == null) { |
| countToPatterns = new TreeMap<String, Object[]>(); |
| 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<String, Object[]> 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 no unitsShort resource was found even after fallback to root locale |
| // then search the units resource fallback from the current level to root |
| if ( parentLocale == null && resourceKey.equals("unitsShort") ) { |
| searchInTree("units", styl, timeUnit, srcPluralCount, searchPluralCount, countToPatterns); |
| if ( countToPatterns != null && |
| countToPatterns.get(srcPluralCount) != null && |
| countToPatterns.get(srcPluralCount)[styl] != null ) { |
| return; |
| } |
| } |
| |
| // 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); |
| } |
| } |
| } |