]> granicus.if.org Git - icu/commitdiff
ICU-10999 Add JAVA version of per unit measure formatting.
authorTravis Keep <keep94@gmail.com>
Thu, 11 Sep 2014 17:06:37 +0000 (17:06 +0000)
committerTravis Keep <keep94@gmail.com>
Thu, 11 Sep 2014 17:06:37 +0000 (17:06 +0000)
X-SVN-Rev: 36464

icu4j/main/classes/core/src/com/ibm/icu/impl/SimplePatternFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java

index f64943788b52f19edbf28c4f2e8a875411334dbb..ed9ae65769c269a3f19c984ff99683caa36e732e 100644 (file)
@@ -283,4 +283,11 @@ public class SimplePatternFormatter {
             return result;
         }
     }
+
+    /**
+     * Returns this pattern with none of the placeholders.
+     */
+    public String getPatternWithNoPlaceholders() {
+        return patternWithoutPlaceholders;
+    }
 }
index f4bd59dc8a7f1236413f5c6bfe8d3aed7a7a36cc..3e119c6c6d455acafaf4ba944bfb6e78d90458b3 100644 (file)
@@ -129,8 +129,12 @@ public class MeasureFormat extends UFormat {
 
     private final transient ImmutableNumberFormat integerFormat;
 
-    private static final SimpleCache<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>>> localeToUnitToStyleToCountToFormat
-    = new SimpleCache<ULocale,Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>>>();
+    private final transient Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern;
+    
+    private final transient EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern;
+
+    private static final SimpleCache<ULocale, MeasureFormatData> localeMeasureFormatData
+    = new SimpleCache<ULocale, MeasureFormatData>();
 
     private static final SimpleCache<ULocale, NumericFormatters> localeToNumericDurationFormatters
     = new SimpleCache<ULocale,NumericFormatters>();
@@ -253,12 +257,11 @@ public class MeasureFormat extends UFormat {
      */
     public static MeasureFormat getInstance(ULocale locale, FormatWidth formatWidth, NumberFormat format) {
         PluralRules rules = PluralRules.forLocale(locale);
-        Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat;
         NumericFormatters formatters = null;
-        unitToStyleToCountToFormat = localeToUnitToStyleToCountToFormat.get(locale);
-        if (unitToStyleToCountToFormat == null) {
-            unitToStyleToCountToFormat = loadLocaleData(locale, rules);
-            localeToUnitToStyleToCountToFormat.put(locale, unitToStyleToCountToFormat);
+        MeasureFormatData data = localeMeasureFormatData.get(locale);
+        if (data == null) {
+            data = loadLocaleData(locale);
+            localeMeasureFormatData.put(locale, data);
         }
         if (formatWidth == FormatWidth.NUMERIC) {
             formatters = localeToNumericDurationFormatters.get(locale);
@@ -276,10 +279,12 @@ public class MeasureFormat extends UFormat {
                 formatWidth,
                 new ImmutableNumberFormat(format),
                 rules,
-                unitToStyleToCountToFormat,
+                data.unitToStyleToCountToFormat,
                 formatters,
                 new ImmutableNumberFormat(NumberFormat.getInstance(locale, formatWidth.getCurrencyStyle())),
-                new ImmutableNumberFormat(intFormat));
+                new ImmutableNumberFormat(intFormat),
+                data.unitToStyleToPerUnitPattern,
+                data.styleToPerPattern);
     }
 
     /**
@@ -491,6 +496,35 @@ public class MeasureFormat extends UFormat {
             result.append(affix.substring(pos+replacement.length()));
         }
     }
+    
+    /**
+     * Like formatMeasures but formats with a per unit.
+     * 
+     * Will format to a string such as "5 kilometers, 300 meters per hour."
+     * 
+     * @param appendTo the formatted string appended here.
+     * @param fieldPosition Identifies a field in the formatted text.
+     * @param perUnit for the example above would be MeasureUnit.HOUR.
+     * @param measures the measures to format.
+     * @return appendTo.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public StringBuilder formatMeasuresPer(
+            StringBuilder appendTo, FieldPosition fieldPosition, MeasureUnit perUnit, Measure... measures) {
+        FieldPosition fpos = new FieldPosition(
+                fieldPosition.getFieldAttribute(), fieldPosition.getField());
+        int offset = withPerUnit(
+                formatMeasures(new StringBuilder(), fpos, measures),
+                perUnit,
+                appendTo);
+        if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
+            fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset);
+            fieldPosition.setEndIndex(fpos.getEndIndex() + offset);
+        }
+        return appendTo;
+    }
 
     /**
      * Formats a sequence of measures.
@@ -650,7 +684,9 @@ public class MeasureFormat extends UFormat {
                 this.unitToStyleToCountToFormat,
                 this.numericFormatters,
                 this.currencyFormat,
-                this.integerFormat);
+                this.integerFormat,
+                this.unitToStyleToPerUnitPattern,
+                this.styleToPerPattern);
     }
 
     private MeasureFormat(
@@ -661,7 +697,9 @@ public class MeasureFormat extends UFormat {
             Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat,
             NumericFormatters formatters,
             ImmutableNumberFormat currencyFormat,
-            ImmutableNumberFormat integerFormat) {
+            ImmutableNumberFormat integerFormat,
+            Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern,
+            EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern) {
         setLocale(locale, locale);
         this.formatWidth = formatWidth;
         this.numberFormat = format;
@@ -670,6 +708,8 @@ public class MeasureFormat extends UFormat {
         this.numericFormatters = formatters;
         this.currencyFormat = currencyFormat;
         this.integerFormat = integerFormat;
+        this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern;
+        this.styleToPerPattern = styleToPerPattern;
     }
 
     MeasureFormat() {
@@ -681,6 +721,8 @@ public class MeasureFormat extends UFormat {
         this.numericFormatters = null;
         this.currencyFormat = null;
         this.integerFormat = null;
+        this.unitToStyleToPerUnitPattern = null;
+        this.styleToPerPattern = null;
     }
 
     static class NumericFormatters {
@@ -715,12 +757,27 @@ public class MeasureFormat extends UFormat {
     /**
      * Returns formatting data for all MeasureUnits except for currency ones.
      */
-    private static Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> loadLocaleData(
-            ULocale locale, PluralRules rules) {
+    private static MeasureFormatData loadLocaleData(
+            ULocale locale) {
         QuantityFormatter.Builder builder = new QuantityFormatter.Builder();
         Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat
         = new HashMap<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>>();
+        Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern
+        = new HashMap<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>>();
         ICUResourceBundle resource = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
+        EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern = new EnumMap<FormatWidth, SimplePatternFormatter>(FormatWidth.class);
+        for (FormatWidth styleItem : FormatWidth.values()) {
+            try {
+                ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey);
+                ICUResourceBundle compoundRes = unitTypeRes.getWithFallback("compound");
+                ICUResourceBundle perRes = compoundRes.getWithFallback("per");
+                styleToPerPattern.put(styleItem, SimplePatternFormatter.compile(perRes.getString()));
+            } catch (MissingResourceException e) {
+                // may not have compound/per for every width.
+                continue;
+            }
+        }
+        fillInStyleMap(styleToPerPattern);
         for (MeasureUnit unit : MeasureUnit.getAvailable()) {
             // Currency data cannot be found here. Skip.
             if (unit instanceof Currency) {
@@ -730,6 +787,8 @@ public class MeasureFormat extends UFormat {
             if (styleToCountToFormat == null) {
                 unitToStyleToCountToFormat.put(unit, styleToCountToFormat = new EnumMap<FormatWidth, QuantityFormatter>(FormatWidth.class));
             }
+            EnumMap<FormatWidth, SimplePatternFormatter> styleToPerUnitPattern = new EnumMap<FormatWidth, SimplePatternFormatter>(FormatWidth.class);
+            unitToStyleToPerUnitPattern.put(unit, styleToPerUnitPattern);
             for (FormatWidth styleItem : FormatWidth.values()) {
                 try {
                     ICUResourceBundle unitTypeRes = resource.getWithFallback(styleItem.resourceKey);
@@ -746,9 +805,14 @@ public class MeasureFormat extends UFormat {
                             continue;
                         }
                         String resKey = countBundle.getKey();
-                        if (resKey.equals("dnam") || resKey.equals("per")) {
+                        if (resKey.equals("dnam")) {
                             continue; // skip display name & per pattern (new in CLDR 26 / ICU 54) for now, not part of plurals
                         }
+                        if (resKey.equals("per")) {
+                            styleToPerUnitPattern.put(
+                                    styleItem, SimplePatternFormatter.compile(countBundle.getString()));
+                            continue;
+                        }
                         havePluralItem = true;
                         builder.add(resKey, countBundle.getString());
                     }
@@ -761,25 +825,48 @@ public class MeasureFormat extends UFormat {
                     continue;
                 }
             }
-            // now fill in the holes
-            fillin:
-                if (styleToCountToFormat.size() != FormatWidth.values().length) {
-                    QuantityFormatter 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()) {
-                        QuantityFormatter countToFormat = styleToCountToFormat.get(styleItem);
-                        if (countToFormat == null) {
-                            styleToCountToFormat.put(styleItem, fallback);
-                        }
-                    }
-                }
+            // TODO: if no fallback available, get from root.
+            fillInStyleMap(styleToCountToFormat);
+            fillInStyleMap(styleToPerUnitPattern);
+        }
+        return new MeasureFormatData(unitToStyleToCountToFormat, unitToStyleToPerUnitPattern, styleToPerPattern);
+    }
+    
+    private static <T> boolean fillInStyleMap(Map<FormatWidth, T> styleMap) {
+        if (styleMap.size() == FormatWidth.values().length) {
+            return true;
+        }
+        T fallback = styleMap.get(FormatWidth.SHORT);
+        if (fallback == null) {
+            fallback = styleMap.get(FormatWidth.WIDE);
         }
-        return unitToStyleToCountToFormat;
+        if (fallback == null) {
+            return false;
+        }
+        for (FormatWidth styleItem : FormatWidth.values()) {
+            T item = styleMap.get(styleItem);
+            if (item == null) {
+                styleMap.put(styleItem, fallback);
+            }
+        }
+        return true;
+    }
+    
+    private int withPerUnit(CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) {
+        int[] offsets = new int[1];
+        Map<FormatWidth, SimplePatternFormatter> styleToPerUnitPattern =
+                unitToStyleToPerUnitPattern.get(perUnit);
+        SimplePatternFormatter perUnitPattern = styleToPerUnitPattern.get(formatWidth);
+        if (perUnitPattern != null) {
+            perUnitPattern.format(appendTo, offsets, formatted);
+            return offsets[0];
+        }
+        SimplePatternFormatter perPattern = styleToPerPattern.get(formatWidth);
+        Map<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(perUnit);
+        QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth);
+        String perUnitString = countToFormat.getByVariant("one").getPatternWithNoPlaceholders().trim();
+        perPattern.format(appendTo, offsets, formatted, perUnitString);
+        return offsets[0];
     }
 
     private String formatMeasure(Measure measure, ImmutableNumberFormat nf) {
@@ -821,6 +908,20 @@ public class MeasureFormat extends UFormat {
         }
         return appendTo;
     }
+    
+    private static final class MeasureFormatData {
+        MeasureFormatData(
+                Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat,
+                Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern,
+                EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern) {
+            this.unitToStyleToCountToFormat = unitToStyleToCountToFormat;
+            this.unitToStyleToPerUnitPattern = unitToStyleToPerUnitPattern;
+            this.styleToPerPattern = styleToPerPattern;
+        }
+        final Map<MeasureUnit, EnumMap<FormatWidth, QuantityFormatter>> unitToStyleToCountToFormat;
+        final Map<MeasureUnit, EnumMap<FormatWidth, SimplePatternFormatter>> unitToStyleToPerUnitPattern;
+        final EnumMap<FormatWidth, SimplePatternFormatter> styleToPerPattern;
+    }
 
     // Wrapper around NumberFormat that provides immutability and thread-safety.
     private static final class ImmutableNumberFormat {
index 7760b3d56248565c09e6442c9605dd895d196ec1..e81ed587ae66984dbf3f76e3ae576562025c0832 100644 (file)
@@ -25,6 +25,7 @@ import java.util.TreeMap;
 
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.dev.test.serializable.SerializableTest;
+import com.ibm.icu.impl.DontCareFieldPosition;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.math.BigDecimal;
 import com.ibm.icu.text.MeasureFormat;
@@ -741,6 +742,31 @@ public class MeasureUnitTest extends TestFmwk {
                             new Measure(2.3, MeasureUnit.INCH)));
         }
     }
+    
+    public void testMultiplesPer() {
+        Object[][] data = new Object[][] {
+                {ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.SECOND, "2 miles, 1 foot, 2.3 inches per second"},
+                {ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.SECOND, "2 mi, 1 ft, 2.3 inps"},
+                {ULocale.ENGLISH, FormatWidth.NARROW, MeasureUnit.SECOND, "2mi 1\u2032 2.3\u2033/s"},
+                {ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.MINUTE, "2 miles, 1 foot, 2.3 inches per minute"},
+                {ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.MINUTE, "2 mi, 1 ft, 2.3 in/min"},
+                {ULocale.ENGLISH, FormatWidth.NARROW, MeasureUnit.MINUTE, "2mi 1\u2032 2.3\u2033/m"},
+        };
+        for (Object[] row : data) {
+            MeasureFormat mf = MeasureFormat.getInstance(
+                    (ULocale) row[0], (FormatWidth) row[1]);
+            assertEquals(
+                    "testMultiples",
+                    row[3],
+                    mf.formatMeasuresPer(
+                            new StringBuilder(),
+                            DontCareFieldPosition.INSTANCE,
+                            (MeasureUnit) row[2],
+                            new Measure(2, MeasureUnit.MILE), 
+                            new Measure(1, MeasureUnit.FOOT), 
+                            new Measure(2.3, MeasureUnit.INCH)).toString());
+        }
+    }
 
     public void testGram() {
         MeasureFormat mf = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.SHORT);
@@ -855,6 +881,64 @@ public class MeasureUnitTest extends TestFmwk {
         
     }
     
+    public void testFieldPositionMultipleWithPer() {
+        MeasureFormat fmt = MeasureFormat.getInstance(
+                ULocale.ENGLISH, FormatWidth.SHORT);
+        FieldPosition pos = new FieldPosition(NumberFormat.Field.INTEGER);
+        String result = fmt.formatMeasuresPer(
+                new StringBuilder(),
+                pos,
+                MeasureUnit.SECOND,
+                new Measure(354, MeasureUnit.METER),
+                new Measure(23, MeasureUnit.CENTIMETER)).toString();
+        assertEquals("result", "354 m, 23 cmps", result);
+        
+        // According to javadocs for {@link Format#format} FieldPosition is set to
+        // beginning and end of first such field encountered instead of the last
+        // such field encountered.
+        assertEquals("beginIndex", 0, pos.getBeginIndex());
+        assertEquals("endIndex", 3, pos.getEndIndex());
+        
+        pos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
+        result = fmt.formatMeasuresPer(
+                new StringBuilder("123456: "),
+                pos,
+                MeasureUnit.SECOND,
+                new Measure(354, MeasureUnit.METER),
+                new Measure(23, MeasureUnit.CENTIMETER),
+                new Measure(5.4, MeasureUnit.MILLIMETER)).toString();
+        assertEquals("result", "123456: 354 m, 23 cm, 5.4 mmps", result);
+        assertEquals("beginIndex", 23, pos.getBeginIndex());
+        assertEquals("endIndex", 24, pos.getEndIndex());
+  
+        pos = new FieldPosition(NumberFormat.Field.INTEGER);
+        result = fmt.formatMeasuresPer(
+                new StringBuilder(),
+                pos,
+                MeasureUnit.MINUTE,
+                new Measure(354, MeasureUnit.METER),
+                new Measure(23, MeasureUnit.CENTIMETER)).toString();
+        assertEquals("result", "354 m, 23 cm/min", result);
+        
+        // According to javadocs for {@link Format#format} FieldPosition is set to
+        // beginning and end of first such field encountered instead of the last
+        // such field encountered.
+        assertEquals("beginIndex", 0, pos.getBeginIndex());
+        assertEquals("endIndex", 3, pos.getEndIndex());
+        
+        pos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
+        result = fmt.formatMeasuresPer(
+                new StringBuilder("123456: "),
+                pos,
+                MeasureUnit.MINUTE,
+                new Measure(354, MeasureUnit.METER),
+                new Measure(23, MeasureUnit.CENTIMETER),
+                new Measure(5.4, MeasureUnit.MILLIMETER)).toString();
+        assertEquals("result", "123456: 354 m, 23 cm, 5.4 mm/min", result);
+        assertEquals("beginIndex", 23, pos.getBeginIndex());
+        assertEquals("endIndex", 24, pos.getEndIndex());
+    }
+    
     public void testOldFormatWithList() {
         List<Measure> measures = new ArrayList<Measure>(2);
         measures.add(new Measure(5, MeasureUnit.ACRE));