2 **************************************************************************
\r
3 * Copyright (C) 2008-2009, Google, International Business Machines
\r
4 * Corporationand others. All Rights Reserved.
\r
5 **************************************************************************
\r
7 package com.ibm.icu.text;
\r
9 import java.text.FieldPosition;
\r
10 import java.text.ParsePosition;
\r
11 import java.util.HashMap;
\r
12 import java.util.Iterator;
\r
13 import java.util.Locale;
\r
14 import java.util.Map;
\r
15 import java.util.MissingResourceException;
\r
16 import java.util.TreeMap;
\r
17 import java.util.Set;
\r
19 import com.ibm.icu.impl.ICUResourceBundle;
\r
20 import com.ibm.icu.util.TimeUnit;
\r
21 import com.ibm.icu.util.TimeUnitAmount;
\r
22 import com.ibm.icu.util.ULocale;
\r
23 import com.ibm.icu.util.UResourceBundle;
\r
27 * Format or parse a TimeUnitAmount, using plural rules for the units where available.
\r
32 * // create a time unit instance.
\r
33 * // only SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, and YEAR are supported
\r
34 * TimeUnit timeUnit = TimeUnit.SECOND;
\r
35 * // create time unit amount instance - a combination of Number and time unit
\r
36 * TimeUnitAmount source = new TimeUnitAmount(2, timeUnit);
\r
37 * // create time unit format instance
\r
38 * TimeUnitFormat format = new TimeUnitFormat();
\r
39 * // set the locale of time unit format
\r
40 * format.setLocale(new ULocale("en"));
\r
41 * // format a time unit amount
\r
42 * String formatted = format.format(source);
\r
43 * System.out.println(formatted);
\r
45 * // parse a string into time unit amount
\r
46 * TimeUnitAmount result = (TimeUnitAmount) format.parseObject(formatted);
\r
47 * // result should equal to source
\r
48 * } catch (ParseException e) {
\r
53 * @see TimeUnitAmount
\r
54 * @see TimeUnitFormat
\r
58 public class TimeUnitFormat extends MeasureFormat {
\r
61 * Constant for full name style format.
\r
62 * For example, the full name for "hour" in English is "hour" or "hours".
\r
64 * @provisional This API might change or be removed in a future release.
\r
66 public static final int FULL_NAME = 0;
\r
68 * Constant for abbreviated name style format.
\r
69 * For example, the abbreviated name for "hour" in English is "hr" or "hrs".
\r
71 * @provisional This API might change or be removed in a future release.
\r
73 public static final int ABBREVIATED_NAME = 1;
\r
75 private static final int TOTAL_STYLES = 2;
\r
77 private static final long serialVersionUID = -3707773153184971529L;
\r
79 private static final String DEFAULT_PATTERN_FOR_SECOND = "{0} s";
\r
80 private static final String DEFAULT_PATTERN_FOR_MINUTE = "{0} min";
\r
81 private static final String DEFAULT_PATTERN_FOR_HOUR = "{0} h";
\r
82 private static final String DEFAULT_PATTERN_FOR_DAY = "{0} d";
\r
83 private static final String DEFAULT_PATTERN_FOR_WEEK = "{0} w";
\r
84 private static final String DEFAULT_PATTERN_FOR_MONTH = "{0} m";
\r
85 private static final String DEFAULT_PATTERN_FOR_YEAR = "{0} y";
\r
87 private NumberFormat format;
\r
88 private ULocale locale;
\r
89 private transient Map timeUnitToCountToPatterns;
\r
90 private transient PluralRules pluralRules;
\r
91 private transient boolean isReady;
\r
95 * Create empty format using full name style, for example, "hours".
\r
96 * Use setLocale and/or setFormat to modify.
\r
99 public TimeUnitFormat() {
\r
106 * Create TimeUnitFormat given a ULocale, and using full name style.
\r
107 * @param locale locale of this time unit formatter.
\r
110 public TimeUnitFormat(ULocale locale) {
\r
111 this(locale, FULL_NAME);
\r
115 * Create TimeUnitFormat given a Locale, and using full name style.
\r
116 * @param locale locale of this time unit formatter.
\r
119 public TimeUnitFormat(Locale locale) {
\r
120 this(locale, FULL_NAME);
\r
124 * Create TimeUnitFormat given a ULocale and a formatting style: full or
\r
126 * @param locale locale of this time unit formatter.
\r
127 * @param style format style, either FULL_NAME or ABBREVIATED_NAME style.
\r
128 * @throws IllegalArgumentException if the style is not FULL_NAME or
\r
129 * ABBREVIATED_NAME style.
\r
131 * @provisional This API might change or be removed in a future release.
\r
133 public TimeUnitFormat(ULocale locale, int style) {
\r
134 if (style < FULL_NAME || style >= TOTAL_STYLES) {
\r
135 throw new IllegalArgumentException("style should be either FULL_NAME or ABBREVIATED_NAME style");
\r
137 this.style = style;
\r
138 this.locale = locale;
\r
143 * Create TimeUnitFormat given a Locale and a formatting style: full or
\r
146 * @provisional This API might change or be removed in a future release.
\r
148 public TimeUnitFormat(Locale locale, int style) {
\r
149 this(ULocale.forLocale(locale), style);
\r
153 * Set the locale used for formatting or parsing.
\r
154 * @param locale locale of this time unit formatter.
\r
155 * @return this, for chaining.
\r
158 public TimeUnitFormat setLocale(ULocale locale) {
\r
159 if ( locale != this.locale ) {
\r
160 this.locale = locale;
\r
167 * Set the locale used for formatting or parsing.
\r
168 * @param locale locale of this time unit formatter.
\r
169 * @return this, for chaining.
\r
172 public TimeUnitFormat setLocale(Locale locale) {
\r
173 return setLocale(ULocale.forLocale(locale));
\r
177 * Set the format used for formatting or parsing. If null or not available, use the getNumberInstance(locale).
\r
178 * @param format the number formatter.
\r
179 * @return this, for chaining.
\r
182 public TimeUnitFormat setNumberFormat(NumberFormat format) {
\r
183 if (format == this.format) {
\r
186 if ( format == null ) {
\r
187 if ( locale == null ) {
\r
191 this.format = NumberFormat.getNumberInstance(locale);
\r
194 this.format = format;
\r
196 // reset the number formatter in the timeUnitToCountToPatterns map
\r
197 if (isReady == false) {
\r
200 for (Iterator it = timeUnitToCountToPatterns.keySet().iterator();
\r
202 TimeUnit timeUnit = (TimeUnit) it.next();
\r
203 Map countToPattern = (Map) timeUnitToCountToPatterns.get(timeUnit);
\r
204 for (Iterator it2 = countToPattern.keySet().iterator(); it2.hasNext();) {
\r
205 String count = (String) it2.next();
\r
206 Object[] pair = (Object[])countToPattern.get(count);
\r
207 MessageFormat pattern = (MessageFormat)pair[FULL_NAME];
\r
208 pattern.setFormatByArgumentIndex(0, format);
\r
209 pattern = (MessageFormat)pair[ABBREVIATED_NAME];
\r
210 pattern.setFormatByArgumentIndex(0, format);
\r
218 * Format a TimeUnitAmount.
\r
219 * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
\r
222 public StringBuffer format(Object obj, StringBuffer toAppendTo,
\r
223 FieldPosition pos) {
\r
224 if ( !(obj instanceof TimeUnitAmount) ) {
\r
225 throw new IllegalArgumentException("can not format non TimeUnitAmount object");
\r
230 TimeUnitAmount amount = (TimeUnitAmount) obj;
\r
231 Map countToPattern = (Map) timeUnitToCountToPatterns.get(amount.getTimeUnit());
\r
232 double number = amount.getNumber().doubleValue();
\r
233 String count = pluralRules.select(number);
\r
234 MessageFormat pattern = (MessageFormat)((Object[])countToPattern.get(count))[style];
\r
235 return pattern.format(new Object[]{amount.getNumber()}, toAppendTo, pos);
\r
240 * Parse a TimeUnitAmount.
\r
241 * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)
\r
244 public Object parseObject(String source, ParsePosition pos) {
\r
248 Number resultNumber = null;
\r
249 TimeUnit resultTimeUnit = null;
\r
250 int oldPos = pos.getIndex();
\r
252 int longestParseDistance = 0;
\r
253 String countOfLongestMatch = null;
\r
254 // we don't worry too much about speed on parsing, but this can be optimized later if needed.
\r
255 // Parse by iterating through all available patterns
\r
256 // and looking for the longest match.
\r
257 for (Iterator it = timeUnitToCountToPatterns.keySet().iterator(); it.hasNext();) {
\r
258 TimeUnit timeUnit = (TimeUnit) it.next();
\r
259 Map countToPattern = (Map) timeUnitToCountToPatterns.get(timeUnit);
\r
260 for (Iterator it2 = countToPattern.keySet().iterator(); it2.hasNext();) {
\r
261 String count = (String) it2.next();
\r
262 for (int styl = FULL_NAME; styl < TOTAL_STYLES; ++styl) {
\r
263 MessageFormat pattern = (MessageFormat)((Object[])countToPattern.get(count))[styl];
\r
264 pos.setErrorIndex(-1);
\r
265 pos.setIndex(oldPos);
\r
266 // see if we can parse
\r
267 Object parsed = pattern.parseObject(source, pos);
\r
268 if ( pos.getErrorIndex() != -1 || pos.getIndex() == oldPos ) {
\r
272 Number temp = null;
\r
273 if ( ((Object[])parsed).length != 0 ) {
\r
274 // pattern with Number as beginning,
\r
275 // such as "{0} d".
\r
276 // check to make sure that the timeUnit is consistent
\r
277 temp = (Number)((Object[])parsed)[0];
\r
278 String select = pluralRules.select(temp.doubleValue());
\r
279 if (!count.equals(select)) {
\r
283 int parseDistance = pos.getIndex() - oldPos;
\r
284 if ( parseDistance > longestParseDistance ) {
\r
285 resultNumber = temp;
\r
286 resultTimeUnit = timeUnit;
\r
287 newPos = pos.getIndex();
\r
288 longestParseDistance = parseDistance;
\r
289 countOfLongestMatch = count;
\r
294 /* After find the longest match, parse the number.
\r
295 * Result number could be null for the pattern without number pattern.
\r
296 * such as unit pattern in Arabic.
\r
297 * When result number is null, use plural rule to set the number.
\r
299 if (resultNumber == null && longestParseDistance != 0) {
\r
300 // set the number using plurrual count
\r
301 if ( countOfLongestMatch.equals("zero") ) {
\r
302 resultNumber = new Integer(0);
\r
303 } else if ( countOfLongestMatch.equals("one") ) {
\r
304 resultNumber = new Integer(1);
\r
305 } else if ( countOfLongestMatch.equals("two") ) {
\r
306 resultNumber = new Integer(2);
\r
308 // should not happen.
\r
309 // TODO: how to handle?
\r
310 resultNumber = new Integer(3);
\r
313 if (longestParseDistance == 0) {
\r
314 pos.setIndex(oldPos);
\r
315 pos.setErrorIndex(0);
\r
318 pos.setIndex(newPos);
\r
319 pos.setErrorIndex(-1);
\r
320 return new TimeUnitAmount(resultNumber, resultTimeUnit);
\r
326 * Initialize locale, number formatter, plural rules, and
\r
327 * time units patterns.
\r
328 * Initially, we are storing all of these as MessageFormats.
\r
329 * I think it might actually be simpler to make them Decimal Formats later.
\r
331 private void setup() {
\r
332 if (locale == null) {
\r
333 if (format != null) {
\r
334 locale = format.getLocale(null);
\r
336 locale = ULocale.getDefault();
\r
339 if (format == null) {
\r
340 format = NumberFormat.getNumberInstance(locale);
\r
342 pluralRules = PluralRules.forLocale(locale);
\r
343 timeUnitToCountToPatterns = new HashMap();
\r
345 setup("units", timeUnitToCountToPatterns, FULL_NAME);
\r
346 setup("unitsShort", timeUnitToCountToPatterns, ABBREVIATED_NAME);
\r
351 private void setup(String resourceKey, Map timeUnitToCountToPatterns,
\r
353 // fill timeUnitToCountToPatterns from resource file
\r
355 ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);
\r
356 ICUResourceBundle unitsRes = resource.getWithFallback(resourceKey);
\r
357 int size = unitsRes.getSize();
\r
358 for ( int index = 0; index < size; ++index) {
\r
359 String timeUnitName = unitsRes.get(index).getKey();
\r
360 TimeUnit timeUnit = null;
\r
361 if ( timeUnitName.equals("year") ) {
\r
362 timeUnit = TimeUnit.YEAR;
\r
363 } else if ( timeUnitName.equals("month") ) {
\r
364 timeUnit = TimeUnit.MONTH;
\r
365 } else if ( timeUnitName.equals("day") ) {
\r
366 timeUnit = TimeUnit.DAY;
\r
367 } else if ( timeUnitName.equals("hour") ) {
\r
368 timeUnit = TimeUnit.HOUR;
\r
369 } else if ( timeUnitName.equals("minute") ) {
\r
370 timeUnit = TimeUnit.MINUTE;
\r
371 } else if ( timeUnitName.equals("second") ) {
\r
372 timeUnit = TimeUnit.SECOND;
\r
373 } else if ( timeUnitName.equals("week") ) {
\r
374 timeUnit = TimeUnit.WEEK;
\r
378 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(timeUnitName);
\r
379 int count = oneUnitRes.getSize();
\r
380 Map countToPatterns = (Map)timeUnitToCountToPatterns.get(timeUnit);
\r
381 if (countToPatterns == null) {
\r
382 countToPatterns = new TreeMap();
\r
383 timeUnitToCountToPatterns.put(timeUnit, countToPatterns);
\r
385 for ( int pluralIndex = 0; pluralIndex < count; ++pluralIndex) {
\r
386 String pluralCount = oneUnitRes.get(pluralIndex).getKey();
\r
387 String pattern = oneUnitRes.get(pluralIndex).getString();
\r
388 final MessageFormat messageFormat = new MessageFormat(pattern, locale);
\r
389 if (format != null) {
\r
390 messageFormat.setFormatByArgumentIndex(0, format);
\r
392 // save both full name and abbreviated name in one table
\r
393 // is good space-wise, but it degrades performance,
\r
394 // since it needs to check whether the needed space
\r
395 // is already allocated or not.
\r
396 Object[] pair = (Object[])countToPatterns.get(pluralCount);
\r
397 if (pair == null) {
\r
398 pair = new Object[2];
\r
399 countToPatterns.put(pluralCount, pair);
\r
401 pair[style] = messageFormat;
\r
404 } catch ( MissingResourceException e ) {
\r
407 // there should be patterns for each plural rule in each time unit.
\r
408 // For each time unit,
\r
409 // for each plural rule, following is unit pattern fall-back rule:
\r
410 // ( for example: "one" hour )
\r
411 // look for its unit pattern in its locale tree.
\r
412 // if pattern is not found in its own locale, such as de_DE,
\r
413 // look for the pattern in its parent, such as de,
\r
414 // keep looking till found or till root.
\r
415 // if the pattern is not found in root either,
\r
416 // fallback to plural count "other",
\r
417 // look for the pattern of "other" in the locale tree:
\r
418 // "de_DE" to "de" to "root".
\r
419 // If not found, fall back to value of
\r
420 // static variable DEFAULT_PATTERN_FOR_xxx, such as "{0} h".
\r
422 // Following is consistency check to create pattern for each
\r
423 // plural rule in each time unit using above fall-back rule.
\r
425 final TimeUnit[] timeUnits = TimeUnit.values();
\r
426 Set keywords = pluralRules.getKeywords();
\r
427 for ( int i = 0; i < timeUnits.length; ++i ) {
\r
428 // for each time unit,
\r
429 // get all the patterns for each plural rule in this locale.
\r
430 final TimeUnit timeUnit = timeUnits[i];
\r
431 Map countToPatterns = (Map) timeUnitToCountToPatterns.get(timeUnit);
\r
432 if ( countToPatterns == null ) {
\r
433 countToPatterns = new TreeMap();
\r
434 timeUnitToCountToPatterns.put(timeUnit, countToPatterns);
\r
436 for (Iterator it = keywords.iterator(); it.hasNext();) {
\r
437 String pluralCount = (String) it.next();
\r
438 if ( countToPatterns.get(pluralCount) == null ||
\r
439 ((Object[])countToPatterns.get(pluralCount))[style] == null ) {
\r
440 // look through parents
\r
441 searchInTree(resourceKey, style, timeUnit, pluralCount, pluralCount, countToPatterns);
\r
449 // srcPluralCount is the original plural count on which the pattern is
\r
451 // searchPluralCount is the fallback plural count.
\r
452 // For example, to search for pattern for ""one" hour",
\r
453 // "one" is the srcPluralCount,
\r
454 // if the pattern is not found even in root, fallback to
\r
455 // using patterns of plural count "other",
\r
456 // then, "other" is the searchPluralCount.
\r
457 private void searchInTree(String resourceKey, int styl,
\r
458 TimeUnit timeUnit, String srcPluralCount,
\r
459 String searchPluralCount, Map countToPatterns) {
\r
460 ULocale parentLocale=locale;
\r
461 String srcTimeUnitName = timeUnit.toString();
\r
462 while ( parentLocale != null ) {
\r
464 // look for pattern for srcPluralCount in locale tree
\r
465 ICUResourceBundle unitsRes = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, parentLocale);
\r
466 unitsRes = unitsRes.getWithFallback(resourceKey);
\r
467 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(srcTimeUnitName);
\r
468 String pattern = oneUnitRes.getStringWithFallback(searchPluralCount);
\r
469 final MessageFormat messageFormat = new MessageFormat(pattern, locale);
\r
470 if (format != null) {
\r
471 messageFormat.setFormatByArgumentIndex(0, format);
\r
473 Object[] pair = (Object[])countToPatterns.get(srcPluralCount);
\r
474 if (pair == null) {
\r
475 pair = new Object[2];
\r
476 countToPatterns.put(srcPluralCount, pair);
\r
478 pair[styl] = messageFormat;
\r
480 } catch ( MissingResourceException e ) {
\r
482 parentLocale=parentLocale.getFallback();
\r
484 // if not found the pattern for this plural count at all,
\r
485 // fall-back to plural count "other"
\r
486 if ( searchPluralCount.equals("other") ) {
\r
487 // set default fall back the same as the resource in root
\r
488 MessageFormat messageFormat = null;
\r
489 if ( timeUnit == TimeUnit.SECOND ) {
\r
490 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_SECOND, locale);
\r
491 } else if ( timeUnit == TimeUnit.MINUTE ) {
\r
492 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MINUTE, locale);
\r
493 } else if ( timeUnit == TimeUnit.HOUR ) {
\r
494 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_HOUR, locale);
\r
495 } else if ( timeUnit == TimeUnit.WEEK ) {
\r
496 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_WEEK, locale);
\r
497 } else if ( timeUnit == TimeUnit.DAY ) {
\r
498 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_DAY, locale);
\r
499 } else if ( timeUnit == TimeUnit.MONTH ) {
\r
500 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MONTH, locale);
\r
501 } else if ( timeUnit == TimeUnit.YEAR ) {
\r
502 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_YEAR, locale);
\r
504 if (format != null && messageFormat != null) {
\r
505 messageFormat.setFormatByArgumentIndex(0, format);
\r
507 Object[] pair = (Object[])countToPatterns.get(srcPluralCount);
\r
508 if (pair == null) {
\r
509 pair = new Object[2];
\r
510 countToPatterns.put(srcPluralCount, pair);
\r
512 pair[styl] = messageFormat;
\r
514 // fall back to rule "other", and search in parents
\r
515 searchInTree(resourceKey, styl, timeUnit, srcPluralCount, "other", countToPatterns);
\r