]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-4_4_2-src/main/classes/core/src/com/ibm/icu/text/TimeUnitFormat.java
go
[Dictionary.git] / jars / icu4j-4_4_2-src / main / classes / core / src / com / ibm / icu / text / TimeUnitFormat.java
1 /*\r
2  **************************************************************************\r
3  * Copyright (C) 2008-2010, Google, International Business Machines\r
4  * Corporationand others. All Rights Reserved.\r
5  **************************************************************************\r
6  */\r
7 package com.ibm.icu.text;\r
8 \r
9 import java.text.FieldPosition;\r
10 import java.text.ParsePosition;\r
11 import java.util.HashMap;\r
12 import java.util.Locale;\r
13 import java.util.Map;\r
14 import java.util.MissingResourceException;\r
15 import java.util.Set;\r
16 import java.util.TreeMap;\r
17 \r
18 import com.ibm.icu.impl.ICUResourceBundle;\r
19 import com.ibm.icu.util.TimeUnit;\r
20 import com.ibm.icu.util.TimeUnitAmount;\r
21 import com.ibm.icu.util.ULocale;\r
22 import com.ibm.icu.util.UResourceBundle;\r
23 \r
24 \r
25 /**\r
26  * Format or parse a TimeUnitAmount, using plural rules for the units where available.\r
27  *\r
28  * <P>\r
29  * Code Sample: \r
30  * <pre>\r
31  *   // create a time unit instance.\r
32  *   // only SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, and YEAR are supported\r
33  *   TimeUnit timeUnit = TimeUnit.SECOND;\r
34  *   // create time unit amount instance - a combination of Number and time unit\r
35  *   TimeUnitAmount source = new TimeUnitAmount(2, timeUnit);\r
36  *   // create time unit format instance\r
37  *   TimeUnitFormat format = new TimeUnitFormat();\r
38  *   // set the locale of time unit format\r
39  *   format.setLocale(new ULocale("en"));\r
40  *   // format a time unit amount\r
41  *   String formatted = format.format(source);\r
42  *   System.out.println(formatted);\r
43  *   try {\r
44  *       // parse a string into time unit amount\r
45  *       TimeUnitAmount result = (TimeUnitAmount) format.parseObject(formatted);\r
46  *       // result should equal to source \r
47  *   } catch (ParseException e) {\r
48  *   }\r
49  * </pre>\r
50  *\r
51  * <P>\r
52  * @see TimeUnitAmount\r
53  * @see TimeUnitFormat\r
54  * @author markdavis\r
55  * @stable ICU 4.0\r
56  */\r
57 public class TimeUnitFormat extends MeasureFormat {\r
58 \r
59     /**\r
60      * Constant for full name style format. \r
61      * For example, the full name for "hour" in English is "hour" or "hours".\r
62      * @stable ICU 4.2\r
63      */\r
64     public static final int FULL_NAME = 0;\r
65     /**\r
66      * Constant for abbreviated name style format. \r
67      * For example, the abbreviated name for "hour" in English is "hr" or "hrs".\r
68      * @stable ICU 4.2\r
69      */\r
70     public static final int ABBREVIATED_NAME = 1;\r
71 \r
72     private static final int TOTAL_STYLES = 2;\r
73 \r
74     private static final long serialVersionUID = -3707773153184971529L;\r
75   \r
76     private static final String DEFAULT_PATTERN_FOR_SECOND = "{0} s";\r
77     private static final String DEFAULT_PATTERN_FOR_MINUTE = "{0} min";\r
78     private static final String DEFAULT_PATTERN_FOR_HOUR = "{0} h";\r
79     private static final String DEFAULT_PATTERN_FOR_DAY = "{0} d";\r
80     private static final String DEFAULT_PATTERN_FOR_WEEK = "{0} w";\r
81     private static final String DEFAULT_PATTERN_FOR_MONTH = "{0} m";\r
82     private static final String DEFAULT_PATTERN_FOR_YEAR = "{0} y";\r
83 \r
84     private NumberFormat format;\r
85     private ULocale locale;\r
86     private transient Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns;\r
87     private transient PluralRules pluralRules;\r
88     private transient boolean isReady;\r
89     private int style;\r
90 \r
91     /**\r
92      * Create empty format using full name style, for example, "hours". \r
93      * Use setLocale and/or setFormat to modify.\r
94      * @stable ICU 4.0\r
95      */\r
96     public TimeUnitFormat() {\r
97         isReady = false;\r
98         style = FULL_NAME;\r
99 \r
100     }\r
101 \r
102     /**\r
103      * Create TimeUnitFormat given a ULocale, and using full name style.\r
104      * @param locale   locale of this time unit formatter.\r
105      * @stable ICU 4.0\r
106      */\r
107     public TimeUnitFormat(ULocale locale) {\r
108         this(locale, FULL_NAME);\r
109     }\r
110 \r
111     /**\r
112      * Create TimeUnitFormat given a Locale, and using full name style.\r
113      * @param locale   locale of this time unit formatter.\r
114      * @stable ICU 4.0\r
115      */\r
116     public TimeUnitFormat(Locale locale) {\r
117         this(locale, FULL_NAME);\r
118     }\r
119 \r
120     /**\r
121      * Create TimeUnitFormat given a ULocale and a formatting style: full or\r
122      * abbreviated.\r
123      * @param locale   locale of this time unit formatter.\r
124      * @param style    format style, either FULL_NAME or ABBREVIATED_NAME style.\r
125      * @throws IllegalArgumentException if the style is not FULL_NAME or\r
126      *                                  ABBREVIATED_NAME style.\r
127      * @stable ICU 4.2\r
128      */\r
129     public TimeUnitFormat(ULocale locale, int style) {\r
130         if (style < FULL_NAME || style >= TOTAL_STYLES) {\r
131             throw new IllegalArgumentException("style should be either FULL_NAME or ABBREVIATED_NAME style");\r
132         }\r
133         this.style = style;\r
134         this.locale = locale;\r
135         isReady = false;\r
136     }\r
137 \r
138     /**\r
139      * Create TimeUnitFormat given a Locale and a formatting style: full or\r
140      * abbreviated.\r
141      * @stable ICU 4.2\r
142      */\r
143     public TimeUnitFormat(Locale locale, int style) {\r
144         this(ULocale.forLocale(locale),  style);\r
145     }\r
146 \r
147     /**\r
148      * Set the locale used for formatting or parsing.\r
149      * @param locale   locale of this time unit formatter.\r
150      * @return this, for chaining.\r
151      * @stable ICU 4.0\r
152      */\r
153     public TimeUnitFormat setLocale(ULocale locale) {\r
154         if ( locale != this.locale ) {\r
155             this.locale = locale;\r
156             isReady = false;\r
157         }\r
158         return this;\r
159     }\r
160     \r
161     /**\r
162      * Set the locale used for formatting or parsing.\r
163      * @param locale   locale of this time unit formatter.\r
164      * @return this, for chaining.\r
165      * @stable ICU 4.0\r
166      */\r
167     public TimeUnitFormat setLocale(Locale locale) {\r
168         return setLocale(ULocale.forLocale(locale));\r
169     }\r
170     \r
171     /**\r
172      * Set the format used for formatting or parsing. If null or not available, use the getNumberInstance(locale).\r
173      * @param format   the number formatter.\r
174      * @return this, for chaining.\r
175      * @stable ICU 4.0\r
176      */\r
177     public TimeUnitFormat setNumberFormat(NumberFormat format) {\r
178         if (format == this.format) {\r
179             return this;\r
180         }\r
181         if ( format == null ) {\r
182             if ( locale == null ) {\r
183                 isReady = false;\r
184                 return this;\r
185             } else {\r
186                 this.format = NumberFormat.getNumberInstance(locale);\r
187             }\r
188         } else {\r
189             this.format = format;\r
190         }\r
191         // reset the number formatter in the timeUnitToCountToPatterns map\r
192         if (isReady == false) {\r
193             return this;\r
194         }\r
195         for (TimeUnit timeUnit : timeUnitToCountToPatterns.keySet()) {\r
196             Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(timeUnit);\r
197             for (String count : countToPattern.keySet()) {\r
198                 Object[] pair = countToPattern.get(count);\r
199                 MessageFormat pattern = (MessageFormat)pair[FULL_NAME];\r
200                 pattern.setFormatByArgumentIndex(0, format);\r
201                 pattern = (MessageFormat)pair[ABBREVIATED_NAME];\r
202                 pattern.setFormatByArgumentIndex(0, format);\r
203             }\r
204         }\r
205         return this;\r
206     }\r
207 \r
208 \r
209     /**\r
210      * Format a TimeUnitAmount.\r
211      * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)\r
212      * @stable ICU 4.0\r
213      */\r
214     public StringBuffer format(Object obj, StringBuffer toAppendTo,\r
215             FieldPosition pos) {\r
216         if ( !(obj instanceof TimeUnitAmount) ) {\r
217             throw new IllegalArgumentException("can not format non TimeUnitAmount object");\r
218         }\r
219         if (!isReady) {\r
220             setup();\r
221         }\r
222         TimeUnitAmount amount = (TimeUnitAmount) obj;\r
223         Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(amount.getTimeUnit());\r
224         double number = amount.getNumber().doubleValue();\r
225         String count = pluralRules.select(number);\r
226         MessageFormat pattern = (MessageFormat)(countToPattern.get(count))[style];\r
227         return pattern.format(new Object[]{amount.getNumber()}, toAppendTo, pos);\r
228     }\r
229 \r
230 \r
231     /**\r
232      * Parse a TimeUnitAmount.\r
233      * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)\r
234      * @stable ICU 4.0\r
235      */\r
236     public Object parseObject(String source, ParsePosition pos) {\r
237         if (!isReady) {\r
238             setup();\r
239         }\r
240         Number resultNumber = null;\r
241         TimeUnit resultTimeUnit = null;\r
242         int oldPos = pos.getIndex();\r
243         int newPos = -1;\r
244         int longestParseDistance = 0;\r
245         String countOfLongestMatch = null;\r
246         // we don't worry too much about speed on parsing, but this can be optimized later if needed.\r
247         // Parse by iterating through all available patterns\r
248         // and looking for the longest match.\r
249         for (TimeUnit timeUnit : timeUnitToCountToPatterns.keySet()) {\r
250             Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(timeUnit);\r
251             for (String count : countToPattern.keySet()) {\r
252               for (int styl = FULL_NAME; styl < TOTAL_STYLES; ++styl) {\r
253                 MessageFormat pattern = (MessageFormat)(countToPattern.get(count))[styl];\r
254                 pos.setErrorIndex(-1);\r
255                 pos.setIndex(oldPos);\r
256                 // see if we can parse\r
257                 Object parsed = pattern.parseObject(source, pos);\r
258                 if ( pos.getErrorIndex() != -1 || pos.getIndex() == oldPos ) {\r
259                     // nothing parsed\r
260                     continue;\r
261                 }\r
262                 Number temp = null;\r
263                 if ( ((Object[])parsed).length != 0 ) {\r
264                     // pattern with Number as beginning,\r
265                     // such as "{0} d".\r
266                     // check to make sure that the timeUnit is consistent\r
267                     temp = (Number)((Object[])parsed)[0];\r
268                     String select = pluralRules.select(temp.doubleValue());\r
269                     if (!count.equals(select)) {\r
270                         continue;\r
271                     }\r
272                 }\r
273                 int parseDistance = pos.getIndex() - oldPos;\r
274                 if ( parseDistance > longestParseDistance ) {\r
275                     resultNumber = temp;\r
276                     resultTimeUnit = timeUnit;\r
277                     newPos = pos.getIndex();\r
278                     longestParseDistance = parseDistance;\r
279                     countOfLongestMatch = count;\r
280                 }\r
281             }\r
282           }\r
283         }\r
284         /* After find the longest match, parse the number.\r
285          * Result number could be null for the pattern without number pattern.\r
286          * such as unit pattern in Arabic.\r
287          * When result number is null, use plural rule to set the number.\r
288          */\r
289         if (resultNumber == null && longestParseDistance != 0) {\r
290             // set the number using plurrual count\r
291             if ( countOfLongestMatch.equals("zero") ) {\r
292                 resultNumber = new Integer(0);\r
293             } else if ( countOfLongestMatch.equals("one") ) {\r
294                 resultNumber = new Integer(1);\r
295             } else if ( countOfLongestMatch.equals("two") ) {\r
296                 resultNumber = new Integer(2);\r
297             } else {\r
298                 // should not happen.\r
299                 // TODO: how to handle?\r
300                 resultNumber = new Integer(3);\r
301             }\r
302         }\r
303         if (longestParseDistance == 0) {\r
304             pos.setIndex(oldPos);\r
305             pos.setErrorIndex(0);\r
306             return null;\r
307         } else {\r
308             pos.setIndex(newPos);\r
309             pos.setErrorIndex(-1);\r
310             return new TimeUnitAmount(resultNumber, resultTimeUnit);\r
311         }\r
312     }\r
313     \r
314     \r
315     /*\r
316      * Initialize locale, number formatter, plural rules, and\r
317      * time units patterns.\r
318      * Initially, we are storing all of these as MessageFormats.\r
319      * I think it might actually be simpler to make them Decimal Formats later.\r
320      */\r
321     private void setup() {\r
322         if (locale == null) {\r
323             if (format != null) {\r
324                 locale = format.getLocale(null);\r
325             } else {\r
326                 locale = ULocale.getDefault();\r
327             }\r
328         }\r
329         if (format == null) {\r
330             format = NumberFormat.getNumberInstance(locale);\r
331         }\r
332         pluralRules = PluralRules.forLocale(locale);\r
333         timeUnitToCountToPatterns = new HashMap<TimeUnit, Map<String, Object[]>>();\r
334 \r
335         setup("units", timeUnitToCountToPatterns, FULL_NAME);\r
336         setup("unitsShort", timeUnitToCountToPatterns, ABBREVIATED_NAME);\r
337         isReady = true;\r
338     }\r
339 \r
340 \r
341     private void setup(String resourceKey, Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns,\r
342                        int style) {\r
343         // fill timeUnitToCountToPatterns from resource file\r
344         try {\r
345             ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);\r
346             ICUResourceBundle unitsRes = resource.getWithFallback(resourceKey);\r
347             int size = unitsRes.getSize();\r
348             for ( int index = 0; index < size; ++index) {\r
349                 String timeUnitName = unitsRes.get(index).getKey();\r
350                 TimeUnit timeUnit = null;\r
351                 if ( timeUnitName.equals("year") ) {\r
352                     timeUnit = TimeUnit.YEAR;\r
353                 } else if ( timeUnitName.equals("month") ) {\r
354                     timeUnit = TimeUnit.MONTH;\r
355                 } else if ( timeUnitName.equals("day") ) {\r
356                     timeUnit = TimeUnit.DAY;\r
357                 } else if ( timeUnitName.equals("hour") ) {\r
358                     timeUnit = TimeUnit.HOUR;\r
359                 } else if ( timeUnitName.equals("minute") ) {\r
360                     timeUnit = TimeUnit.MINUTE;\r
361                 } else if ( timeUnitName.equals("second") ) {\r
362                     timeUnit = TimeUnit.SECOND;\r
363                 } else if ( timeUnitName.equals("week") ) {\r
364                     timeUnit = TimeUnit.WEEK;\r
365                 } else {\r
366                     continue;\r
367                 }\r
368                 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(timeUnitName);\r
369                 int count = oneUnitRes.getSize();\r
370                 Map<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit);\r
371                 if (countToPatterns ==  null) {\r
372                     countToPatterns = new TreeMap<String, Object[]>();\r
373                     timeUnitToCountToPatterns.put(timeUnit, countToPatterns);\r
374                 } \r
375                 for ( int pluralIndex = 0; pluralIndex < count; ++pluralIndex) {\r
376                     String pluralCount = oneUnitRes.get(pluralIndex).getKey();\r
377                     String pattern = oneUnitRes.get(pluralIndex).getString();\r
378                     final MessageFormat messageFormat = new MessageFormat(pattern, locale);\r
379                     if (format != null) {\r
380                         messageFormat.setFormatByArgumentIndex(0, format);\r
381                     }\r
382                     // save both full name and abbreviated name in one table\r
383                     // is good space-wise, but it degrades performance, \r
384                     // since it needs to check whether the needed space \r
385                     // is already allocated or not.\r
386                     Object[] pair = countToPatterns.get(pluralCount);\r
387                     if (pair == null) {\r
388                         pair = new Object[2];\r
389                         countToPatterns.put(pluralCount, pair);\r
390                     } \r
391                     pair[style] = messageFormat;\r
392                 }\r
393             }\r
394         } catch ( MissingResourceException e ) {\r
395         }\r
396 \r
397         // there should be patterns for each plural rule in each time unit.\r
398         // For each time unit, \r
399         //     for each plural rule, following is unit pattern fall-back rule:\r
400         //         ( for example: "one" hour )\r
401         //         look for its unit pattern in its locale tree.\r
402         //         if pattern is not found in its own locale, such as de_DE,\r
403         //         look for the pattern in its parent, such as de,\r
404         //         keep looking till found or till root.\r
405         //         if the pattern is not found in root either,\r
406         //         fallback to plural count "other",\r
407         //         look for the pattern of "other" in the locale tree:\r
408         //         "de_DE" to "de" to "root".\r
409         //         If not found, fall back to value of \r
410         //         static variable DEFAULT_PATTERN_FOR_xxx, such as "{0} h". \r
411         //\r
412         // Following is consistency check to create pattern for each\r
413         // plural rule in each time unit using above fall-back rule.\r
414         //\r
415         final TimeUnit[] timeUnits = TimeUnit.values();\r
416         Set<String> keywords = pluralRules.getKeywords();\r
417         for ( int i = 0; i < timeUnits.length; ++i ) {\r
418             // for each time unit, \r
419             // get all the patterns for each plural rule in this locale.\r
420             final TimeUnit timeUnit = timeUnits[i];\r
421             Map<String, Object[]> countToPatterns = timeUnitToCountToPatterns.get(timeUnit);\r
422             if (countToPatterns == null) {\r
423                 countToPatterns = new TreeMap<String, Object[]>();\r
424                 timeUnitToCountToPatterns.put(timeUnit, countToPatterns);\r
425             }\r
426             for (String pluralCount : keywords) {\r
427                 if ( countToPatterns.get(pluralCount) == null ||\r
428                      countToPatterns.get(pluralCount)[style] == null ) {\r
429                     // look through parents\r
430                     searchInTree(resourceKey, style, timeUnit, pluralCount, pluralCount, countToPatterns);\r
431                 }\r
432             }\r
433         }\r
434     }\r
435 \r
436 \r
437 \r
438     // srcPluralCount is the original plural count on which the pattern is\r
439     // searched for.\r
440     // searchPluralCount is the fallback plural count.\r
441     // For example, to search for pattern for ""one" hour",\r
442     // "one" is the srcPluralCount,\r
443     // if the pattern is not found even in root, fallback to \r
444     // using patterns of plural count "other", \r
445     // then, "other" is the searchPluralCount.\r
446     private void searchInTree(String resourceKey, int styl,\r
447                               TimeUnit timeUnit, String srcPluralCount,\r
448                               String searchPluralCount, Map<String, Object[]> countToPatterns) {\r
449         ULocale parentLocale=locale;\r
450         String srcTimeUnitName = timeUnit.toString();\r
451         while ( parentLocale != null ) {\r
452             try {\r
453                 // look for pattern for srcPluralCount in locale tree\r
454                 ICUResourceBundle unitsRes = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, parentLocale);\r
455                 unitsRes = unitsRes.getWithFallback(resourceKey);\r
456                 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(srcTimeUnitName);\r
457                 String pattern = oneUnitRes.getStringWithFallback(searchPluralCount);\r
458                 final MessageFormat messageFormat = new MessageFormat(pattern, locale);\r
459                 if (format != null) {\r
460                     messageFormat.setFormatByArgumentIndex(0, format);\r
461                 }\r
462                 Object[] pair = countToPatterns.get(srcPluralCount);\r
463                 if (pair == null) {\r
464                     pair = new Object[2];\r
465                     countToPatterns.put(srcPluralCount, pair);\r
466                 }\r
467                 pair[styl] = messageFormat;\r
468                 return;\r
469             } catch ( MissingResourceException e ) {\r
470             }\r
471             parentLocale=parentLocale.getFallback();\r
472         }\r
473         // if not found the pattern for this plural count at all,\r
474         // fall-back to plural count "other"\r
475         if ( searchPluralCount.equals("other") ) {\r
476             // set default fall back the same as the resource in root\r
477             MessageFormat messageFormat = null;\r
478             if ( timeUnit == TimeUnit.SECOND ) {\r
479                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_SECOND, locale);\r
480             } else if ( timeUnit == TimeUnit.MINUTE ) {\r
481                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MINUTE, locale);\r
482             } else if ( timeUnit == TimeUnit.HOUR ) {\r
483                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_HOUR, locale);\r
484             } else if ( timeUnit == TimeUnit.WEEK ) {\r
485                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_WEEK, locale);\r
486             } else if ( timeUnit == TimeUnit.DAY ) {\r
487                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_DAY, locale);\r
488             } else if ( timeUnit == TimeUnit.MONTH ) {\r
489                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_MONTH, locale);\r
490             } else if ( timeUnit == TimeUnit.YEAR ) {\r
491                 messageFormat = new MessageFormat(DEFAULT_PATTERN_FOR_YEAR, locale);\r
492             }\r
493             if (format != null && messageFormat != null) {\r
494                 messageFormat.setFormatByArgumentIndex(0, format);\r
495             }\r
496             Object[] pair = countToPatterns.get(srcPluralCount);\r
497             if (pair == null) {\r
498                 pair = new Object[2];\r
499                 countToPatterns.put(srcPluralCount, pair);\r
500             }\r
501             pair[styl] = messageFormat;\r
502         } else {\r
503             // fall back to rule "other", and search in parents\r
504             searchInTree(resourceKey, styl, timeUnit, srcPluralCount, "other", countToPatterns);\r
505         }\r
506     }\r
507 }\r