| /* |
| ******************************************************************************* |
| * Copyright (C) 2013, Google Inc, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.text; |
| |
| import java.io.Externalizable; |
| import java.io.IOException; |
| import java.io.ObjectInput; |
| import java.io.ObjectOutput; |
| import java.io.ObjectStreamException; |
| import java.text.FieldPosition; |
| import java.text.ParsePosition; |
| import java.util.ArrayList; |
| import java.util.BitSet; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.EnumMap; |
| import java.util.HashMap; |
| import java.util.List; |
| 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.FormatWidth; |
| import com.ibm.icu.util.Measure; |
| import com.ibm.icu.util.MeasureUnit; |
| import com.ibm.icu.util.ULocale; |
| import com.ibm.icu.util.ULocale.Category; |
| import com.ibm.icu.util.UResourceBundle; |
| |
| /** |
| * Mutable class for formatting GeneralMeasures, or sequences of them. |
| * @author markdavis |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public class GeneralMeasureFormat extends MeasureFormat { |
| |
| // Cache the data for units so we don't have to look it up each time. |
| // For each format, we'll store a pointer into the EnumMap for quick access. |
| // TODO use the data to allow parsing. |
| static final transient Map<ULocale,ParseData> localeToParseData = new HashMap<ULocale,ParseData>(); |
| static final transient Map<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>> localeToUnitToStyleToCountToFormat |
| = new HashMap<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>>(); |
| static final transient Index<MeasureUnit> index = new Index<MeasureUnit>(); |
| |
| static final class PatternData { |
| final String prefix; |
| final String suffix; |
| public PatternData(String pattern) { |
| int pos = pattern.indexOf("{0}"); |
| if (pos < 0) { |
| prefix = pattern; |
| suffix = null; |
| } else { |
| prefix = pattern.substring(0,pos); |
| suffix = pattern.substring(pos+3); |
| } |
| } |
| public String toString() { |
| return prefix + "; " + suffix; |
| } |
| |
| } |
| private final ULocale locale; |
| private final FormatWidth length; |
| private final NumberFormat numberFormat; |
| |
| private final transient PluralRules rules; |
| private final transient Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat; // invariant once built |
| private transient ParseData parseData; // set as needed |
| |
| |
| private static final long serialVersionUID = 7922671801770278517L; |
| |
| /** |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| protected GeneralMeasureFormat(ULocale locale, FormatWidth style, |
| Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat, |
| NumberFormat numberFormat) { |
| this.locale = locale; |
| this.length = style; |
| this.unitToStyleToCountToFormat = unitToStyleToCountToFormat; |
| rules = PluralRules.forLocale(locale); |
| this.numberFormat = numberFormat; |
| } |
| |
| |
| /** |
| * Create a format from the locale and length |
| * @param locale locale of this time unit formatter. |
| * @param length the desired length |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public static GeneralMeasureFormat getInstance(ULocale locale, FormatWidth length) { |
| return getInstance(locale, length, NumberFormat.getInstance(locale)); |
| } |
| |
| /** |
| * Create a format from the locale and length |
| * @param locale locale of this time unit formatter. |
| * @param length the desired length |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public static GeneralMeasureFormat getInstance(ULocale locale, FormatWidth length, |
| NumberFormat decimalFormat) { |
| synchronized (localeToUnitToStyleToCountToFormat) { |
| Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat |
| = localeToUnitToStyleToCountToFormat.get(locale); |
| if (unitToStyleToCountToFormat == null) { |
| unitToStyleToCountToFormat = cacheLocaleData(locale); |
| } |
| // System.out.println(styleToCountToFormat); |
| return new GeneralMeasureFormat(locale, length, unitToStyleToCountToFormat, decimalFormat); |
| } |
| } |
| |
| /** |
| * Return a formatter for CurrencyAmount objects in the given |
| * locale. |
| * @param locale desired locale |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public static MeasureFormat getCurrencyFormat(ULocale locale) { |
| return new CurrencyFormat(locale); |
| } |
| |
| /** |
| * Return a formatter for CurrencyAmount objects in the default |
| * <code>FORMAT</code> locale. |
| * @return a formatter object |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public static MeasureFormat getCurrencyFormat() { |
| return getCurrencyFormat(ULocale.getDefault(Category.FORMAT)); |
| } |
| |
| /** |
| * @return the locale of the format. |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public ULocale getLocale() { |
| return locale; |
| } |
| |
| /** |
| * @return the desired length for the format |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public FormatWidth getLength() { |
| return length; |
| } |
| |
| private static Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> cacheLocaleData(ULocale locale) { |
| PluralRules rules = PluralRules.forLocale(locale); |
| Set<String> keywords = rules.getKeywords(); |
| Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat; |
| localeToUnitToStyleToCountToFormat.put(locale, unitToStyleToCountToFormat |
| = new HashMap<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>()); |
| for (MeasureUnit unit : MeasureUnit.getAvailable()) { |
| EnumMap<FormatWidth, Map<String, PatternData>> styleToCountToFormat = unitToStyleToCountToFormat.get(unit); |
| if (styleToCountToFormat == null) { |
| unitToStyleToCountToFormat.put(unit, styleToCountToFormat = new EnumMap<FormatWidth, Map<String, PatternData>>(FormatWidth.class)); |
| } |
| ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale); |
| for (FormatWidth styleItem : FormatWidth.values()) { |
| try { |
| ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey); |
| ICUResourceBundle unitsRes = unitTypeRes.getWithFallback(unit.getType()); |
| ICUResourceBundle oneUnitRes = unitsRes.getWithFallback(unit.getCode()); |
| Map<String, PatternData> countToFormat = styleToCountToFormat.get(styleItem); |
| if (countToFormat == null) { |
| styleToCountToFormat.put(styleItem, countToFormat = new HashMap<String, PatternData>()); |
| } |
| for (String keyword : keywords) { |
| UResourceBundle countBundle; |
| try { |
| countBundle = oneUnitRes.get(keyword); |
| } catch (MissingResourceException e) { |
| continue; |
| } |
| String pattern = countBundle.getString(); |
| // System.out.println(styleItem.resourceKey + "/" |
| // + unit.getType() + "/" |
| // + unit.getCode() + "/" |
| // + keyword + "=" + pattern); |
| PatternData format = new PatternData(pattern); |
| countToFormat.put(keyword, format); |
| // System.out.println(styleToCountToFormat); |
| } |
| // fill in 'other' for any missing values |
| PatternData other = countToFormat.get("other"); |
| for (String keyword : keywords) { |
| if (!countToFormat.containsKey(keyword)) { |
| countToFormat.put(keyword, other); |
| } |
| } |
| } catch (MissingResourceException e) { |
| continue; |
| } |
| } |
| // now fill in the holes |
| fillin: |
| if (styleToCountToFormat.size() != FormatWidth.values().length) { |
| Map<String, PatternData> fallback = styleToCountToFormat.get(FormatWidth.SHORT); |
| if (fallback == null) { |
| fallback = styleToCountToFormat.get(FormatWidth.WIDE); |
| } |
| if (fallback == null) { |
| break fillin; // TODO use root |
| } |
| for (FormatWidth styleItem : FormatWidth.values()) { |
| Map<String, PatternData> countToFormat = styleToCountToFormat.get(styleItem); |
| if (countToFormat == null) { |
| styleToCountToFormat.put(styleItem, countToFormat = new HashMap<String, PatternData>()); |
| for (Entry<String, PatternData> entry : fallback.entrySet()) { |
| countToFormat.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| } |
| } |
| return unitToStyleToCountToFormat; |
| } |
| |
| /** |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { |
| if (obj instanceof Collection) { |
| Collection<Measure> coll = (Collection<Measure>) obj; |
| return format(toAppendTo, pos, coll.toArray(new Measure[coll.size()])); |
| } else if (obj instanceof Measure[]) { |
| return format(toAppendTo, pos, (Measure[]) obj); |
| } else { |
| return format((Measure) obj, toAppendTo, pos); |
| } |
| } |
| |
| /** |
| * Format a general measure (type-safe). |
| * @param measure the measure to format |
| * @param toAppendTo as in {@link #format(Object, StringBuffer, FieldPosition)} |
| * @param pos as in {@link #format(Object, StringBuffer, FieldPosition)} |
| * @return passed-in buffer with appended text. |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public StringBuffer format(Measure measure, StringBuffer toAppendTo, FieldPosition pos) { |
| Number n = measure.getNumber(); |
| MeasureUnit unit = measure.getUnit(); |
| UFieldPosition fpos = new UFieldPosition(pos.getFieldAttribute(), pos.getField()); |
| StringBuffer formattedNumber = numberFormat.format(n, new StringBuffer(), fpos); |
| String keyword = rules.select(new PluralRules.FixedDecimal(n.doubleValue(), fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits())); |
| |
| Map<FormatWidth, Map<String, PatternData>> styleToCountToFormat = unitToStyleToCountToFormat.get(unit); |
| Map<String, PatternData> countToFormat = styleToCountToFormat.get(length); |
| PatternData messagePatternData = countToFormat.get(keyword); |
| |
| toAppendTo.append(messagePatternData.prefix); |
| if (messagePatternData.suffix != null) { // there is a number (may not happen with, say, Arabic dual) |
| // Fix field position |
| pos.setBeginIndex(fpos.getBeginIndex() + messagePatternData.prefix.length()); |
| pos.setEndIndex(fpos.getEndIndex() + messagePatternData.prefix.length()); |
| toAppendTo.append(formattedNumber); |
| toAppendTo.append(messagePatternData.suffix); |
| } |
| return toAppendTo; |
| } |
| |
| |
| /** |
| * Format a sequence of measures. |
| * @param toAppendto as in {@link #format(Object, StringBuffer, FieldPosition)} |
| * @param pos as in {@link #format(Object, StringBuffer, FieldPosition)} |
| * @param measures a sequence of one or more measures. |
| * @return passed-in buffer with appended text. |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public StringBuffer format(StringBuffer toAppendto, FieldPosition pos, Measure... measures) { |
| StringBuffer[] results = new StringBuffer[measures.length]; |
| for (int i = 0; i < measures.length; ++i) { |
| results[i] = format(measures[i], new StringBuffer(), pos); |
| } |
| ListFormatter listFormatter = ListFormatter.getInstance(locale, |
| length == FormatWidth.WIDE ? ListFormatter.Style.DURATION : ListFormatter.Style.DURATION_SHORT); |
| return toAppendto.append(listFormatter.format((Object[]) results)); |
| } |
| |
| /** |
| * Format a sequence of measures. |
| * @param measures a sequence of one or more measures. |
| * @return passed-in buffer with appended text. |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| public String format(Measure... measures) { |
| StringBuffer result = format(new StringBuffer(), new FieldPosition(0), measures); |
| return result.toString(); |
| } |
| |
| static final class ParseData { |
| transient Map<String,BitSet> prefixMap; |
| transient Map<String,BitSet> suffixMap; |
| transient BitSet nullSuffix; |
| |
| ParseData(ULocale locale, Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat) { |
| prefixMap = new TreeMap<String,BitSet>(LONGEST_FIRST); |
| suffixMap = new TreeMap<String,BitSet>(LONGEST_FIRST); |
| nullSuffix = new BitSet(); |
| for (Entry<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> entry3 : unitToStyleToCountToFormat.entrySet()) { |
| MeasureUnit unit = entry3.getKey(); |
| int unitIndex = index.addItem(unit); |
| for (Entry<FormatWidth, Map<String, PatternData>> entry : entry3.getValue().entrySet()) { |
| //Style style = entry.getKey(); |
| for (Entry<String, PatternData> entry2 : entry.getValue().entrySet()) { |
| //String keyword = entry2.getKey(); |
| PatternData data = entry2.getValue(); |
| setBits(prefixMap, data.prefix, unitIndex); |
| if (data.suffix == null) { |
| nullSuffix.set(unitIndex); |
| } else { |
| setBits(suffixMap, data.suffix, unitIndex); |
| } |
| } |
| } |
| } |
| } |
| private void setBits(Map<String, BitSet> map, String string, int unitIndex) { |
| BitSet bs = map.get(string); |
| if (bs == null) { |
| map.put(string, bs = new BitSet()); |
| } |
| bs.set(unitIndex); |
| } |
| public static ParseData of(ULocale locale, |
| Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat) { |
| ParseData result = localeToParseData.get(locale); |
| if (result == null) { |
| localeToParseData.put(locale, result = new ParseData(locale, unitToStyleToCountToFormat)); |
| // System.out.println("Prefix:\t" + result.prefixMap.size()); |
| // System.out.println("Suffix:\t" + result.suffixMap.size()); |
| } |
| return result; |
| } |
| |
| private Measure parse(NumberFormat numberFormat, String toParse, ParsePosition parsePosition) { |
| // TODO optimize this as necessary |
| // In particular, if we've already matched a suffix and number, store that. |
| // If the same suffix turns up we can jump |
| int startIndex = parsePosition.getIndex(); |
| Number bestNumber = null; |
| int bestUnit = -1; |
| int longestMatch = -1; |
| int furthestError = -1; |
| for (Entry<String, BitSet> prefixEntry : prefixMap.entrySet()) { |
| String prefix = prefixEntry.getKey(); |
| BitSet prefixSet = prefixEntry.getValue(); |
| for (Entry<String, BitSet> suffixEntry : suffixMap.entrySet()) { |
| String suffix = suffixEntry.getKey(); |
| BitSet suffixSet = suffixEntry.getValue(); |
| parsePosition.setIndex(startIndex); |
| if (looseMatches(prefix, toParse, parsePosition)) { |
| // if (nullSuffix.intersects(prefixSet)) |
| //// // can only happen with singular rule |
| //// if (longestMatch < parsePosition.getIndex()) { |
| //// longestMatch = parsePosition.getIndex(); |
| //// Collection<Double> samples = rules.getSamples(keyword); |
| //// bestNumber = samples.iterator().next(); |
| //// bestUnit = unit; |
| //// } |
| // } |
| Number number = numberFormat.parse(toParse, parsePosition); |
| if (parsePosition.getErrorIndex() >= 0) { |
| if (furthestError < parsePosition.getErrorIndex()) { |
| furthestError = parsePosition.getErrorIndex(); |
| } |
| continue; |
| } |
| if (looseMatches(suffix, toParse, parsePosition) && prefixSet.intersects(suffixSet)) { |
| if (longestMatch < parsePosition.getIndex()) { |
| longestMatch = parsePosition.getIndex(); |
| bestNumber = number; |
| bestUnit = getFirst(prefixSet, suffixSet); |
| } |
| } else if (furthestError < parsePosition.getErrorIndex()) { |
| furthestError = parsePosition.getErrorIndex(); |
| } |
| } else if (furthestError < parsePosition.getErrorIndex()) { |
| furthestError = parsePosition.getErrorIndex(); |
| } |
| |
| } |
| } |
| if (longestMatch >= 0) { |
| parsePosition.setIndex(longestMatch); |
| return new Measure(bestNumber, index.getUnit(bestUnit)); |
| } |
| parsePosition.setErrorIndex(furthestError); |
| return null; |
| } |
| } |
| |
| static class Index<T> { |
| List<T> intToItem = new ArrayList<T>(); |
| Map<T,Integer> itemToInt = new HashMap<T,Integer>(); |
| |
| int getIndex(T item) { |
| return itemToInt.get(item); |
| } |
| T getUnit(int index) { |
| return intToItem.get(index); |
| } |
| int addItem(T item) { |
| Integer index = itemToInt.get(item); |
| if (index != null) { |
| return index; |
| } |
| int size = intToItem.size(); |
| itemToInt.put(item, size); |
| intToItem.add(item); |
| return size; |
| } |
| } |
| |
| /** |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| @Override |
| public Measure parseObject(String toParse, ParsePosition parsePosition) { |
| if (parseData == null) { |
| parseData = ParseData.of(locale, unitToStyleToCountToFormat); |
| } |
| // int index = parsePosition.getIndex(); |
| // int errorIndex = parsePosition.getIndex(); |
| Measure result = parseData.parse(numberFormat, toParse, parsePosition); |
| // if (result == null) { |
| // parsePosition.setIndex(index); |
| // parsePosition.setErrorIndex(errorIndex); |
| // result = compatCurrencyFormat.parseCurrency(toParse, parsePosition); |
| // } |
| return result; |
| } |
| |
| |
| /** |
| * @param prefixSet |
| * @param suffixSet |
| * @return |
| */ |
| private static int getFirst(BitSet prefixSet, BitSet suffixSet) { |
| for (int i = prefixSet.nextSetBit(0); i >= 0; i = prefixSet.nextSetBit(i+1)) { |
| if (suffixSet.get(i)) { |
| return i; |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * @param suffix |
| * @param arg0 |
| * @param arg1 |
| * @return |
| */ |
| // TODO make this lenient |
| private static boolean looseMatches(String suffix, String arg0, ParsePosition arg1) { |
| boolean matches = suffix.regionMatches(0, arg0, arg1.getIndex(), suffix.length()); |
| if (matches) { |
| arg1.setErrorIndex(-1); |
| arg1.setIndex(arg1.getIndex() + suffix.length()); |
| } else { |
| arg1.setErrorIndex(arg1.getIndex()); |
| } |
| return matches; |
| } |
| |
| static final Comparator<String> LONGEST_FIRST = new Comparator<String>() { |
| public int compare(String as, String bs) { |
| if (as.length() > bs.length()) { |
| return -1; |
| } |
| if (as.length() < bs.length()) { |
| return 1; |
| } |
| return as.compareTo(bs); |
| } |
| }; |
| |
| /** |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == null || obj.getClass() != GeneralMeasureFormat.class) { |
| return false; |
| } |
| GeneralMeasureFormat other = (GeneralMeasureFormat) obj; |
| return locale.equals(other.locale) |
| && length == other.length |
| && numberFormat.equals(other.numberFormat); |
| } |
| |
| /** |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| @Override |
| public int hashCode() { |
| // TODO Auto-generated method stub |
| return (locale.hashCode() * 37 + length.hashCode()) * 37 + numberFormat.hashCode(); |
| } |
| |
| private Object writeReplace() throws ObjectStreamException { |
| return new GeneralMeasureProxy(locale, length, numberFormat); |
| } |
| |
| static class GeneralMeasureProxy implements Externalizable { |
| private static final long serialVersionUID = -6033308329886716770L; |
| |
| private ULocale locale; |
| private FormatWidth length; |
| private NumberFormat numberFormat; |
| |
| public GeneralMeasureProxy(ULocale locale, FormatWidth length, NumberFormat numberFormat) { |
| this.locale = locale; |
| this.length = length; |
| this.numberFormat = numberFormat; |
| } |
| |
| // Must have public constructor, to enable Externalizable |
| public GeneralMeasureProxy() { |
| } |
| |
| public void writeExternal(ObjectOutput out) throws IOException { |
| out.writeByte(0); // version |
| out.writeObject(locale); |
| out.writeObject(length); |
| out.writeObject(numberFormat); |
| out.writeShort(0); // allow for more data. |
| } |
| |
| public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { |
| /* byte version = */ in.readByte(); // version |
| locale = (ULocale) in.readObject(); |
| length = (FormatWidth) in.readObject(); |
| numberFormat = (NumberFormat) in.readObject(); |
| // allow for more data from future version |
| int extra = in.readShort(); |
| if (extra > 0) { |
| byte[] extraBytes = new byte[extra]; |
| in.read(extraBytes, 0, extra); |
| } |
| } |
| |
| private Object readResolve() throws ObjectStreamException { |
| return GeneralMeasureFormat.getInstance(locale, length, numberFormat); |
| } |
| } |
| } |