2 **************************************************************************
3 * Copyright (C) 2008-2013, Google, International Business Machines
4 * Corporation and others. All Rights Reserved.
5 **************************************************************************
7 package com.ibm.icu.text;
9 import java.text.FieldPosition;
10 import java.text.ParsePosition;
11 import java.util.HashMap;
12 import java.util.Locale;
14 import java.util.Map.Entry;
15 import java.util.MissingResourceException;
17 import java.util.TreeMap;
19 import com.ibm.icu.impl.ICUResourceBundle;
20 import com.ibm.icu.util.TimeUnit;
21 import com.ibm.icu.util.TimeUnitAmount;
22 import com.ibm.icu.util.ULocale;
23 import com.ibm.icu.util.ULocale.Category;
24 import com.ibm.icu.util.UResourceBundle;
28 * Format or parse a TimeUnitAmount, using plural rules for the units where available.
33 * // create a time unit instance.
34 * // only SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, and YEAR are supported
35 * TimeUnit timeUnit = TimeUnit.SECOND;
36 * // create time unit amount instance - a combination of Number and time unit
37 * TimeUnitAmount source = new TimeUnitAmount(2, timeUnit);
38 * // create time unit format instance
39 * TimeUnitFormat format = new TimeUnitFormat();
40 * // set the locale of time unit format
41 * format.setLocale(new ULocale("en"));
42 * // format a time unit amount
43 * String formatted = format.format(source);
44 * System.out.println(formatted);
46 * // parse a string into time unit amount
47 * TimeUnitAmount result = (TimeUnitAmount) format.parseObject(formatted);
48 * // result should equal to source
49 * } catch (ParseException e) {
59 public class TimeUnitFormat extends MeasureFormat {
62 * Constant for full name style format.
63 * For example, the full name for "hour" in English is "hour" or "hours".
66 public static final int FULL_NAME = 0;
68 * Constant for abbreviated name style format.
69 * For example, the abbreviated name for "hour" in English is "hr" or "hrs".
72 public static final int ABBREVIATED_NAME = 1;
74 private static final int TOTAL_STYLES = 2;
76 private static final long serialVersionUID = -3707773153184971529L;
78 private static final String DEFAULT_PATTERN_FOR_SECOND = "{0} s";
79 private static final String DEFAULT_PATTERN_FOR_MINUTE = "{0} min";
80 private static final String DEFAULT_PATTERN_FOR_HOUR = "{0} h";
81 private static final String DEFAULT_PATTERN_FOR_DAY = "{0} d";
82 private static final String DEFAULT_PATTERN_FOR_WEEK = "{0} w";
83 private static final String DEFAULT_PATTERN_FOR_MONTH = "{0} m";
84 private static final String DEFAULT_PATTERN_FOR_YEAR = "{0} y";
86 private NumberFormat format;
87 private ULocale locale;
88 private transient Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns;
89 private transient PluralRules pluralRules;
90 private transient boolean isReady;
94 * Create empty format using full name style, for example, "hours".
95 * Use setLocale and/or setFormat to modify.
98 public TimeUnitFormat() {
105 * Create TimeUnitFormat given a ULocale, and using full name style.
106 * @param locale locale of this time unit formatter.
109 public TimeUnitFormat(ULocale locale) {
110 this(locale, FULL_NAME);
114 * Create TimeUnitFormat given a Locale, and using full name style.
115 * @param locale locale of this time unit formatter.
118 public TimeUnitFormat(Locale locale) {
119 this(locale, FULL_NAME);
123 * Create TimeUnitFormat given a ULocale and a formatting style.
124 * @param locale locale of this time unit formatter.
125 * @param style format style, either FULL_NAME or ABBREVIATED_NAME style.
126 * @throws IllegalArgumentException if the style is not FULL_NAME or
127 * ABBREVIATED_NAME style.
130 public TimeUnitFormat(ULocale locale, int style) {
131 if (style < FULL_NAME || style >= TOTAL_STYLES) {
132 throw new IllegalArgumentException("style should be either FULL_NAME or ABBREVIATED_NAME style");
135 this.locale = locale;
140 * Create TimeUnitFormat given a Locale and a formatting style.
143 public TimeUnitFormat(Locale locale, int style) {
144 this(ULocale.forLocale(locale), style);
148 * Set the locale used for formatting or parsing.
149 * @param locale locale of this time unit formatter.
150 * @return this, for chaining.
153 public TimeUnitFormat setLocale(ULocale locale) {
154 if ( locale != this.locale ) {
155 this.locale = locale;
162 * Set the locale used for formatting or parsing.
163 * @param locale locale of this time unit formatter.
164 * @return this, for chaining.
167 public TimeUnitFormat setLocale(Locale locale) {
168 return setLocale(ULocale.forLocale(locale));
172 * Set the format used for formatting or parsing. Passing null is equivalent to passing
173 * {@link NumberFormat#getNumberInstance(ULocale)}.
174 * @param format the number formatter.
175 * @return this, for chaining.
178 public TimeUnitFormat setNumberFormat(NumberFormat format) {
179 if (format == this.format) {
182 if ( format == null ) {
183 if ( locale == null ) {
187 this.format = NumberFormat.getNumberInstance(locale);
190 this.format = format;
192 // reset the number formatter in the timeUnitToCountToPatterns map
193 if (isReady == false) {
196 for (Map<String, Object[]> countToPattern : timeUnitToCountToPatterns.values()) {
197 for (Object[] pair : countToPattern.values()) {
198 MessageFormat pattern = (MessageFormat)pair[FULL_NAME];
199 pattern.setFormatByArgumentIndex(0, format);
200 pattern = (MessageFormat)pair[ABBREVIATED_NAME];
201 pattern.setFormatByArgumentIndex(0, format);
209 * Format a TimeUnitAmount.
210 * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
213 public StringBuffer format(Object obj, StringBuffer toAppendTo,
215 if ( !(obj instanceof TimeUnitAmount) ) {
216 throw new IllegalArgumentException(
217 "cannot format a non TimeUnitAmount object");
222 TimeUnitAmount amount = (TimeUnitAmount) obj;
223 Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(amount.getTimeUnit());
224 double number = amount.getNumber().doubleValue();
225 String count = pluralRules.select(number);
226 MessageFormat pattern = (MessageFormat)(countToPattern.get(count))[style];
227 return pattern.format(new Object[]{amount.getNumber()}, toAppendTo, pos);
231 * Parse a TimeUnitAmount.
232 * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)
235 public Object parseObject(String source, ParsePosition pos) {
239 Number resultNumber = null;
240 TimeUnit resultTimeUnit = null;
241 int oldPos = pos.getIndex();
243 int longestParseDistance = 0;
244 String countOfLongestMatch = null;
245 // we don't worry too much about speed on parsing, but this can be optimized later if needed.
246 // Parse by iterating through all available patterns
247 // and looking for the longest match.
248 for (TimeUnit timeUnit : timeUnitToCountToPatterns.keySet()) {
249 Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(timeUnit);
250 for (Entry<String, Object[]> patternEntry : countToPattern.entrySet()) {
251 String count = patternEntry.getKey();
252 for (int styl = FULL_NAME; styl < TOTAL_STYLES; ++styl) {
253 MessageFormat pattern = (MessageFormat)(patternEntry.getValue())[styl];
254 pos.setErrorIndex(-1);
255 pos.setIndex(oldPos);
256 // see if we can parse
257 Object parsed = pattern.parseObject(source, pos);
258 if ( pos.getErrorIndex() != -1 || pos.getIndex() == oldPos ) {
263 if ( ((Object[])parsed).length != 0 ) {
264 // pattern with Number as beginning,
266 // check to make sure that the timeUnit is consistent
267 temp = (Number)((Object[])parsed)[0];
268 String select = pluralRules.select(temp.doubleValue());
269 if (!count.equals(select)) {
273 int parseDistance = pos.getIndex() - oldPos;
274 if ( parseDistance > longestParseDistance ) {
276 resultTimeUnit = timeUnit;
277 newPos = pos.getIndex();
278 longestParseDistance = parseDistance;
279 countOfLongestMatch = count;
284 /* After find the longest match, parse the number.
285 * Result number could be null for the pattern without number pattern.
286 * such as unit pattern in Arabic.
287 * When result number is null, use plural rule to set the number.
289 if (resultNumber == null && longestParseDistance != 0) {
290 // set the number using plurrual count
291 if ( countOfLongestMatch.equals("zero") ) {
292 resultNumber = Integer.valueOf(0);
293 } else if ( countOfLongestMatch.equals("one") ) {
294 resultNumber = Integer.valueOf(1);
295 } else if ( countOfLongestMatch.equals("two") ) {
296 resultNumber = Integer.valueOf(2);
298 // should not happen.
299 // TODO: how to handle?
300 resultNumber = Integer.valueOf(3);
303 if (longestParseDistance == 0) {
304 pos.setIndex(oldPos);
305 pos.setErrorIndex(0);
308 pos.setIndex(newPos);
309 pos.setErrorIndex(-1);
310 return new TimeUnitAmount(resultNumber, resultTimeUnit);
316 * Initialize locale, number formatter, plural rules, and
317 * time units patterns.
318 * Initially, we are storing all of these as MessageFormats.
319 * I think it might actually be simpler to make them Decimal Formats later.
321 private void setup() {
322 if (locale == null) {
323 if (format != null) {
324 locale = format.getLocale(null);
326 locale = ULocale.getDefault(Category.FORMAT);
329 if (format == null) {
330 format = NumberFormat.getNumberInstance(locale);
332 pluralRules = PluralRules.forLocale(locale);
333 timeUnitToCountToPatterns = new HashMap<TimeUnit, Map<String, Object[]>>();
334 Set<String> pluralKeywords = pluralRules.getKeywords();
335 setup("units/duration", timeUnitToCountToPatterns, FULL_NAME, pluralKeywords);
336 setup("unitsShort/duration", timeUnitToCountToPatterns, ABBREVIATED_NAME, pluralKeywords);
340 private void setup(String resourceKey, Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns,
341 int style, Set<String> pluralKeywords) {
342 // fill timeUnitToCountToPatterns from resource file
344 ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);
345 ICUResourceBundle unitsRes = resource.getWithFallback(resourceKey);
346 int size = unitsRes.getSize();
347 for ( int index = 0; index < size; ++index) {
348 String timeUnitName = unitsRes.get(index).getKey();
349 TimeUnit timeUnit = null;
350 if ( timeUnitName.equals("year") ) {
351 timeUnit = TimeUnit.YEAR;
352 } else if ( timeUnitName.equals("month") ) {
353 timeUnit = TimeUnit.MONTH;
354 } else if ( timeUnitName.equals("day") ) {
355 timeUnit = TimeUnit.DAY;
356 } else if ( timeUnitName.equals("hour") ) {
357 timeUnit = TimeUnit.HOUR;
358 } else if ( timeUnitName.equals("minute") ) {
359 timeUnit = TimeUnit.MINUTE;
360 } else if ( timeUnitName.equals("second") ) {
361 timeUnit = TimeUnit.SECOND;
362 } else if ( timeUnitName.equals("week") ) {
363 timeUnit = TimeUnit.WEEK;
367 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(timeUnitName);
368 int count = oneUnitRes.getSize();
369 Map<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit);
370 if (countToPatterns == null) {
371 countToPatterns = new TreeMap<String, Object[]>();
372 timeUnitToCountToPatterns.put(timeUnit, countToPatterns);
374 for ( int pluralIndex = 0; pluralIndex < count; ++pluralIndex) {
375 String pluralCount = oneUnitRes.get(pluralIndex).getKey();
376 if (!pluralKeywords.contains(pluralCount))
378 String pattern = oneUnitRes.get(pluralIndex).getString();
379 final MessageFormat messageFormat = new MessageFormat(pattern, locale);
380 if (format != null) {
381 messageFormat.setFormatByArgumentIndex(0, format);
383 // save both full name and abbreviated name in one table
384 // is good space-wise, but it degrades performance,
385 // since it needs to check whether the needed space
386 // is already allocated or not.
387 Object[] pair = countToPatterns.get(pluralCount);
389 pair = new Object[2];
390 countToPatterns.put(pluralCount, pair);
392 pair[style] = messageFormat;
395 } catch ( MissingResourceException e ) {
398 // there should be patterns for each plural rule in each time unit.
399 // For each time unit,
400 // for each plural rule, following is unit pattern fall-back rule:
401 // ( for example: "one" hour )
402 // look for its unit pattern in its locale tree.
403 // if pattern is not found in its own locale, such as de_DE,
404 // look for the pattern in its parent, such as de,
405 // keep looking till found or till root.
406 // if the pattern is not found in root either,
407 // fallback to plural count "other",
408 // look for the pattern of "other" in the locale tree:
409 // "de_DE" to "de" to "root".
410 // If not found, fall back to value of
411 // static variable DEFAULT_PATTERN_FOR_xxx, such as "{0} h".
413 // Following is consistency check to create pattern for each
414 // plural rule in each time unit using above fall-back rule.
416 final TimeUnit[] timeUnits = TimeUnit.values();
417 Set<String> keywords = pluralRules.getKeywords();
418 for ( int i = 0; i < timeUnits.length; ++i ) {
419 // for each time unit,
420 // get all the patterns for each plural rule in this locale.
421 final TimeUnit timeUnit = timeUnits[i];
422 Map<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit);
423 if (countToPatterns == null) {
424 countToPatterns = new TreeMap<String, Object[]>();
425 timeUnitToCountToPatterns.put(timeUnit, countToPatterns);
427 for (String pluralCount : keywords) {
428 if ( countToPatterns.get(pluralCount) == null ||
429 countToPatterns.get(pluralCount)[style] == null ) {
430 // look through parents
431 searchInTree(resourceKey, style, timeUnit, pluralCount, pluralCount, countToPatterns);
439 // srcPluralCount is the original plural count on which the pattern is
441 // searchPluralCount is the fallback plural count.
442 // For example, to search for pattern for ""one" hour",
443 // "one" is the srcPluralCount,
444 // if the pattern is not found even in root, fallback to
445 // using patterns of plural count "other",
446 // then, "other" is the searchPluralCount.
447 private void searchInTree(String resourceKey, int styl,
448 TimeUnit timeUnit, String srcPluralCount,
449 String searchPluralCount, Map<String, Object[]> countToPatterns) {
450 ULocale parentLocale=locale;
451 String srcTimeUnitName = timeUnit.toString();
452 while ( parentLocale != null ) {
454 // look for pattern for srcPluralCount in locale tree
455 ICUResourceBundle unitsRes = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, parentLocale);
456 unitsRes = unitsRes.getWithFallback(resourceKey);
457 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(srcTimeUnitName);
458 String pattern = oneUnitRes.getStringWithFallback(searchPluralCount);
459 final MessageFormat messageFormat = new MessageFormat(pattern, locale);
460 if (format != null) {
461 messageFormat.setFormatByArgumentIndex(0, format);
463 Object[] pair = countToPatterns.get(srcPluralCount);
465 pair = new Object[2];
466 countToPatterns.put(srcPluralCount, pair);
468 pair[styl] = messageFormat;
470 } catch ( MissingResourceException e ) {
472 parentLocale=parentLocale.getFallback();
475 // if no unitsShort resource was found even after fallback to root locale
476 // then search the units resource fallback from the current level to root
477 if ( parentLocale == null && resourceKey.equals("unitsShort") ) {
478 searchInTree("units", styl, timeUnit, srcPluralCount, searchPluralCount, countToPatterns);
479 if ( countToPatterns != null &&
480 countToPatterns.get(srcPluralCount) != null &&
481 countToPatterns.get(srcPluralCount)[styl] != null ) {
486 // if not found the pattern for this plural count at all,
487 // fall-back to plural count "other"
488 if ( searchPluralCount.equals("other") ) {
489 // set default fall back the same as the resource in root
490 MessageFormat messageFormat = null;
491 if ( timeUnit == TimeUnit.SECOND ) {
492 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_SECOND, locale);
493 } else if ( timeUnit == TimeUnit.MINUTE ) {
494 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MINUTE, locale);
495 } else if ( timeUnit == TimeUnit.HOUR ) {
496 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_HOUR, locale);
497 } else if ( timeUnit == TimeUnit.WEEK ) {
498 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_WEEK, locale);
499 } else if ( timeUnit == TimeUnit.DAY ) {
500 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_DAY, locale);
501 } else if ( timeUnit == TimeUnit.MONTH ) {
502 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MONTH, locale);
503 } else if ( timeUnit == TimeUnit.YEAR ) {
504 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_YEAR, locale);
506 if (format != null && messageFormat != null) {
507 messageFormat.setFormatByArgumentIndex(0, format);
509 Object[] pair = countToPatterns.get(srcPluralCount);
511 pair = new Object[2];
512 countToPatterns.put(srcPluralCount, pair);
514 pair[styl] = messageFormat;
516 // fall back to rule "other", and search in parents
517 searchInTree(resourceKey, styl, timeUnit, srcPluralCount, "other", countToPatterns);