From 652d2952a2e125a2530a26d2efec7bfb820fefce Mon Sep 17 00:00:00 2001 From: Travis Keep Date: Wed, 18 Dec 2013 22:32:17 +0000 Subject: [PATCH] ICU-10603 Add NUMERIC to MeasureFormat.FormatWidth. X-SVN-Rev: 34797 --- .../src/com/ibm/icu/text/MeasureFormat.java | 209 +++++++++++++++++- .../icu/dev/test/format/MeasureUnitTest.java | 36 ++- 2 files changed, 232 insertions(+), 13 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java index 63e9d2444a7..d1ea18545d1 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java @@ -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>> unitToStyleToCountToFormat; + + private final transient NumericFormatters numericFormatters; - static final SimpleCache>>> localeToUnitToStyleToCountToFormat + private static final SimpleCache>>> localeToUnitToStyleToCountToFormat = new SimpleCache>>>(); + + private static final SimpleCache localeToNumericDurationFormatters + = new SimpleCache(); + + private static final Map hmsTo012 = + new HashMap(); + + 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>> unitToStyleToCountToFormat; + Map>> 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 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>> unitToStyleToCountToFormat) { + Map>> 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 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 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); } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java index 35d4d28353b..2d85fa0940c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java @@ -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); }