From: Travis Keep Date: Wed, 29 Jan 2014 00:09:46 +0000 (+0000) Subject: ICU-10646 Introduce Template class. Change ListFormatter and RelativeDateTimeFormatte... X-Git-Tag: milestone-59-0-1~2211 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=1cf0f06b0103be20240e6cfab2ffa1b387dbfe06;p=icu ICU-10646 Introduce Template class. Change ListFormatter and RelativeDateTimeFormatter to use the Template class instead of ad hoc replacement of placeholders. X-SVN-Rev: 35014 --- diff --git a/.gitattributes b/.gitattributes index fc78bbe4522..66c122c9df1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 index 00000000000..a46ce81b76d --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/Template.java @@ -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}". + *

+ * Using Template objects is both faster and safer than adhoc replacement + * such as pattern.replace("{0}", "Colorado").replace("{1} "Fred");. + * 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. + *

+ * Template objects are immutable and can be safely cached like strings. + *

+ * Example: + *

+ * Template template = Template.compile("{1} '{born} in {0}");
+ * 
+ * // Output: "paul {born} in england"
+ * System.out.println(template.evaluate("england", "paul"));
+ * 
+ */ +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 placeholderIdsOrderedByOffset = new ArrayList(); + 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); + } + + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java index 775c8c86ff5..07a9eefc867 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java @@ -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}{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); } } 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 987a90e6a16..cf884c2e46f 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 @@ -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 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 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> styleToCountToFormat = unitToStyleToCountToFormat.get(unit); Map 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 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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java index 1e8ad7da075..2898c5f3681 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/QuantityFormatter.java @@ -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; } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/TestAll.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/TestAll.java index f66b8556616..71ecbd2a50d 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/TestAll.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/TestAll.java @@ -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 index 00000000000..b1315995d4d --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TemplateTest.java @@ -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()); + } + +}