ICU-10603 Add NUMERIC to MeasureFormat.FormatWidth.

X-SVN-Rev: 34797
This commit is contained in:
Travis Keep 2013-12-18 22:32:17 +00:00
parent ef9fbd093c
commit 652d2952a2
2 changed files with 232 additions and 13 deletions

View file

@ -16,9 +16,11 @@ import java.io.InvalidObjectException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Collection;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
@ -31,6 +33,7 @@ import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import com.ibm.icu.util.UResourceBundle;
@ -104,9 +107,23 @@ public class MeasureFormat extends UFormat {
// Measure unit -> format width -> plural form -> pattern ("{0} meters")
private final transient Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat;
private final transient NumericFormatters numericFormatters;
static final SimpleCache<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>> localeToUnitToStyleToCountToFormat
private static final SimpleCache<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>> localeToUnitToStyleToCountToFormat
= new SimpleCache<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>>>();
private static final SimpleCache<ULocale, NumericFormatters> localeToNumericDurationFormatters
= new SimpleCache<ULocale,NumericFormatters>();
private static final Map<MeasureUnit, Integer> hmsTo012 =
new HashMap<MeasureUnit, Integer>();
static {
hmsTo012.put(MeasureUnit.HOUR, 0);
hmsTo012.put(MeasureUnit.MINUTE, 1);
hmsTo012.put(MeasureUnit.SECOND, 2);
}
// For serialization: sub-class types.
private static final int MEASURE_FORMAT = 0;
@ -145,7 +162,17 @@ public class MeasureFormat extends UFormat {
* @draft ICU 53
* @provisional
*/
NARROW("unitsNarrow");
NARROW("unitsNarrow"),
/**
* Identical to NARROW except when formatMeasures is called with
* an hour and minute; minute and second; or hour, minute, and second Measures.
* In these cases formatMeasures formats as 5:37:23 instead of 5h, 37m, 23s.
*
* @draft ICU 53
* @provisional
*/
NUMERIC("unitsNarrow");
// Be sure to update the toFormatWidth and fromFormatWidth() functions
// when adding an enum value.
@ -182,14 +209,27 @@ public class MeasureFormat extends UFormat {
*/
public static MeasureFormat getInstance(ULocale locale, FormatWidth width, NumberFormat format) {
PluralRules rules = PluralRules.forLocale(locale);
Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat;
Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat;
NumericFormatters formatters = null;
unitToStyleToCountToFormat = localeToUnitToStyleToCountToFormat.get(locale);
if (unitToStyleToCountToFormat == null) {
unitToStyleToCountToFormat = loadLocaleData(locale, rules);
localeToUnitToStyleToCountToFormat.put(locale, unitToStyleToCountToFormat);
}
if (width == FormatWidth.NUMERIC) {
formatters = localeToNumericDurationFormatters.get(locale);
if (formatters == null) {
formatters = loadNumericFormatters(locale);
localeToNumericDurationFormatters.put(locale, formatters);
}
}
return new MeasureFormat(
locale, width, new ImmutableNumberFormat(format), rules, unitToStyleToCountToFormat);
locale,
width,
new ImmutableNumberFormat(format),
rules,
unitToStyleToCountToFormat,
formatters);
}
/**
@ -278,7 +318,6 @@ public class MeasureFormat extends UFormat {
@SuppressWarnings("unchecked")
public <T extends Appendable> T formatMeasures(
T appendable, FieldPosition fieldPosition, Measure... measures) {
// fast track for trivial cases
if (measures.length == 0) {
return (T) appendable;
@ -287,6 +326,15 @@ public class MeasureFormat extends UFormat {
return formatMeasure(measures[0], appendable, fieldPosition);
}
if (length == FormatWidth.NUMERIC) {
// If we have just hour, minute, or second follow the numeric
// track.
Number[] hms = toHMS(measures);
if (hms != null) {
return formatNumeric(hms, appendable);
}
}
// Zero out our field position so that we can tell when we find our field.
FieldPosition fpos = new FieldPosition(fieldPosition.getFieldAttribute(), fieldPosition.getField());
FieldPosition dummyPos = new FieldPosition(0);
@ -421,7 +469,8 @@ public class MeasureFormat extends UFormat {
this.length,
new ImmutableNumberFormat(format),
this.rules,
this.unitToStyleToCountToFormat);
this.unitToStyleToCountToFormat,
this.numericFormatters);
}
private MeasureFormat(
@ -429,12 +478,14 @@ public class MeasureFormat extends UFormat {
FormatWidth width,
ImmutableNumberFormat format,
PluralRules rules,
Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat) {
Map<MeasureUnit, EnumMap<FormatWidth, Map<String, PatternData>>> unitToStyleToCountToFormat,
NumericFormatters formatters) {
setLocale(locale, locale);
this.length = width;
this.numberFormat = format;
this.rules = rules;
this.unitToStyleToCountToFormat = unitToStyleToCountToFormat;
this.numericFormatters = formatters;
}
/**
@ -448,7 +499,36 @@ public class MeasureFormat extends UFormat {
this.numberFormat = null;
this.rules = null;
this.unitToStyleToCountToFormat = null;
this.numericFormatters = null;
}
static class NumericFormatters {
private DateFormat hourMinute;
private DateFormat minuteSecond;
private DateFormat hourMinuteSecond;
public NumericFormatters(
DateFormat hourMinute,
DateFormat minuteSecond,
DateFormat hourMinuteSecond) {
this.hourMinute = hourMinute;
this.minuteSecond = minuteSecond;
this.hourMinuteSecond = hourMinuteSecond;
}
public DateFormat getHourMinute() { return hourMinute; }
public DateFormat getMinuteSecond() { return minuteSecond; }
public DateFormat getHourMinuteSecond() { return hourMinuteSecond; }
}
private static NumericFormatters loadNumericFormatters(
ULocale locale) {
ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);
return new NumericFormatters(
loadNumericDurationFormat(r, "hm"),
loadNumericDurationFormat(r, "ms"),
loadNumericDurationFormat(r, "hms"));
}
/**
@ -576,6 +656,10 @@ public class MeasureFormat extends UFormat {
Number n, StringBuffer buffer, FieldPosition pos) {
return nf.format(n, buffer, pos);
}
public synchronized String format(Number number) {
return nf.format(number);
}
}
static final class PatternData {
@ -605,6 +689,113 @@ public class MeasureFormat extends UFormat {
return new MeasureProxy(getLocale(), length, numberFormat.get(), CURRENCY_FORMAT);
}
// type is one of "hm", "ms" or "hms"
private static DateFormat loadNumericDurationFormat(
ICUResourceBundle r, String type) {
r = r.getWithFallback(String.format("durationUnits/%s", type));
// We replace 'h' with 'H' because 'h' does not make sense in the context of durations.
DateFormat result = new SimpleDateFormat(r.getString().replace("h", "H"));
result.setTimeZone(TimeZone.GMT_ZONE);
return result;
}
private static Number[] toHMS(Measure[] measures) {
Number[] result = new Number[3];
int count = 0;
for (Measure m : measures) {
Integer idx = hmsTo012.get(m.getUnit());
if (idx == null) {
return null;
}
if (result[idx.intValue()] != null) {
return null;
}
result[idx.intValue()] = m.getNumber();
count++;
}
if (count < 2) {
return null;
}
return result;
}
private <T extends Appendable> T formatNumeric(Number[] hms, T appendable) {
int startIndex = -1;
int endIndex = -1;
for (int i = 0; i < hms.length; i++) {
if (hms[i] != null) {
endIndex = i;
if (startIndex == -1) {
startIndex = endIndex;
}
} else {
hms[i] = Integer.valueOf(0);
}
}
long millis = (long) (((hms[0].doubleValue() * 60.0
+ hms[1].doubleValue()) * 60.0
+ hms[2].doubleValue()) * 1000.0);
Date d = new Date(millis);
if (startIndex == 0 && endIndex == 2) {
return formatNumeric(
d,
numericFormatters.getHourMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
appendable);
}
if (startIndex == 1 && endIndex == 2) {
return formatNumeric(
d,
numericFormatters.getMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
appendable);
}
if (startIndex == 0 && endIndex == 1) {
return formatNumeric(
d,
numericFormatters.getHourMinute(),
DateFormat.Field.MINUTE,
hms[endIndex],
appendable);
}
throw new IllegalStateException();
}
private <T extends Appendable> T formatNumeric(
Date duration,
DateFormat formatter,
DateFormat.Field smallestField,
Number smallestAmount,
T appendable) {
// Format the smallest amount ahead of time.
String smallestAmountFormatted;
smallestAmountFormatted = numberFormat.format(smallestAmount);
// Format the duration using the provided DateFormat object. The smallest
// field in this result will be missing the fractional part.
AttributedCharacterIterator iterator = formatter.formatToCharacterIterator(duration);
// iterate through formatted text copying to 'builder' one character at a time.
// When we get to the smallest amount, skip over it and copy
// 'smallestAmountFormatted' to the builder instead.
for (iterator.first(); iterator.getIndex() < iterator.getEndIndex();) {
try {
if (iterator.getAttributes().containsKey(smallestField)) {
appendable.append(smallestAmountFormatted);
iterator.setIndex(iterator.getRunLimit(smallestField));
} else {
appendable.append(iterator.current());
iterator.next();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return appendable;
}
private Object writeReplace() throws ObjectStreamException {
return new MeasureProxy(
getLocale(), length, numberFormat.get(), MEASURE_FORMAT);
@ -699,6 +890,8 @@ public class MeasureFormat extends UFormat {
return FormatWidth.SHORT;
case 2:
return FormatWidth.NARROW;
case 3:
return FormatWidth.NUMERIC;
default:
return FormatWidth.WIDE;
}
@ -712,6 +905,8 @@ public class MeasureFormat extends UFormat {
return 1;
case NARROW:
return 2;
case NUMERIC:
return 3;
default:
throw new IllegalStateException("Unable to serialize Format Width " + fw);
}

View file

@ -70,6 +70,22 @@ public class MeasureUnitTest extends TestFmwk {
TimeUnitAmount[] _1m_59_9996s = {
new TimeUnitAmount(1.0, TimeUnit.MINUTE),
new TimeUnitAmount(59.9996, TimeUnit.SECOND)};
TimeUnitAmount[] _5h_17m = {
new TimeUnitAmount(5.0, TimeUnit.HOUR),
new TimeUnitAmount(17.0, TimeUnit.MINUTE)};
TimeUnitAmount[] _19m_28s = {
new TimeUnitAmount(19.0, TimeUnit.MINUTE),
new TimeUnitAmount(28.0, TimeUnit.SECOND)};
TimeUnitAmount[] _0h_0m_17s = {
new TimeUnitAmount(0.0, TimeUnit.HOUR),
new TimeUnitAmount(0.0, TimeUnit.MINUTE),
new TimeUnitAmount(17.0, TimeUnit.SECOND)};
TimeUnitAmount[] _6h_56_92m = {
new TimeUnitAmount(6.0, TimeUnit.HOUR),
new TimeUnitAmount(56.92, TimeUnit.MINUTE)};
TimeUnitAmount[] _3h_5h = {
new TimeUnitAmount(3.0, TimeUnit.HOUR),
new TimeUnitAmount(5.0, TimeUnit.HOUR)};
Object[][] fullData = {
{_1m_59_9996s, "1 minute, 59.9996 seconds"},
@ -85,21 +101,27 @@ public class MeasureUnitTest extends TestFmwk {
{_1h_23_5m, "1 hr, 23.5 mins"},
{_1h_0m_23s, "1 hr, 0 mins, 23 secs"},
{_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, 4 days"}};
Object[][] narrowData = {
{_1m_59_9996s, "1 min, 59.9996 secs"},
{_19m, "19 mins"},
{_1h_23_5s, "1 hr, 23.5 secs"},
{_1h_23_5m, "1 hr, 23.5 mins"},
{_1h_0m_23s, "1 hr, 0 mins, 23 secs"},
{_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, 4 days"}};
// TODO(Travis Keep): We need to support numeric formatting. Either here or in TimeUnitFormat.
/*
Object[][] numericData = {
{_1m_59_9996s, "1:59.9996"},
{_19m, "19 mins"},
{_19m, "19m"},
{_1h_23_5s, "1:00:23.5"},
{_1h_0m_23s, "1:00:23"},
{_1h_23_5m, "1:23.5"},
{_5h_17m, "5:17"},
{_19m_28s, "19:28"},
{_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, 4 days"},
{_2y_5M_3w_4d, "2y, 5m, 3w, 4d"},
{_0h_0m_17s, "0:00:17"},
{_6h_56_92m, "6:56.92"}};
*/
{_6h_56_92m, "6:56.92"},
{_3h_5h, "3h, 5h"}};
NumberFormat nf = NumberFormat.getNumberInstance(ULocale.ENGLISH);
nf.setMaximumFractionDigits(4);
@ -107,6 +129,8 @@ public class MeasureUnitTest extends TestFmwk {
verifyFormatPeriod("en FULL", mf, fullData);
mf = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.SHORT, nf);
verifyFormatPeriod("en SHORT", mf, abbrevData);
mf = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.NUMERIC, nf);
verifyFormatPeriod("en NUMERIC", mf, numericData);
}