2 *******************************************************************************
3 * Copyright (C) 2013, Google Inc, International Business Machines Corporation and *
4 * others. All Rights Reserved. *
5 *******************************************************************************
7 package com.ibm.icu.text;
9 import java.io.Externalizable;
10 import java.io.IOException;
11 import java.io.ObjectInput;
12 import java.io.ObjectOutput;
13 import java.io.ObjectStreamException;
14 import java.text.FieldPosition;
15 import java.text.ParsePosition;
16 import java.util.ArrayList;
17 import java.util.BitSet;
18 import java.util.Collection;
19 import java.util.Comparator;
20 import java.util.EnumMap;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.Map.Entry;
25 import java.util.MissingResourceException;
27 import java.util.TreeMap;
29 import com.ibm.icu.impl.ICUResourceBundle;
30 import com.ibm.icu.util.FormatWidth;
31 import com.ibm.icu.util.Measure;
32 import com.ibm.icu.util.MeasureUnit;
33 import com.ibm.icu.util.ULocale;
34 import com.ibm.icu.util.ULocale.Category;
35 import com.ibm.icu.util.UResourceBundle;
38 * Mutable class for formatting GeneralMeasures, or sequences of them.
41 * @deprecated This API is ICU internal only.
43 public class GeneralMeasureFormat extends MeasureFormat {
45 // Cache the data for units so we don't have to look it up each time.
46 // For each format, we'll store a pointer into the EnumMap for quick access.
47 // TODO use the data to allow parsing.
48 static final transient Map<ULocale,ParseData> localeToParseData = new HashMap<ULocale,ParseData>();
49 static final transient Map<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>> localeToUnitToStyleToCountToFormat
50 = new HashMap<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>>();
51 static final transient Index<MeasureUnit> index = new Index<MeasureUnit>();
53 static final class PatternData {
56 public PatternData(String pattern) {
57 int pos = pattern.indexOf("{0}");
62 prefix = pattern.substring(0,pos);
63 suffix = pattern.substring(pos+3);
66 public String toString() {
67 return prefix + "; " + suffix;
71 private final ULocale locale;
72 private final FormatWidth length;
73 private final NumberFormat numberFormat;
75 private final transient PluralRules rules;
76 private final transient Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat; // invariant once built
77 private transient ParseData parseData; // set as needed
80 private static final long serialVersionUID = 7922671801770278517L;
84 * @deprecated This API is ICU internal only.
86 protected GeneralMeasureFormat(ULocale locale, FormatWidth style,
87 Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat,
88 NumberFormat numberFormat) {
91 this.unitToStyleToCountToFormat = unitToStyleToCountToFormat;
92 rules = PluralRules.forLocale(locale);
93 this.numberFormat = numberFormat;
98 * Create a format from the locale and length
99 * @param locale locale of this time unit formatter.
100 * @param length the desired length
102 * @deprecated This API is ICU internal only.
104 public static GeneralMeasureFormat getInstance(ULocale locale, FormatWidth length) {
105 return getInstance(locale, length, NumberFormat.getInstance(locale));
109 * Create a format from the locale and length
110 * @param locale locale of this time unit formatter.
111 * @param length the desired length
113 * @deprecated This API is ICU internal only.
115 public static GeneralMeasureFormat getInstance(ULocale locale, FormatWidth length,
116 NumberFormat decimalFormat) {
117 synchronized (localeToUnitToStyleToCountToFormat) {
118 Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat
119 = localeToUnitToStyleToCountToFormat.get(locale);
120 if (unitToStyleToCountToFormat == null) {
121 unitToStyleToCountToFormat = cacheLocaleData(locale);
123 // System.out.println(styleToCountToFormat);
124 return new GeneralMeasureFormat(locale, length, unitToStyleToCountToFormat, decimalFormat);
129 * Return a formatter for CurrencyAmount objects in the given
131 * @param locale desired locale
133 * @deprecated This API is ICU internal only.
135 public static MeasureFormat getCurrencyFormat(ULocale locale) {
136 return new CurrencyFormat(locale);
140 * Return a formatter for CurrencyAmount objects in the default
141 * <code>FORMAT</code> locale.
142 * @return a formatter object
144 * @deprecated This API is ICU internal only.
146 public static MeasureFormat getCurrencyFormat() {
147 return getCurrencyFormat(ULocale.getDefault(Category.FORMAT));
151 * @return the locale of the format.
153 * @deprecated This API is ICU internal only.
155 public ULocale getLocale() {
160 * @return the desired length for the format
162 * @deprecated This API is ICU internal only.
164 public FormatWidth getLength() {
168 private static Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> cacheLocaleData(ULocale locale) {
169 PluralRules rules = PluralRules.forLocale(locale);
170 Set<String> keywords = rules.getKeywords();
171 Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat;
172 localeToUnitToStyleToCountToFormat.put(locale, unitToStyleToCountToFormat
173 = new HashMap<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>());
174 for (MeasureUnit unit : MeasureUnit.getAvailable()) {
175 EnumMap<FormatWidth, Map<String, PatternData>> styleToCountToFormat = unitToStyleToCountToFormat.get(unit);
176 if (styleToCountToFormat == null) {
177 unitToStyleToCountToFormat.put(unit, styleToCountToFormat = new EnumMap<FormatWidth, Map<String, PatternData>>(FormatWidth.class));
179 ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);
180 for (FormatWidth styleItem : FormatWidth.values()) {
182 ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey);
183 ICUResourceBundle unitsRes = unitTypeRes.getWithFallback(unit.getType());
184 ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(unit.getCode());
185 Map<String, PatternData> countToFormat = styleToCountToFormat.get(styleItem);
186 if (countToFormat == null) {
187 styleToCountToFormat.put(styleItem, countToFormat = new HashMap<String, PatternData>());
189 for (String keyword : keywords) {
190 UResourceBundle countBundle;
192 countBundle = oneUnitRes.get(keyword);
193 } catch (MissingResourceException e) {
196 String pattern = countBundle.getString();
197 // System.out.println(styleItem.resourceKey + "/"
198 // + unit.getType() + "/"
199 // + unit.getCode() + "/"
200 // + keyword + "=" + pattern);
201 PatternData format = new PatternData(pattern);
202 countToFormat.put(keyword, format);
203 // System.out.println(styleToCountToFormat);
205 // fill in 'other' for any missing values
206 PatternData other = countToFormat.get("other");
207 for (String keyword : keywords) {
208 if (!countToFormat.containsKey(keyword)) {
209 countToFormat.put(keyword, other);
212 } catch (MissingResourceException e) {
216 // now fill in the holes
218 if (styleToCountToFormat.size() != FormatWidth.values().length) {
219 Map<String, PatternData> fallback = styleToCountToFormat.get(FormatWidth.SHORT);
220 if (fallback == null) {
221 fallback = styleToCountToFormat.get(FormatWidth.WIDE);
223 if (fallback == null) {
224 break fillin; // TODO use root
226 for (FormatWidth styleItem : FormatWidth.values()) {
227 Map<String, PatternData> countToFormat = styleToCountToFormat.get(styleItem);
228 if (countToFormat == null) {
229 styleToCountToFormat.put(styleItem, countToFormat = new HashMap<String, PatternData>());
230 for (Entry<String, PatternData> entry : fallback.entrySet()) {
231 countToFormat.put(entry.getKey(), entry.getValue());
237 return unitToStyleToCountToFormat;
242 * @deprecated This API is ICU internal only.
244 @SuppressWarnings("unchecked")
246 public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
247 if (obj instanceof Collection) {
248 Collection<Measure> coll = (Collection<Measure>) obj;
249 return format(toAppendTo, pos, coll.toArray(new Measure[coll.size()]));
250 } else if (obj instanceof Measure[]) {
251 return format(toAppendTo, pos, (Measure[]) obj);
253 return format((Measure) obj, toAppendTo, pos);
258 * Format a general measure (type-safe).
259 * @param measure the measure to format
260 * @param toAppendTo as in {@link #format(Object, StringBuffer, FieldPosition)}
261 * @param pos as in {@link #format(Object, StringBuffer, FieldPosition)}
262 * @return passed-in buffer with appended text.
264 * @deprecated This API is ICU internal only.
266 public StringBuffer format(Measure measure, StringBuffer toAppendTo, FieldPosition pos) {
267 Number n = measure.getNumber();
268 MeasureUnit unit = measure.getUnit();
269 UFieldPosition fpos = new UFieldPosition(pos.getFieldAttribute(), pos.getField());
270 StringBuffer formattedNumber = numberFormat.format(n, new StringBuffer(), fpos);
271 String keyword = rules.select(new PluralRules.FixedDecimal(n.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits()));
273 Map<FormatWidth, Map<String, PatternData>> styleToCountToFormat = unitToStyleToCountToFormat.get(unit);
274 Map<String, PatternData> countToFormat = styleToCountToFormat.get(length);
275 PatternData messagePatternData = countToFormat.get(keyword);
277 toAppendTo.append(messagePatternData.prefix);
278 if (messagePatternData.suffix != null) { // there is a number (may not happen with, say, Arabic dual)
279 // Fix field position
280 pos.setBeginIndex(fpos.getBeginIndex() + messagePatternData.prefix.length());
281 pos.setEndIndex(fpos.getEndIndex() + messagePatternData.prefix.length());
282 toAppendTo.append(formattedNumber);
283 toAppendTo.append(messagePatternData.suffix);
290 * Format a sequence of measures.
291 * @param toAppendto as in {@link #format(Object, StringBuffer, FieldPosition)}
292 * @param pos as in {@link #format(Object, StringBuffer, FieldPosition)}
293 * @param measures a sequence of one or more measures.
294 * @return passed-in buffer with appended text.
296 * @deprecated This API is ICU internal only.
298 public StringBuffer format(StringBuffer toAppendto, FieldPosition pos, Measure... measures) {
299 StringBuffer[] results = new StringBuffer[measures.length];
300 for (int i = 0; i < measures.length; ++i) {
301 results[i] = format(measures[i], new StringBuffer(), pos);
303 ListFormatter listFormatter = ListFormatter.getInstance(locale,
304 length == FormatWidth.WIDE ? ListFormatter.Style.DURATION : ListFormatter.Style.DURATION_SHORT);
305 return toAppendto.append(listFormatter.format((Object[]) results));
309 * Format a sequence of measures.
310 * @param measures a sequence of one or more measures.
311 * @return passed-in buffer with appended text.
313 * @deprecated This API is ICU internal only.
315 public String format(Measure... measures) {
316 StringBuffer result = format(new StringBuffer(), new FieldPosition(0), measures);
317 return result.toString();
320 static final class ParseData {
321 transient Map<String,BitSet> prefixMap;
322 transient Map<String,BitSet> suffixMap;
323 transient BitSet nullSuffix;
325 ParseData(ULocale locale, Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat) {
326 prefixMap = new TreeMap<String,BitSet>(LONGEST_FIRST);
327 suffixMap = new TreeMap<String,BitSet>(LONGEST_FIRST);
328 nullSuffix = new BitSet();
329 for (Entry<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> entry3 : unitToStyleToCountToFormat.entrySet()) {
330 MeasureUnit unit = entry3.getKey();
331 int unitIndex = index.addItem(unit);
332 for (Entry<FormatWidth, Map<String, PatternData>> entry : entry3.getValue().entrySet()) {
333 //Style style = entry.getKey();
334 for (Entry<String, PatternData> entry2 : entry.getValue().entrySet()) {
335 //String keyword = entry2.getKey();
336 PatternData data = entry2.getValue();
337 setBits(prefixMap, data.prefix, unitIndex);
338 if (data.suffix == null) {
339 nullSuffix.set(unitIndex);
341 setBits(suffixMap, data.suffix, unitIndex);
347 private void setBits(Map<String, BitSet> map, String string, int unitIndex) {
348 BitSet bs = map.get(string);
350 map.put(string, bs = new BitSet());
354 public static synchronized ParseData of(ULocale locale,
355 Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat) {
356 ParseData result = localeToParseData.get(locale);
357 if (result == null) {
358 localeToParseData.put(locale, result = new ParseData(locale, unitToStyleToCountToFormat));
359 // System.out.println("Prefix:\t" + result.prefixMap.size());
360 // System.out.println("Suffix:\t" + result.suffixMap.size());
365 private Measure parse(NumberFormat numberFormat, String toParse, ParsePosition parsePosition) {
366 // TODO optimize this as necessary
367 // In particular, if we've already matched a suffix and number, store that.
368 // If the same suffix turns up we can jump
369 int startIndex = parsePosition.getIndex();
370 Number bestNumber = null;
372 int longestMatch = -1;
373 int furthestError = -1;
374 for (Entry<String, BitSet> prefixEntry : prefixMap.entrySet()) {
375 String prefix = prefixEntry.getKey();
376 BitSet prefixSet = prefixEntry.getValue();
377 for (Entry<String, BitSet> suffixEntry : suffixMap.entrySet()) {
378 String suffix = suffixEntry.getKey();
379 BitSet suffixSet = suffixEntry.getValue();
380 parsePosition.setIndex(startIndex);
381 if (looseMatches(prefix, toParse, parsePosition)) {
382 // if (nullSuffix.intersects(prefixSet))
383 //// // can only happen with singular rule
384 //// if (longestMatch < parsePosition.getIndex()) {
385 //// longestMatch = parsePosition.getIndex();
386 //// Collection<Double> samples = rules.getSamples(keyword);
387 //// bestNumber = samples.iterator().next();
388 //// bestUnit = unit;
391 Number number = numberFormat.parse(toParse, parsePosition);
392 if (parsePosition.getErrorIndex() >= 0) {
393 if (furthestError < parsePosition.getErrorIndex()) {
394 furthestError = parsePosition.getErrorIndex();
398 if (looseMatches(suffix, toParse, parsePosition) && prefixSet.intersects(suffixSet)) {
399 if (longestMatch < parsePosition.getIndex()) {
400 longestMatch = parsePosition.getIndex();
402 bestUnit = getFirst(prefixSet, suffixSet);
404 } else if (furthestError < parsePosition.getErrorIndex()) {
405 furthestError = parsePosition.getErrorIndex();
407 } else if (furthestError < parsePosition.getErrorIndex()) {
408 furthestError = parsePosition.getErrorIndex();
413 if (longestMatch >= 0) {
414 parsePosition.setIndex(longestMatch);
415 return new Measure(bestNumber, index.getUnit(bestUnit));
417 parsePosition.setErrorIndex(furthestError);
422 static class Index<T> {
423 List<T> intToItem = new ArrayList<T>();
424 Map<T,Integer> itemToInt = new HashMap<T,Integer>();
426 int getIndex(T item) {
427 return itemToInt.get(item);
429 T getUnit(int index) {
430 return intToItem.get(index);
432 int addItem(T item) {
433 Integer index = itemToInt.get(item);
437 int size = intToItem.size();
438 itemToInt.put(item, size);
446 * @deprecated This API is ICU internal only.
449 public Measure parseObject(String toParse, ParsePosition parsePosition) {
450 if (parseData == null) {
451 parseData = ParseData.of(locale, unitToStyleToCountToFormat);
453 // int index = parsePosition.getIndex();
454 // int errorIndex = parsePosition.getIndex();
455 Measure result = parseData.parse(numberFormat, toParse, parsePosition);
456 // if (result == null) {
457 // parsePosition.setIndex(index);
458 // parsePosition.setErrorIndex(errorIndex);
459 // result = compatCurrencyFormat.parseCurrency(toParse, parsePosition);
470 private static int getFirst(BitSet prefixSet, BitSet suffixSet) {
471 for (int i = prefixSet.nextSetBit(0); i >= 0; i = prefixSet.nextSetBit(i+1)) {
472 if (suffixSet.get(i)) {
485 // TODO make this lenient
486 private static boolean looseMatches(String suffix, String arg0, ParsePosition arg1) {
487 boolean matches = suffix.regionMatches(0, arg0, arg1.getIndex(), suffix.length());
489 arg1.setErrorIndex(-1);
490 arg1.setIndex(arg1.getIndex() + suffix.length());
492 arg1.setErrorIndex(arg1.getIndex());
497 static final Comparator<String> LONGEST_FIRST = new Comparator<String>() {
498 public int compare(String as, String bs) {
499 if (as.length() > bs.length()) {
502 if (as.length() < bs.length()) {
505 return as.compareTo(bs);
511 * @deprecated This API is ICU internal only.
514 public boolean equals(Object obj) {
515 if (obj == null || obj.getClass() != GeneralMeasureFormat.class) {
518 GeneralMeasureFormat other = (GeneralMeasureFormat) obj;
519 return locale.equals(other.locale)
520 && length == other.length
521 && numberFormat.equals(other.numberFormat);
526 * @deprecated This API is ICU internal only.
529 public int hashCode() {
530 // TODO Auto-generated method stub
531 return (locale.hashCode() * 37 + length.hashCode()) * 37 + numberFormat.hashCode();
534 private Object writeReplace() throws ObjectStreamException {
535 return new GeneralMeasureProxy(locale, length, numberFormat);
538 static class GeneralMeasureProxy implements Externalizable {
539 private static final long serialVersionUID = -6033308329886716770L;
541 private ULocale locale;
542 private FormatWidth length;
543 private NumberFormat numberFormat;
545 public GeneralMeasureProxy(ULocale locale, FormatWidth length, NumberFormat numberFormat) {
546 this.locale = locale;
547 this.length = length;
548 this.numberFormat = numberFormat;
551 // Must have public constructor, to enable Externalizable
552 public GeneralMeasureProxy() {
555 public void writeExternal(ObjectOutput out) throws IOException {
556 out.writeByte(0); // version
557 out.writeObject(locale);
558 out.writeObject(length);
559 out.writeObject(numberFormat);
560 out.writeShort(0); // allow for more data.
563 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
564 /* byte version = */ in.readByte(); // version
565 locale = (ULocale) in.readObject();
566 length = (FormatWidth) in.readObject();
567 numberFormat = (NumberFormat) in.readObject();
568 // allow for more data from future version
569 int extra = in.readShort();
571 byte[] extraBytes = new byte[extra];
572 in.read(extraBytes, 0, extra);
576 private Object readResolve() throws ObjectStreamException {
577 return GeneralMeasureFormat.getInstance(locale, length, numberFormat);