]> granicus.if.org Git - icu/commitdiff
ICU-10646 Introduce Template class. Change ListFormatter and RelativeDateTimeFormatte...
authorTravis Keep <keep94@gmail.com>
Wed, 29 Jan 2014 00:09:46 +0000 (00:09 +0000)
committerTravis Keep <keep94@gmail.com>
Wed, 29 Jan 2014 00:09:46 +0000 (00:09 +0000)
X-SVN-Rev: 35014

.gitattributes
icu4j/main/classes/core/src/com/ibm/icu/impl/Template.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/TestAll.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TemplateTest.java [new file with mode: 0644]

index fc78bbe45221d6cdde2c147164fc009f49e62185..66c122c9df1fd4d33b3a35f9e876b9a829bc0f5e 100644 (file)
@@ -260,6 +260,7 @@ icu4j/main/classes/core/.settings/edu.umd.cs.findbugs.core.prefs -text
 icu4j/main/classes/core/.settings/org.eclipse.core.resources.prefs -text
 icu4j/main/classes/core/.settings/org.eclipse.jdt.core.prefs -text
 icu4j/main/classes/core/manifest.stub -text
+icu4j/main/classes/core/src/com/ibm/icu/impl/Template.java -text
 icu4j/main/classes/currdata/.externalToolBuilders/copy-data-currdata.launch -text
 icu4j/main/classes/currdata/.settings/org.eclipse.core.resources.prefs -text
 icu4j/main/classes/currdata/.settings/org.eclipse.jdt.core.prefs -text
@@ -548,6 +549,7 @@ icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_52.1/com.ib
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_52.1/com.ibm.icu.util.ULocale.dat -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_52.1/com.ibm.icu.util.UResourceTypeMismatchException.dat -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/data/ICU_52.1/com.ibm.icu.util.VTimeZone.dat -text
+icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TemplateTest.java -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/Trie2Test.setRanges1.16.tri2 -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/Trie2Test.setRanges1.32.tri2 -text
 icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/Trie2Test.setRanges2.16.tri2 -text
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/Template.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/Template.java
new file mode 100644 (file)
index 0000000..a46ce81
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2014, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Compiled version of a template such as "{1} was born in {0}".
+ * <p>
+ * Using Template objects is both faster and safer than adhoc replacement 
+ * such as <code>pattern.replace("{0}", "Colorado").replace("{1} "Fred");</code>.
+ * They are faster because they are precompiled; they are safer because they
+ * account for curly braces escaped by apostrophe (').
+ * 
+ * Placeholders are of the form \{[0-9]+\}. If a curly brace is preceded
+ * by a single quote, it becomes a curly brace instead of the start of a
+ * placeholder. Two single quotes resolve to one single quote. 
+ * <p>
+ * Template objects are immutable and can be safely cached like strings.
+ * <p>
+ * Example:
+ * <pre>
+ * Template template = Template.compile("{1} '{born} in {0}");
+ * 
+ * // Output: "paul {born} in england"
+ * System.out.println(template.evaluate("england", "paul"));
+ * </pre>
+ */
+public class Template {
+    private final String patternWithoutPlaceholders;
+    private final int placeholderCount;
+    
+    // [0] first offset; [1] first placeholderId; [2] second offset;
+    // [3] second placeholderId etc.
+    private final int[] placeholderIdsOrderedByOffset;
+
+    private Template(String pattern, PlaceholdersBuilder builder) {
+        this.patternWithoutPlaceholders = pattern;
+        this.placeholderIdsOrderedByOffset =
+                builder.getPlaceholderIdsOrderedByOffset();
+        this.placeholderCount = builder.getPlaceholderCount();
+    }
+
+    /**
+     * Compiles a string into a template.
+     * @param pattern The string.
+     * @return the new template object.
+     */
+    public static Template compile(String pattern) {
+        PlaceholdersBuilder placeholdersBuilder = new PlaceholdersBuilder();
+        PlaceholderIdBuilder idBuilder =  new PlaceholderIdBuilder();
+        StringBuilder newPattern = new StringBuilder();
+        State state = State.INIT;
+        for (int i = 0; i < pattern.length(); i++) {
+            char ch = pattern.charAt(i);
+            switch (state) {
+            case INIT:
+                if (ch == 0x27) {
+                    state = State.APOSTROPHE;
+                } else if (ch == '{') {
+                    state = State.PLACEHOLDER;
+                    idBuilder.reset();
+                } else {
+                    newPattern.append(ch);
+                }
+                break;
+            case APOSTROPHE:
+                if (ch == 0x27) {
+                    newPattern.append("'");
+                } else if (ch == '{') {
+                    newPattern.append("{");
+                } else {
+                    newPattern.append("'");
+                    newPattern.append(ch);
+                }
+                state = State.INIT;
+                break;
+            case PLACEHOLDER:
+                if (ch >= '0' && ch <= '9') {
+                    idBuilder.add(ch);
+                } else if (ch == '}' && idBuilder.isValid()) {
+                    placeholdersBuilder.add(idBuilder.getId(), newPattern.length());
+                    state = State.INIT;
+                } else {
+                    newPattern.append('{');
+                    idBuilder.appendTo(newPattern);
+                    newPattern.append(ch);
+                    state = State.INIT;
+                }
+                break;
+            default:
+                throw new IllegalStateException();
+            }
+        }
+        switch (state) {
+        case INIT:
+            break;
+        case APOSTROPHE:
+            newPattern.append("'");
+            break;
+        case PLACEHOLDER:
+            newPattern.append('{');
+            idBuilder.appendTo(newPattern);
+            break;
+        default:
+            throw new IllegalStateException();
+        }
+        return new Template(newPattern.toString(), placeholdersBuilder);
+        
+    }
+    
+    /**
+     * Evaluates this template with given values. The first value
+     * corresponds to {0}; the second to {1} etc.
+     * @param values the values.
+     * @return The result.
+     * @throws IllegalArgumentException if the number of arguments is
+     * insufficient to match all the placeholders.
+     */
+    public String evaluate(Object... values) {
+        StringResultBuilder builder = new StringResultBuilder();
+        evaluatePrivate(values, builder);
+        return builder.build();
+    }
+    
+    /**
+     * Evaluates this template with given values. The first value
+     * corresponds to {0}; the second to {1} etc.
+     * @param values the values.
+     * @return The result of the evaluation.
+     * @throws IllegalArgumentException if the number of arguments is
+     * insufficient to match all the placeholders.
+     */
+    public Evaluation evaluateFull(Object... values) {
+        EvaluationResultBuilder builder = new EvaluationResultBuilder();
+        evaluatePrivate(values, builder);
+        return builder.build();
+    }
+    
+    /**
+     * Returns the max placeholder ID + 1.
+     */
+    public int getPlaceholderCount() {
+        return placeholderCount;
+    }
+    
+    /**
+     * Evaluates this template using values {0}, {1} etc. Note that this is
+     * not the same as the original pattern string used to build the template.
+     */
+    @Override
+    public String toString() {
+        String[] values = new String[this.getPlaceholderCount()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = String.format("{%d}", i);
+        }
+        return evaluate((Object[]) values);
+    }
+    
+    /**
+     * The immutable evaluation of a template.
+     */
+    public static class Evaluation {
+        
+        private final String result;
+        private final int[] offsets;
+
+        private Evaluation(String result, int[] placeholderOffsets) {
+            this.result = result;
+            this.offsets = placeholderOffsets;
+        }
+
+        /**
+         * Returns the offset of a particular placeholder in the evaluated
+         * string. Returns -1 if the placeholder did not exist in the 
+         * corresponding template.
+         * @throws IndexOutOfBoundsException if placeholderId is negative.
+         */
+        public int getOffset(int placeholderId) {
+            if (placeholderId < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            if (placeholderId >= offsets.length) {
+                return -1;
+            }
+            return offsets[placeholderId];
+        }
+
+        /**
+         * Returns the evaluated string.
+         */
+        public String toString() {
+            return result;
+        }
+
+    }
+    
+    private void evaluatePrivate(Object[] values, ResultBuilder builder) {
+        if (values.length < placeholderCount) {
+            throw new IllegalArgumentException(
+                    "There must be at least as values as placeholders.");
+        }
+        builder.setPlaceholderCount(placeholderCount);   
+        if (placeholderIdsOrderedByOffset.length == 0) {
+            builder.setResult(patternWithoutPlaceholders);
+            return;
+        }
+        StringBuilder result = new StringBuilder();
+        result.append(
+                patternWithoutPlaceholders,
+                0,
+                placeholderIdsOrderedByOffset[0]);
+        builder.setPlaceholderOffset(
+                placeholderIdsOrderedByOffset[1], result.length());
+        result.append(values[placeholderIdsOrderedByOffset[1]]);
+        for (int i = 2; i < placeholderIdsOrderedByOffset.length; i += 2) {
+            result.append(
+                    patternWithoutPlaceholders,
+                    placeholderIdsOrderedByOffset[i - 2],
+                    placeholderIdsOrderedByOffset[i]);
+            builder.setPlaceholderOffset(
+                    placeholderIdsOrderedByOffset[i + 1], result.length());
+            result.append(values[placeholderIdsOrderedByOffset[i + 1]]);
+        }
+        result.append(
+                patternWithoutPlaceholders,
+                placeholderIdsOrderedByOffset[placeholderIdsOrderedByOffset.length - 2],
+                patternWithoutPlaceholders.length());
+        builder.setResult(result.toString());
+    }
+    
+    private static enum State {
+        INIT,
+        APOSTROPHE,
+        PLACEHOLDER,
+    }
+    
+    private static class PlaceholderIdBuilder {
+        private int id = 0;
+        private int idLen = 0;
+        
+        public void reset() {
+            id = 0;
+            idLen = 0;
+        }
+
+        public int getId() {
+           return id;
+        }
+
+        public void appendTo(StringBuilder appendTo) {
+            if (idLen > 0) {
+                appendTo.append(id);
+            }
+        }
+
+        public boolean isValid() {
+           return idLen > 0;
+        }
+
+        public void add(char ch) {
+            id = id * 10 + ch - '0';
+            idLen++;
+        }     
+    }
+    
+    private static class PlaceholdersBuilder {
+        private List<Integer> placeholderIdsOrderedByOffset = new ArrayList<Integer>();
+        private int placeholderCount = 0;
+        
+        public void add(int placeholderId, int offset) {
+            placeholderIdsOrderedByOffset.add(offset);
+            placeholderIdsOrderedByOffset.add(placeholderId);
+            if (placeholderId >= placeholderCount) {
+                placeholderCount = placeholderId + 1;
+            }
+        }
+        
+        public int getPlaceholderCount() {
+            return placeholderCount;
+        }
+        
+        public int[] getPlaceholderIdsOrderedByOffset() {
+            int[] result = new int[placeholderIdsOrderedByOffset.size()];
+            for (int i = 0; i < result.length; i++) {
+                result[i] = placeholderIdsOrderedByOffset.get(i).intValue();
+            }
+            return result;
+        }
+    }
+    
+    private static interface ResultBuilder {
+        void setPlaceholderCount(int length);
+        void setPlaceholderOffset(int i, int length);
+        void setResult(String patternWithoutPlaceholders);
+    }
+    
+    private static class StringResultBuilder implements ResultBuilder {
+
+        private String result;
+        
+        public void setPlaceholderCount(int count) {
+        }
+
+        public void setPlaceholderOffset(int placeholderId, int offset) {
+        }
+
+        public void setResult(String result) {
+            this.result = result;    
+        }
+        
+        public String build() {
+            return result;
+        }
+    }
+    
+    private static class EvaluationResultBuilder implements ResultBuilder {
+        private int[] placeholderOffsets;
+        private String result;
+        
+        public void setPlaceholderCount(int count) {
+            placeholderOffsets = new int[count];
+            for (int i = 0; i < count; i++) {
+                placeholderOffsets[i] = -1;
+            }
+        }
+
+        public void setPlaceholderOffset(int placeholderId, int offset) {
+            placeholderOffsets[placeholderId] = offset;
+        }
+
+        public void setResult(String result) {
+            this.result = result;
+        }
+        
+        public Evaluation build() {
+            return new Evaluation(this.result, this.placeholderOffsets);
+        }
+        
+    }
+
+}
index 775c8c86ff55cecbcdcccc2b6d6665038b3c2a2d..07a9eefc86774e8f97ec32dca993c9411e5aa2ff 100644 (file)
@@ -18,6 +18,7 @@ import java.util.MissingResourceException;
 import com.ibm.icu.impl.ICUCache;
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.SimpleCache;
+import com.ibm.icu.impl.Template;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 
@@ -30,10 +31,10 @@ import com.ibm.icu.util.UResourceBundle;
  * @provisional This API might change or be removed in a future release.
  */
 final public class ListFormatter {
-    private final String two;
-    private final String start;
-    private final String middle;
-    private final String end;
+    private final Template two;
+    private final Template start;
+    private final Template middle;
+    private final Template end;
     private final ULocale locale;
     
     /**
@@ -102,10 +103,16 @@ final public class ListFormatter {
      * @deprecated This API is ICU internal only.
      */
     public ListFormatter(String two, String start, String middle, String end) {
-        this(two, start, middle, end, null);
+        this(
+                Template.compile(two),
+                Template.compile(start),
+                Template.compile(middle),
+                Template.compile(end),
+                null);
+        
     }
     
-    private ListFormatter(String two, String start, String middle, String end, ULocale locale) {
+    private ListFormatter(Template two, Template start, Template middle, Template end, ULocale locale) {
         this.two = two;
         this.start = start;
         this.middle = middle;
@@ -189,22 +196,30 @@ final public class ListFormatter {
         // TODO optimize this for the common case that the patterns are all of the
         // form {0}<sometext>{1}.
         // We avoid MessageFormat, because there is no "sub" formatting.
+        return format(items, -1).toString();
+    }
+    
+    // Formats a collection of objects and returns the formatted string plus the offset
+    // in the string where the index th element appears. index is zero based. If index is
+    // negative or greater than or equal to the size of items then this function returns -1 for
+    // the offset.
+    FormattedListBuilder format(Collection<?> items, int index) {
         Iterator<?> it = items.iterator();
         int count = items.size();
         switch (count) {
         case 0:
-            return "";
+            return new FormattedListBuilder("", false);
         case 1:
-            return it.next().toString();
+            return new FormattedListBuilder(it.next(), index == 0);
         case 2:
-            return format2(two, it.next(), it.next());
+            return new FormattedListBuilder(it.next(), index == 0).append(two, it.next(), index == 1);
         }
-        String result = it.next().toString();
-        result = format2(start, result, it.next());
-        for (count -= 3; count > 0; --count) {
-            result = format2(middle, result, it.next());
+        FormattedListBuilder builder = new FormattedListBuilder(it.next(), index == 0);
+        builder.append(start, it.next(), index == 1);
+        for (int idx = 2; idx < count - 1; ++idx) {
+            builder.append(middle, it.next(), index == idx);
         }
-        return format2(end, result, it.next());
+        return builder.append(end, it.next(), index == count - 1);
     }
     
     /**
@@ -235,17 +250,58 @@ final public class ListFormatter {
     public ULocale getLocale() {
         return locale;
     }
+    
+    // Builds a formatted list
+    static class FormattedListBuilder {
+        private String current;
+        private int offset;
+        
+        // Start is the first object in the list; If recordOffset is true, records the offset of
+        // this first object.
+        public FormattedListBuilder(Object start, boolean recordOffset) {
+            this.current = start.toString();
+            this.offset = recordOffset ? 0 : -1;
+        }
+        
+        // Appends additional object. pattern is a template indicating where the new object gets
+        // added in relation to the rest of the list. {0} represents the rest of the list; {1}
+        // represents the new object in pattern. next is the object to be added. If recordOffset
+        // is true, records the offset of next in the formatted string.
+        public FormattedListBuilder append(Template pattern, Object next, boolean recordOffset) {
+            if (pattern.getPlaceholderCount() != 2) {
+                throw new IllegalArgumentException("Need {0} and {1} only in pattern " + pattern);
+            }
+            if (recordOffset || offsetRecorded()) {
+                Template.Evaluation evaluation = pattern.evaluateFull(current, next);
+                int oneOffset = evaluation.getOffset(1);
+                int zeroOffset = evaluation.getOffset(0);
+                if (zeroOffset == -1 || oneOffset == -1) {
+                    throw new IllegalArgumentException("{0} or {1} missing from pattern " + pattern);
+                }
+                if (recordOffset) {
+                    offset = oneOffset;
+                } else {
+                    offset += zeroOffset;
+                }
+                current = evaluation.toString();
+            } else {
+                current = pattern.evaluate(current, next);
+            }
+            return this;
+        }
 
-    private String format2(String pattern, Object a, Object b) {
-        int i0 = pattern.indexOf("{0}");
-        int i1 = pattern.indexOf("{1}");
-        if (i0 < 0 || i1 < 0) {
-            throw new IllegalArgumentException("Missing {0} or {1} in pattern " + pattern);
+        @Override
+        public String toString() {
+            return current;
         }
-        if (i0 < i1) {
-            return pattern.substring(0, i0) + a + pattern.substring(i0+3, i1) + b + pattern.substring(i1+3);
-        } else {
-            return pattern.substring(0, i1) + b + pattern.substring(i1+3, i0) + a + pattern.substring(i0+3);
+        
+        // Gets the last recorded offset or -1 if no offset recorded.
+        public int getOffset() {
+            return offset;
+        }
+        
+        private boolean offsetRecorded() {
+            return offset >= 0;
         }
     }
 
@@ -280,17 +336,17 @@ final public class ListFormatter {
             // for listPattern/duration and listPattern/duration-narrow in root.txt.
             try {
                 return new ListFormatter(
-                    r.getWithFallback("listPattern/" + style + "/2").getString(),
-                    r.getWithFallback("listPattern/" + style + "/start").getString(),
-                    r.getWithFallback("listPattern/" + style + "/middle").getString(),
-                    r.getWithFallback("listPattern/" + style + "/end").getString(),
+                    Template.compile(r.getWithFallback("listPattern/" + style + "/2").getString()),
+                    Template.compile(r.getWithFallback("listPattern/" + style + "/start").getString()),
+                    Template.compile(r.getWithFallback("listPattern/" + style + "/middle").getString()),
+                    Template.compile(r.getWithFallback("listPattern/" + style + "/end").getString()),
                     ulocale);
             } catch (MissingResourceException e) {
                 return new ListFormatter(
-                        r.getWithFallback("listPattern/standard/2").getString(),
-                        r.getWithFallback("listPattern/standard/start").getString(),
-                        r.getWithFallback("listPattern/standard/middle").getString(),
-                        r.getWithFallback("listPattern/standard/end").getString(),
+                        Template.compile(r.getWithFallback("listPattern/standard/2").getString()),
+                        Template.compile(r.getWithFallback("listPattern/standard/start").getString()),
+                        Template.compile(r.getWithFallback("listPattern/standard/middle").getString()),
+                        Template.compile(r.getWithFallback("listPattern/standard/end").getString()),
                         ulocale);
             }
         }
index 987a90e6a164e3532c099417d380455fe8cd9226..cf884c2e46f404faed428a6bf3e1e293ac7e54f3 100644 (file)
@@ -19,6 +19,7 @@ import java.io.ObjectStreamException;
 import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
 import java.util.EnumMap;
@@ -346,7 +347,6 @@ public class MeasureFormat extends UFormat {
      * @draft ICU 53
      * @provisional
      */
-    @SuppressWarnings("unchecked")
     public <T extends Appendable> T formatMeasures(
             T appendable, FieldPosition fieldPosition, Measure... measures) {
         // fast track for trivial cases
@@ -368,26 +368,16 @@ public class MeasureFormat extends UFormat {
         
         ListFormatter listFormatter = ListFormatter.getInstance(
                 getLocale(), formatWidth.getListFormatterStyle());
-        String[] results = null;
-        if (fieldPosition == DontCareFieldPosition.INSTANCE) {
-            
-            // Fast track: No field position.
-            results = new String[measures.length];
-            for (int i = 0; i < measures.length; i++) {
-                results[i] = formatMeasure(measures[i]);
-            }
-        } else {
-            
-            // Slow track: Have to calculate field position.
-            results = formatMeasuresSlowTrack(listFormatter, fieldPosition, measures);            
+        if (fieldPosition != DontCareFieldPosition.INSTANCE) {
+            return append(formatMeasuresSlowTrack(listFormatter, fieldPosition, measures), appendable);
         }
-                 
-        // This is safe because appendable is of type T.
-        try {
-            return (T) appendable.append(listFormatter.format((Object[]) results));
-        } catch (IOException e) {
-            throw new RuntimeException(e);
+        // Fast track: No field position.
+        String[] results = new String[measures.length];
+        for (int i = 0; i < measures.length; i++) {
+            results[i] = formatMeasure(measures[i]);
         }
+        return append(listFormatter.format((Object[]) results), appendable);                 
+       
     }   
     
     /**
@@ -633,16 +623,13 @@ public class MeasureFormat extends UFormat {
     private <T extends Appendable> T formatMeasure(
             Measure measure, T appendable, FieldPosition fieldPosition) {
         if (measure.getUnit() instanceof Currency) {
-            try {
-                appendable.append(
-                        currencyFormat.format(
-                                new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()),
-                                new StringBuffer(),
-                                fieldPosition));
-                return appendable;
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
+            return append(
+                    currencyFormat.format(
+                    new CurrencyAmount(measure.getNumber(), (Currency) measure.getUnit()),
+                    new StringBuffer(),
+                    fieldPosition),
+                    appendable);
+            
         }
         Number n = measure.getNumber();
         MeasureUnit unit = measure.getUnit(); 
@@ -653,21 +640,17 @@ public class MeasureFormat extends UFormat {
         Map<FormatWidth, Map<String, PatternData>> styleToCountToFormat = unitToStyleToCountToFormat.get(unit);
         Map<String, PatternData> countToFormat = styleToCountToFormat.get(formatWidth);
         PatternData messagePatternData = countToFormat.get(keyword);
-        try {
-            appendable.append(messagePatternData.prefix);
-            if (messagePatternData.suffix != null) { // there is a number (may not happen with, say, Arabic dual)
-                // Fix field position
-                if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
-                    fieldPosition.setBeginIndex(fpos.getBeginIndex() + messagePatternData.prefix.length());
-                    fieldPosition.setEndIndex(fpos.getEndIndex() + messagePatternData.prefix.length());
-                }
-                appendable.append(formattedNumber);
-                appendable.append(messagePatternData.suffix);
+        append(messagePatternData.prefix, appendable);
+        if (messagePatternData.suffix != null) { // there is a number (may not happen with, say, Arabic dual)
+            // Fix field position
+            if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
+                fieldPosition.setBeginIndex(fpos.getBeginIndex() + messagePatternData.prefix.length());
+                fieldPosition.setEndIndex(fpos.getEndIndex() + messagePatternData.prefix.length());
             }
-            return appendable;
-        } catch (IOException e) {
-            throw new RuntimeException(e);
+            append(formattedNumber, appendable);
+            append(messagePatternData.suffix, appendable);
         }
+        return appendable;
     }
     
     // Wrapper around NumberFormat that provides immutability and thread-safety.
@@ -724,7 +707,7 @@ public class MeasureFormat extends UFormat {
         return new MeasureProxy(getLocale(), formatWidth, numberFormat.get(), CURRENCY_FORMAT);
     }
     
-    private String[] formatMeasuresSlowTrack(ListFormatter listFormatter, FieldPosition fieldPosition,
+    private String formatMeasuresSlowTrack(ListFormatter listFormatter, FieldPosition fieldPosition,
             Measure... measures) {
         String[] results = new String[measures.length];
         
@@ -742,23 +725,15 @@ public class MeasureFormat extends UFormat {
                 results[i] = formatMeasure(measures[i]);
             }
         }
+        ListFormatter.FormattedListBuilder builder =
+                listFormatter.format(Arrays.asList(results), fieldPositionFoundIndex);
         
         // Fix up FieldPosition indexes if our field is found.
-        if (fieldPositionFoundIndex != -1) {
-            String listPattern = listFormatter.getPatternForNumItems(measures.length);
-            int positionInPattern = listPattern.indexOf("{" + fieldPositionFoundIndex + "}");
-            if (positionInPattern == -1) {
-                throw new IllegalStateException("Can't find position with ListFormatter.");
-            }
-            // Now we have to adjust our position in pattern
-            // based on the previous values.
-            for (int i = 0; i < fieldPositionFoundIndex; i++) {
-                positionInPattern += (results[i].length() - ("{" + i + "}").length());
-            }
-            fieldPosition.setBeginIndex(fpos.getBeginIndex() + positionInPattern);
-            fieldPosition.setEndIndex(fpos.getEndIndex() + positionInPattern);
+        if (builder.getOffset() != -1) {
+            fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset());
+            fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset());
         }
-        return results;
+        return builder.toString();
     }
     
     // type is one of "hm", "ms" or "hms"
@@ -853,21 +828,26 @@ public class MeasureFormat extends UFormat {
         // 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);
+            if (iterator.getAttributes().containsKey(smallestField)) {
+                append(smallestAmountFormatted, appendable);
+                iterator.setIndex(iterator.getRunLimit(smallestField));
+            } else {
+                append(iterator.current(), appendable);
+                iterator.next();
             }
         }
         return appendable;
     }
     
+    private static <T extends Appendable> T append(Object o, T appendable) {
+        try {
+            appendable.append(o.toString());
+            return appendable;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } 
+    }
+    
     private Object writeReplace() throws ObjectStreamException {
         return new MeasureProxy(
                 getLocale(), formatWidth, numberFormat.get(), MEASURE_FORMAT);
index 1e8ad7da075f01be3689f7fb2c8290412366d65a..2898c5f3681f1b8c7c1a72bd9901d554410a7539 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 2013, International Business Machines Corporation and         *
+ * Copyright (C) 2013-2014, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
@@ -9,6 +9,8 @@ package com.ibm.icu.text;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.ibm.icu.impl.Template;
+
 /**
  * QuantityFormatter represents an unknown quantity of something and formats a known quantity
  * in terms of that something. For example, a QuantityFormatter that represents X apples may
@@ -25,6 +27,7 @@ class QuantityFormatter {
     
     static {
         int idx = 0;
+        // Other must be first.
         INDEX_MAP.put("other", idx++);
         INDEX_MAP.put("zero", idx++);
         INDEX_MAP.put("one", idx++);
@@ -42,7 +45,7 @@ class QuantityFormatter {
      */
     static class Builder {
         
-        private String[] templates;
+        private Template[] templates;
 
         /**
          * Adds a template.
@@ -51,16 +54,27 @@ class QuantityFormatter {
          * example, in English, the template for the "one" variant may be "{0} apple" while the
          * template for the "other" variant may be "{0} apples"
          * @return a reference to this Builder for chaining.
+         * @throws IllegalArgumentException if variant is not recognized or
+         *  if template has more than just the {0} placeholder.
          */
         public Builder add(String variant, String template) {
             ensureCapacity();
-            templates[INDEX_MAP.get(variant)] = template;
+            Integer idx = INDEX_MAP.get(variant);
+            if (idx == null) {
+                throw new IllegalArgumentException(variant);
+            }
+            Template newT = Template.compile(template);
+            if (newT.getPlaceholderCount() > 1) {
+                throw new IllegalArgumentException(
+                        "Extra placeholders: " + template);
+            }
+            templates[idx.intValue()] = newT;
             return this;
         }
 
         private void ensureCapacity() {
             if (templates == null) {
-                templates = new String[MAX_INDEX];
+                templates = new Template[MAX_INDEX];
             }
         }
 
@@ -83,9 +97,9 @@ class QuantityFormatter {
 
     }
 
-    private final String[] templates;
+    private final Template[] templates;
 
-    private QuantityFormatter(String[] templates) {
+    private QuantityFormatter(Template[] templates) {
         this.templates = templates;
     }
 
@@ -105,11 +119,12 @@ class QuantityFormatter {
         } else {
             variant = pluralRules.select(quantity);
         }
-        return getByVariant(variant).replace("{0}", formatStr);
+        return getByVariant(variant).evaluate(formatStr);
     }
 
-    private String getByVariant(String variant) {
-        String template = templates[INDEX_MAP.get(variant)];
+    private Template getByVariant(String variant) {
+        Integer idxObj = INDEX_MAP.get(variant);
+        Template template = templates[idxObj == null ? 0 : idxObj.intValue()];
         return template == null ? templates[0] : template;
     }
 }
index f66b8556616b7ff037189c7398cc2d1ee42495d3..71ecbd2a50d21128de6df043c137f7cd651dd1c7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 1996-2007, International Business Machines Corporation and    *
+ * Copyright (C) 1996-2014, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
@@ -22,6 +22,7 @@ public class TestAll extends TestGroup {
                   "ICUServiceTest",
                   "ICUServiceThreadTest",
                   "ICUBinaryTest",
+                  "TemplateTest",
                   "TextTrieMapTest"
               },
               "Test miscellaneous implementation utilities");
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TemplateTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TemplateTest.java
new file mode 100644 (file)
index 0000000..b131599
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2014, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.util;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.impl.Template;
+
+/**
+ * @author rocketman
+ *
+ */
+public class TemplateTest extends TestFmwk {
+
+    /**
+     * Constructor
+     */
+     public TemplateTest()
+     {
+     }
+       
+     // public methods -----------------------------------------------
+     
+     public static void main(String arg[]) 
+     {
+         TemplateTest test = new TemplateTest();
+         try {
+             test.run(arg);
+         } catch (Exception e) {
+             test.errln("Error testing templatetest");
+         }
+     }
+     
+     public void TestWithNoPlaceholders() {
+         Template t = Template.compile("This doesn''t have templates '{0}");
+         assertEquals(
+                 "getPlaceholderCount",
+                 0,
+                 t.getPlaceholderCount());
+         assertEquals(
+                 "evaluate",
+                 "This doesn't have templates {0}",
+                 t.evaluate());
+         assertEquals(
+                 "toString",
+                 "This doesn't have templates {0}",
+                 t.toString());
+         Template.Evaluation eval = t.evaluateFull();
+         assertEquals(
+                 "toString2",
+                 "This doesn't have templates {0}",
+                 eval.toString());
+         assertEquals(
+                 "getOffset(0)",
+                 -1,
+                 eval.getOffset(0));
+         t = Template.compile("Some {} messed {12d up stuff.");
+         assertEquals(
+                 "getPlaceholderCount",
+                 0,
+                 t.getPlaceholderCount());
+         assertEquals(
+                 "evaluate",
+                 "Some {} messed {12d up stuff.",
+                 t.evaluate("to"));
+     }
+     
+     public void TestOnePlaceholder() {
+        assertEquals("TestOnePlaceholder",
+                "1 meter",
+                Template.compile("{0} meter").evaluate(1));
+     }
+     
+     public void TestWithPlaceholders() {
+         Template t = Template.compile(
+                 "Templates {2}{1} and {4} are out of order.");
+         assertEquals(
+                 "getPlaceholderCount",
+                 5,
+                 t.getPlaceholderCount());
+         try {
+             t.evaluate("freddy", "tommy", "frog", "leg");
+             fail("Expected IllegalArgumentException");
+         } catch (IllegalArgumentException e) {
+             // Expected
+         }
+         assertEquals(
+                 "evaluate",
+                 "Templates frogtommy and {0} are out of order.",
+                 t.evaluate("freddy", "tommy", "frog", "leg", "{0}"));
+         assertEquals(
+                 "toString",
+                 "Templates {2}{1} and {4} are out of order.",
+                 t.toString());
+         Template.Evaluation eval =
+                 t.evaluateFull("freddy", "tommy", "frog", "leg", "{0}");
+         int[] offsets = {-1, 14, 10, -1, 24, -1};
+         for (int i = 0; i < offsets.length; i++) {
+             if (offsets[i] != eval.getOffset(i)) {
+                 fail("getOffset() returned wrong value for " + i);
+             }
+         }
+         assertEquals(
+                 "toString2",
+                 "Templates frogtommy and {0} are out of order.",
+                 eval.toString());
+     }
+    
+}