blob: 8855ec87a300ea6189737c760d66cff081cb1139 [file] [log] [blame]
/*
*******************************************************************************
* 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);
}
}
}