diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimplePatternFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java similarity index 60% rename from icu4j/main/classes/core/src/com/ibm/icu/impl/SimplePatternFormatter.java rename to icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java index 32dcb642ce0..7098c607d02 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimplePatternFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java @@ -8,39 +8,21 @@ package com.ibm.icu.impl; /** * Formats simple patterns like "{1} was born in {0}". - * Minimal subset of MessageFormat; fast, simple, minimal dependencies. - * Supports only numbered arguments with no type nor style parameters, - * and formats only string values. - * Quoting via ASCII apostrophe compatible with ICU MessageFormat default behavior. + * Internal version of {@link com.ibm.icu.text.SimpleFormatter} + * with only static methods, to avoid wrapper objects. * - *

Factory methods throw exceptions for syntax errors - * and for too few or too many arguments/placeholders. + *

This class "compiles" pattern strings into a binary format + * and implements formatting etc. based on that. * - *

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

Format: + * Index 0: One more than the highest argument number. + * Followed by zero or more arguments or literal-text segments. * - *

Example: - *

- * SimplePatternFormatter fmt = SimplePatternFormatter.compile("{1} '{born}' in {0}");
- *
- * // Output: "paul {born} in england"
- * System.out.println(fmt.format("england", "paul"));
- * 
- * - * @see com.ibm.icu.text.MessageFormat - * @see com.ibm.icu.text.MessagePattern.ApostropheMode + *

An argument is stored as its number, less than ARG_NUM_LIMIT. + * A literal-text segment is stored as its length (at least 1) offset by ARG_NUM_LIMIT, + * followed by that many chars. */ -public final class SimplePatternFormatter { - // For internal use in Java, - // it is most efficient to compile patterns to compiled-pattern strings - // and use them with static methods. - // - // If and when we make this public API, - // we should probably make only the non-static methods public - // and keep the static ones in an impl class. - // - // TODO: Consider changing methods & docs to use "argument" not "placeholder", - // consistent with MessageFormat. - +public final class SimpleFormatterImpl { /** * Argument numbers must be smaller than this limit. * Text segment lengths are offset by this much. @@ -48,66 +30,53 @@ public final class SimplePatternFormatter { * except it is the maximum value of the first unit (max arg +1). */ private static final int ARG_NUM_LIMIT = 0x100; + private static final char LEN1_CHAR = (char)(ARG_NUM_LIMIT + 1); + private static final char LEN2_CHAR = (char)(ARG_NUM_LIMIT + 2); + private static final char LEN3_CHAR = (char)(ARG_NUM_LIMIT + 3); /** * Initial and maximum char/UChar value set for a text segment. * Segment length char values are from ARG_NUM_LIMIT+1 to this value here. * Normally 0xffff, but can be as small as ARG_NUM_LIMIT+1 for testing. */ - private static final char SEGMENT_LENGTH_PLACEHOLDER_CHAR = (char)0xffff; + private static final char SEGMENT_LENGTH_ARGUMENT_CHAR = (char)0xffff; /** * Maximum length of a text segment. Longer segments are split into shorter ones. */ - private static final int MAX_SEGMENT_LENGTH = SEGMENT_LENGTH_PLACEHOLDER_CHAR - ARG_NUM_LIMIT; + private static final int MAX_SEGMENT_LENGTH = SEGMENT_LENGTH_ARGUMENT_CHAR - ARG_NUM_LIMIT; - /** - * Binary representation of the compiled pattern. - * Index 0: One more than the highest argument number. - * Followed by zero or more arguments or literal-text segments. - * - *

An argument is stored as its number, less than ARG_NUM_LIMIT. - * A literal-text segment is stored as its length (at least 1) offset by ARG_NUM_LIMIT, - * followed by that many chars. - */ - private final String compiledPattern; + /** "Intern" some common patterns. */ + private static final String[][] COMMON_PATTERNS = { + { "{0} {1}", "\u0002\u0000" + LEN1_CHAR + " \u0001" }, + { "{0} ({1})", "\u0002\u0000" + LEN2_CHAR + " (\u0001" + LEN1_CHAR + ')' }, + { "{0}, {1}", "\u0002\u0000" + LEN2_CHAR + ", \u0001" }, + { "{0} – {1}", "\u0002\u0000" + LEN3_CHAR + " – \u0001" }, // en dash + }; - private SimplePatternFormatter(String compiledPattern) { - this.compiledPattern = compiledPattern; - } - - /** - * Creates a formatter from the pattern string. - * - * @param pattern The pattern string. - * @return The new SimplePatternFormatter object. - */ - public static SimplePatternFormatter compile(CharSequence pattern) { - return compileMinMaxPlaceholders(pattern, 0, Integer.MAX_VALUE); - } - - /** - * Creates a formatter from the pattern string. - * - * @param pattern The pattern string. - * @param min The pattern must have at least this many placeholders. - * @param max The pattern must have at most this many placeholders. - * @return The new SimplePatternFormatter object. - */ - public static SimplePatternFormatter compileMinMaxPlaceholders(CharSequence pattern, int min, int max) { - StringBuilder sb = new StringBuilder(); - String compiledPattern = compileToStringMinMaxPlaceholders(pattern, sb, min, max); - return new SimplePatternFormatter(compiledPattern); - } + /** Use only static methods. */ + private SimpleFormatterImpl() {} /** * Creates a compiled form of the pattern string, for use with appropriate static methods. + * The number of arguments checked against the given limits is the + * highest argument number plus one, not the number of occurrences of arguments. * * @param pattern The pattern string. - * @param min The pattern must have at least this many placeholders. - * @param max The pattern must have at most this many placeholders. + * @param min The pattern must have at least this many arguments. + * @param max The pattern must have at most this many arguments. * @return The compiled-pattern string. + * @throws IllegalArgumentException for bad argument syntax and too few or too many arguments. */ - public static String compileToStringMinMaxPlaceholders( + public static String compileToStringMinMaxArguments( CharSequence pattern, StringBuilder sb, int min, int max) { + // Return some precompiled common two-argument patterns. + if (min <= 2 && 2 <= max) { + for (String[] pair : COMMON_PATTERNS) { + if (pair[0].contentEquals(pattern)) { + assert pair[1].charAt(0) == 2; + return pair[1]; + } + } + } // Parse consistent with MessagePattern, but // - support only simple numbered arguments // - build a simple binary structure into the result string @@ -177,7 +146,7 @@ public final class SimplePatternFormatter { // Append c and track the literal-text segment length. if (textLength == 0) { // Reserve a char for the length of a new text segment, preset the maximum length. - sb.append(SEGMENT_LENGTH_PLACEHOLDER_CHAR); + sb.append(SEGMENT_LENGTH_ARGUMENT_CHAR); } sb.append(c); if (++textLength == MAX_SEGMENT_LENGTH) { @@ -190,38 +159,24 @@ public final class SimplePatternFormatter { int argCount = maxArg + 1; if (argCount < min) { throw new IllegalArgumentException( - "Fewer than minimum " + min + " placeholders in pattern \"" + pattern + "\""); + "Fewer than minimum " + min + " arguments in pattern \"" + pattern + "\""); } if (argCount > max) { throw new IllegalArgumentException( - "More than maximum " + max + " placeholders in pattern \"" + pattern + "\""); + "More than maximum " + max + " arguments in pattern \"" + pattern + "\""); } sb.setCharAt(0, (char)argCount); return sb.toString(); } - /** - * @return The max argument number/placeholder ID + 1. - */ - public int getPlaceholderCount() { - return getPlaceholderCount(compiledPattern); - } - /** * @param compiledPattern Compiled form of a pattern string. - * @return The max argument number/placeholder ID + 1. + * @return The max argument number + 1. */ - public static int getPlaceholderCount(String compiledPattern) { + public static int getArgumentLimit(String compiledPattern) { return compiledPattern.charAt(0); } - /** - * Formats the given values. - */ - public String format(CharSequence... values) { - return formatCompiledPattern(compiledPattern, values); - } - /** * Formats the given values. * @@ -231,25 +186,6 @@ public final class SimplePatternFormatter { return formatAndAppend(compiledPattern, new StringBuilder(), null, values).toString(); } - /** - * Formats the given values, appending to the appendTo builder. - * - * @param appendTo Gets the formatted pattern and values appended. - * @param offsets offsets[i] receives the offset of where - * values[i] replaced pattern argument {i}. - * Can be null, or can be shorter or longer than values. - * If there is no {i} in the pattern, then offsets[i] is set to -1. - * @param values The placeholder values. - * A placeholder value must not be the same object as appendTo. - * values.length must be at least getPlaceholderCount(). - * Can be null if getPlaceholderCount()==0. - * @return appendTo - */ - public StringBuilder formatAndAppend( - StringBuilder appendTo, int[] offsets, CharSequence... values) { - return formatAndAppend(compiledPattern, appendTo, offsets, values); - } - /** * Formats the given values, appending to the appendTo builder. * @@ -259,16 +195,16 @@ public final class SimplePatternFormatter { * values[i] replaced pattern argument {i}. * Can be null, or can be shorter or longer than values. * If there is no {i} in the pattern, then offsets[i] is set to -1. - * @param values The placeholder values. - * A placeholder value must not be the same object as appendTo. - * values.length must be at least getPlaceholderCount(). - * Can be null if getPlaceholderCount()==0. + * @param values The argument values. + * An argument value must not be the same object as appendTo. + * values.length must be at least getArgumentLimit(). + * Can be null if getArgumentLimit()==0. * @return appendTo */ public static StringBuilder formatAndAppend( String compiledPattern, StringBuilder appendTo, int[] offsets, CharSequence... values) { int valuesLength = values != null ? values.length : 0; - if (valuesLength < getPlaceholderCount(compiledPattern)) { + if (valuesLength < getArgumentLimit(compiledPattern)) { throw new IllegalArgumentException("Too few values."); } return format(compiledPattern, values, appendTo, null, true, offsets); @@ -277,27 +213,7 @@ public final class SimplePatternFormatter { /** * Formats the given values, replacing the contents of the result builder. * May optimize by actually appending to the result if it is the same object - * as the initial argument's corresponding value. - * - * @param result Gets its contents replaced by the formatted pattern and values. - * @param offsets offsets[i] receives the offset of where - * values[i] replaced pattern argument {i}. - * Can be null, or can be shorter or longer than values. - * If there is no {i} in the pattern, then offsets[i] is set to -1. - * @param values The placeholder values. - * A placeholder value may be the same object as result. - * values.length must be at least getPlaceholderCount(). - * @return result - */ - public StringBuilder formatAndReplace( - StringBuilder result, int[] offsets, CharSequence... values) { - return formatAndReplace(compiledPattern, result, offsets, values); - } - - /** - * Formats the given values, replacing the contents of the result builder. - * May optimize by actually appending to the result if it is the same object - * as the initial argument's corresponding value. + * as the value corresponding to the initial argument in the pattern. * * @param compiledPattern Compiled form of a pattern string. * @param result Gets its contents replaced by the formatted pattern and values. @@ -305,15 +221,15 @@ public final class SimplePatternFormatter { * values[i] replaced pattern argument {i}. * Can be null, or can be shorter or longer than values. * If there is no {i} in the pattern, then offsets[i] is set to -1. - * @param values The placeholder values. - * A placeholder value may be the same object as result. - * values.length must be at least getPlaceholderCount(). + * @param values The argument values. + * An argument value may be the same object as result. + * values.length must be at least getArgumentLimit(). * @return result */ public static StringBuilder formatAndReplace( String compiledPattern, StringBuilder result, int[] offsets, CharSequence... values) { int valuesLength = values != null ? values.length : 0; - if (valuesLength < getPlaceholderCount(compiledPattern)) { + if (valuesLength < getArgumentLimit(compiledPattern)) { throw new IllegalArgumentException("Too few values."); } @@ -324,7 +240,7 @@ public final class SimplePatternFormatter { // If any non-initial argument value is the same object as the result, // then we first copy its contents and use that instead while formatting. String resultCopy = null; - if (getPlaceholderCount(compiledPattern) > 0) { + if (getArgumentLimit(compiledPattern) > 0) { for (int i = 1; i < compiledPattern.length();) { int n = compiledPattern.charAt(i++); if (n < ARG_NUM_LIMIT) { @@ -347,33 +263,13 @@ public final class SimplePatternFormatter { } /** - * Returns a string similar to the original pattern, only for debugging. - */ - @Override - public String toString() { - String[] values = new String[getPlaceholderCount()]; - for (int i = 0; i < values.length; i++) { - values[i] = String.format("{%d}", i); - } - return formatAndAppend(new StringBuilder(), null, values).toString(); - } - - /** - * Returns the pattern text with none of the placeholders. - * Like formatting with all-empty string values. - */ - public String getTextWithNoPlaceholders() { - return getTextWithNoPlaceholders(compiledPattern); - } - - /** - * Returns the pattern text with none of the placeholders. + * Returns the pattern text with none of the arguments. * Like formatting with all-empty string values. * * @param compiledPattern Compiled form of a pattern string. */ - public static String getTextWithNoPlaceholders(String compiledPattern) { - int capacity = compiledPattern.length() - 1 - getPlaceholderCount(compiledPattern); + public static String getTextWithNoArguments(String compiledPattern) { + int capacity = compiledPattern.length() - 1 - getArgumentLimit(compiledPattern); StringBuilder sb = new StringBuilder(capacity); for (int i = 1; i < compiledPattern.length();) { int segmentLength = compiledPattern.charAt(i++) - ARG_NUM_LIMIT; 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 0dda989128d..cc1c76c098d 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 @@ -15,7 +15,7 @@ import java.util.Locale; import com.ibm.icu.impl.ICUCache; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; -import com.ibm.icu.impl.SimplePatternFormatter; +import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; @@ -27,7 +27,7 @@ import com.ibm.icu.util.UResourceBundle; * @stable ICU 50 */ final public class ListFormatter { - // Compiled SimplePatternFormatter patterns. + // Compiled SimpleFormatter patterns. private final String two; private final String start; private final String middle; @@ -124,7 +124,7 @@ final public class ListFormatter { } private static String compilePattern(String pattern, StringBuilder sb) { - return SimplePatternFormatter.compileToStringMinMaxPlaceholders(pattern, sb, 2, 2); + return SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); } /** @@ -269,7 +269,7 @@ final public class ListFormatter { // is true, records the offset of next in the formatted string. public FormattedListBuilder append(String pattern, Object next, boolean recordOffset) { int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null; - SimplePatternFormatter.formatAndReplace( + SimpleFormatterImpl.formatAndReplace( pattern, current, offsets, current, next.toString()); if (offsets != null) { if (offsets[0] == -1 || offsets[1] == -1) { 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 cff1437e25a..15ecee7c3da 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 @@ -32,7 +32,7 @@ import com.ibm.icu.impl.DontCareFieldPosition; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; -import com.ibm.icu.impl.SimplePatternFormatter; +import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.UResource; import com.ibm.icu.math.BigDecimal; @@ -427,7 +427,8 @@ public class MeasureFormat extends UFormat { StandardPlural.fromString(keywordHigh)); String rangeFormatter = getRangeFormat(getLocale(), formatWidth); - String formattedNumber = SimplePatternFormatter.formatCompiledPattern(rangeFormatter, lowFormatted, highFormatted); + String formattedNumber = SimpleFormatterImpl.formatCompiledPattern( + rangeFormatter, lowFormatted, highFormatted); if (isCurrency) { // Nasty hack @@ -451,7 +452,7 @@ public class MeasureFormat extends UFormat { } else { String formatter = getPluralFormatter(lowValue.getUnit(), formatWidth, resolvedPlural.ordinal()); - return SimplePatternFormatter.formatCompiledPattern(formatter, formattedNumber); + return SimpleFormatterImpl.formatCompiledPattern(formatter, formattedNumber); } } @@ -766,7 +767,7 @@ public class MeasureFormat extends UFormat { } } if (patterns[index] == null) { - patterns[index] = SimplePatternFormatter.compileToStringMinMaxPlaceholders( + patterns[index] = SimpleFormatterImpl.compileToStringMinMaxArguments( value.getString(), sb, minPlaceholders, 1); } } @@ -813,7 +814,7 @@ public class MeasureFormat extends UFormat { public void put(UResource.Key key, UResource.Value value) { if (key.contentEquals("per")) { cacheData.styleToPerPattern.put(width, - SimplePatternFormatter.compileToStringMinMaxPlaceholders( + SimpleFormatterImpl.compileToStringMinMaxArguments( value.getString(), sb, 2, 2)); } } @@ -996,13 +997,13 @@ public class MeasureFormat extends UFormat { String perUnitPattern = getFormatterOrNull(perUnit, formatWidth, MeasureFormatData.PER_UNIT_INDEX); if (perUnitPattern != null) { - SimplePatternFormatter.formatAndAppend(perUnitPattern, appendTo, offsets, formatted); + SimpleFormatterImpl.formatAndAppend(perUnitPattern, appendTo, offsets, formatted); return offsets[0]; } String perPattern = getPerFormatter(formatWidth); String pattern = getPluralFormatter(perUnit, formatWidth, StandardPlural.ONE.ordinal()); - String perUnitString = SimplePatternFormatter.getTextWithNoPlaceholders(pattern).trim(); - SimplePatternFormatter.formatAndAppend( + String perUnitString = SimpleFormatterImpl.getTextWithNoArguments(pattern).trim(); + SimpleFormatterImpl.formatAndAppend( perPattern, appendTo, offsets, formatted, perUnitString); return offsets[0]; } @@ -1414,7 +1415,7 @@ public class MeasureFormat extends UFormat { new ConcurrentHashMap(); /** - * Return a formatter (compiled SimplePatternFormatter pattern) for a range, such as "{0}–{1}". + * Return a formatter (compiled SimpleFormatter pattern) for a range, such as "{0}–{1}". * @param forLocale locale to get the format for * @param width the format width * @return range formatter, such as "{0}–{1}" @@ -1449,7 +1450,8 @@ public class MeasureFormat extends UFormat { } catch ( MissingResourceException ex ) { resultString = rb.getStringWithFallback("NumberElements/latn/patterns/range"); } - result = SimplePatternFormatter.compileToStringMinMaxPlaceholders(resultString, new StringBuilder(), 2, 2); + result = SimpleFormatterImpl.compileToStringMinMaxArguments( + resultString, new StringBuilder(), 2, 2); localeIdToRangeFormat.put(forLocale, result); if (!forLocale.equals(realLocale)) { localeIdToRangeFormat.put(realLocale, result); 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 bf3b3ed2a6a..f7706a2f798 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 @@ -8,9 +8,10 @@ package com.ibm.icu.text; import java.text.FieldPosition; -import com.ibm.icu.impl.SimplePatternFormatter; +import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.text.PluralRules.FixedDecimal; +import com.ibm.icu.text.SimpleFormatter; /** * QuantityFormatter represents an unknown quantity of something and formats a known quantity @@ -21,8 +22,8 @@ import com.ibm.icu.text.PluralRules.FixedDecimal; * PluralRules and DecimalFormat. It is package-protected as it is not meant for public use. */ class QuantityFormatter { - private final SimplePatternFormatter[] templates = - new SimplePatternFormatter[StandardPlural.COUNT]; + private final SimpleFormatter[] templates = + new SimpleFormatter[StandardPlural.COUNT]; public QuantityFormatter() {} @@ -41,7 +42,7 @@ class QuantityFormatter { if (templates[idx] != null) { return; } - templates[idx] = SimplePatternFormatter.compileMinMaxPlaceholders(template, 0, 1); + templates[idx] = SimpleFormatter.compileMinMaxArguments(template, 0, 1); } /** @@ -62,7 +63,7 @@ class QuantityFormatter { public String format(double number, NumberFormat numberFormat, PluralRules pluralRules) { String formatStr = numberFormat.format(number); StandardPlural p = selectPlural(number, numberFormat, pluralRules); - SimplePatternFormatter formatter = templates[p.ordinal()]; + SimpleFormatter formatter = templates[p.ordinal()]; if (formatter == null) { formatter = templates[StandardPlural.OTHER_INDEX]; assert formatter != null; @@ -71,20 +72,20 @@ class QuantityFormatter { } /** - * Gets the SimplePatternFormatter for a particular variant. + * Gets the SimpleFormatter for a particular variant. * @param variant "zero", "one", "two", "few", "many", "other" - * @return the SimplePatternFormatter + * @return the SimpleFormatter */ - public SimplePatternFormatter getByVariant(CharSequence variant) { + public SimpleFormatter getByVariant(CharSequence variant) { assert isValid(); int idx = StandardPlural.indexOrOtherIndexFromString(variant); - SimplePatternFormatter template = templates[idx]; + SimpleFormatter template = templates[idx]; return (template == null && idx != StandardPlural.OTHER_INDEX) ? templates[StandardPlural.OTHER_INDEX] : template; } // The following methods live here so that class PluralRules does not depend on number formatting, - // and the SimplePatternFormatter does not depend on FieldPosition. + // and the SimpleFormatter does not depend on FieldPosition. /** * Selects the standard plural form for the number/formatter/rules. @@ -123,7 +124,7 @@ class QuantityFormatter { public static StringBuilder format(String compiledPattern, CharSequence value, StringBuilder appendTo, FieldPosition pos) { int[] offsets = new int[1]; - SimplePatternFormatter.formatAndAppend(compiledPattern, appendTo, offsets, value); + SimpleFormatterImpl.formatAndAppend(compiledPattern, appendTo, offsets, value); if (pos.getBeginIndex() != 0 || pos.getEndIndex() != 0) { if (offsets[0] >= 0) { pos.setBeginIndex(pos.getBeginIndex() + offsets[0]); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java index 6120b3cd764..49260501f8a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java @@ -14,7 +14,7 @@ import com.ibm.icu.impl.DontCareFieldPosition; import com.ibm.icu.impl.ICUCache; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; -import com.ibm.icu.impl.SimplePatternFormatter; +import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.util.Calendar; @@ -409,7 +409,7 @@ public final class RelativeDateTimeFormatter { numberFormat, pluralRules, formatStr, fieldPosition); String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); - result = SimplePatternFormatter.formatCompiledPattern(formatter, formatStr); + result = SimpleFormatterImpl.formatCompiledPattern(formatter, formatStr); } return adjustForContext(result); @@ -852,7 +852,7 @@ public final class RelativeDateTimeFormatter { @Override public void put(UResource.Key key, UResource.Value value) { /* Make two lists of simplePatternFmtList, one for past and one for future. - * Set a SimplePatternFormatter for the + * Set a SimpleFormatter pattern for the * * Fill in values for the particular plural given, e.g., ONE, FEW, OTHER, etc. */ @@ -869,9 +869,9 @@ public final class RelativeDateTimeFormatter { } int pluralIndex = StandardPlural.indexFromString(key.toString()); if (patterns[pastFutureIndex][pluralIndex] == null) { - patterns[pastFutureIndex][pluralIndex] = - SimplePatternFormatter.compileToStringMinMaxPlaceholders(value.getString(), - sb, 0, 1); + patterns[pastFutureIndex][pluralIndex] = + SimpleFormatterImpl.compileToStringMinMaxArguments( + value.getString(), sb, 0, 1); } } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleFormatter.java new file mode 100644 index 00000000000..cfb664d6adc --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleFormatter.java @@ -0,0 +1,170 @@ +/* + ******************************************************************************* + * Copyright (C) 2014-2016, International Business Machines Corporation and + * others. All Rights Reserved. + ******************************************************************************* + */ +package com.ibm.icu.text; + +import com.ibm.icu.impl.SimpleFormatterImpl; + +/** + * Formats simple patterns like "{1} was born in {0}". + * Minimal subset of MessageFormat; fast, simple, minimal dependencies. + * Supports only numbered arguments with no type nor style parameters, + * and formats only string values. + * Quoting via ASCII apostrophe compatible with ICU MessageFormat default behavior. + * + *

Factory methods throw exceptions for syntax errors + * and for too few or too many arguments/placeholders. + * + *

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

Example: + *

+ * SimpleFormatter fmt = SimpleFormatter.compile("{1} '{born}' in {0}");
+ *
+ * // Output: "paul {born} in england"
+ * System.out.println(fmt.format("england", "paul"));
+ * 
+ * + * @see MessageFormat + * @see MessagePattern.ApostropheMode + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ +public final class SimpleFormatter { + // For internal use in Java, use SimpleFormatterImpl directly instead: + // It is most efficient to compile patterns to compiled-pattern strings + // and use them with static methods. + // (Avoids allocating SimpleFormatter wrapper objects.) + + /** + * Binary representation of the compiled pattern. + * @see SimpleFormatterImpl + */ + private final String compiledPattern; + + private SimpleFormatter(String compiledPattern) { + this.compiledPattern = compiledPattern; + } + + /** + * Creates a formatter from the pattern string. + * + * @param pattern The pattern string. + * @return The new SimpleFormatter object. + * @throws IllegalArgumentException for bad argument syntax. + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public static SimpleFormatter compile(CharSequence pattern) { + return compileMinMaxArguments(pattern, 0, Integer.MAX_VALUE); + } + + /** + * Creates a formatter from the pattern string. + * The number of arguments checked against the given limits is the + * highest argument number plus one, not the number of occurrences of arguments. + * + * @param pattern The pattern string. + * @param min The pattern must have at least this many arguments. + * @param max The pattern must have at most this many arguments. + * @return The new SimpleFormatter object. + * @throws IllegalArgumentException for bad argument syntax and too few or too many arguments. + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public static SimpleFormatter compileMinMaxArguments(CharSequence pattern, int min, int max) { + StringBuilder sb = new StringBuilder(); + String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, min, max); + return new SimpleFormatter(compiledPattern); + } + + /** + * @return The max argument number + 1. + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public int getArgumentLimit() { + return SimpleFormatterImpl.getArgumentLimit(compiledPattern); + } + + /** + * Formats the given values. + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public String format(CharSequence... values) { + return SimpleFormatterImpl.formatCompiledPattern(compiledPattern, values); + } + + /** + * Formats the given values, appending to the appendTo builder. + * + * @param appendTo Gets the formatted pattern and values appended. + * @param offsets offsets[i] receives the offset of where + * values[i] replaced pattern argument {i}. + * Can be null, or can be shorter or longer than values. + * If there is no {i} in the pattern, then offsets[i] is set to -1. + * @param values The argument values. + * An argument value must not be the same object as appendTo. + * values.length must be at least getArgumentLimit(). + * Can be null if getArgumentLimit()==0. + * @return appendTo + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public StringBuilder formatAndAppend( + StringBuilder appendTo, int[] offsets, CharSequence... values) { + return SimpleFormatterImpl.formatAndAppend(compiledPattern, appendTo, offsets, values); + } + + /** + * Formats the given values, replacing the contents of the result builder. + * May optimize by actually appending to the result if it is the same object + * as the value corresponding to the initial argument in the pattern. + * + * @param result Gets its contents replaced by the formatted pattern and values. + * @param offsets offsets[i] receives the offset of where + * values[i] replaced pattern argument {i}. + * Can be null, or can be shorter or longer than values. + * If there is no {i} in the pattern, then offsets[i] is set to -1. + * @param values The argument values. + * An argument value may be the same object as result. + * values.length must be at least getArgumentLimit(). + * @return result + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public StringBuilder formatAndReplace( + StringBuilder result, int[] offsets, CharSequence... values) { + return SimpleFormatterImpl.formatAndReplace(compiledPattern, result, offsets, values); + } + + /** + * Returns a string similar to the original pattern, only for debugging. + * + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + @Override + public String toString() { + String[] values = new String[getArgumentLimit()]; + for (int i = 0; i < values.length; i++) { + values[i] = "{" + i + '}'; + } + return formatAndAppend(new StringBuilder(), null, values).toString(); + } + + /** + * Returns the pattern text with none of the arguments. + * Like formatting with all-empty string values. + * + * @draft ICU 57 + * @provisional This API might change or be removed in a future release. + */ + public String getTextWithNoArguments() { + return SimpleFormatterImpl.getTextWithNoArguments(compiledPattern); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java index 77b5da25534..33dff6d890b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRangesTest.java @@ -9,7 +9,7 @@ package com.ibm.icu.dev.test.format; import java.util.Arrays; import com.ibm.icu.dev.test.TestFmwk; -import com.ibm.icu.impl.SimplePatternFormatter; +import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.text.MeasureFormat; import com.ibm.icu.text.MeasureFormat.FormatWidth; @@ -62,7 +62,7 @@ public class PluralRangesTest extends TestFmwk { FormatWidth width = FormatWidth.valueOf(test[1]); String expected = test[2]; String formatter = MeasureFormat.getRangeFormat(ulocale, width); - String actual = SimplePatternFormatter.formatCompiledPattern(formatter, "{0}", "{1}"); + String actual = SimpleFormatterImpl.formatCompiledPattern(formatter, "{0}", "{1}"); assertEquals("range pattern " + Arrays.asList(test), expected, actual); } } 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 7a4cd1e4f5c..b7ed931e3ba 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,7 +1,7 @@ /* ******************************************************************************* - * Copyright (C) 1996-2014, International Business Machines Corporation and * - * others. All Rights Reserved. * + * Copyright (C) 1996-2016, International Business Machines Corporation and + * others. All Rights Reserved. ******************************************************************************* */ package com.ibm.icu.dev.test.impl; @@ -22,7 +22,7 @@ public class TestAll extends TestGroup { "ICUServiceTest", "ICUServiceThreadTest", "ICUBinaryTest", - "SimplePatternFormatterTest", + "SimpleFormatterTest", "TextTrieMapTest" }, "Test miscellaneous implementation utilities"); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimplePatternFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimpleFormatterTest.java similarity index 76% rename from icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimplePatternFormatterTest.java rename to icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimpleFormatterTest.java index cdde8f182ad..edb8f90a97d 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimplePatternFormatterTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/SimpleFormatterTest.java @@ -7,16 +7,16 @@ package com.ibm.icu.dev.test.util; import com.ibm.icu.dev.test.TestFmwk; -import com.ibm.icu.impl.SimplePatternFormatter; import com.ibm.icu.text.MessageFormat; +import com.ibm.icu.text.SimpleFormatter; import com.ibm.icu.util.ULocale; -public class SimplePatternFormatterTest extends TestFmwk { +public class SimpleFormatterTest extends TestFmwk { /** * Constructor */ - public SimplePatternFormatterTest() + public SimpleFormatterTest() { } @@ -24,20 +24,20 @@ public class SimplePatternFormatterTest extends TestFmwk { public static void main(String arg[]) { - SimplePatternFormatterTest test = new SimplePatternFormatterTest(); + SimpleFormatterTest test = new SimpleFormatterTest(); try { test.run(arg); } catch (Exception e) { - test.errln("Error testing SimplePatternFormatterTest"); + test.errln("Error testing SimpleFormatterTest"); } } - public void TestWithNoPlaceholders() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("This doesn''t have templates '{0}"); + public void TestWithNoArguments() { + SimpleFormatter fmt = SimpleFormatter.compile("This doesn''t have templates '{0}"); assertEquals( - "getPlaceholderCount", + "getArgumentLimit", 0, - fmt.getPlaceholderCount()); + fmt.getArgumentLimit()); assertEquals( "format", "This doesn't have templates {0}", @@ -71,41 +71,41 @@ public class SimplePatternFormatterTest extends TestFmwk { public void TestSyntaxErrors() { try { - SimplePatternFormatter.compile("{}"); + SimpleFormatter.compile("{}"); fail("Syntax error did not yield an exception."); } catch (IllegalArgumentException expected) { } try { - SimplePatternFormatter.compile("{12d"); + SimpleFormatter.compile("{12d"); fail("Syntax error did not yield an exception."); } catch (IllegalArgumentException expected) { } } - public void TestOnePlaceholder() { - assertEquals("TestOnePlaceholder", + public void TestOneArgument() { + assertEquals("TestOneArgument", "1 meter", - SimplePatternFormatter.compile("{0} meter").format("1")); + SimpleFormatter.compile("{0} meter").format("1")); } - public void TestBigPlaceholder() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("a{20}c"); - assertEquals("{20} count", 21, fmt.getPlaceholderCount()); + public void TestBigArgument() { + SimpleFormatter fmt = SimpleFormatter.compile("a{20}c"); + assertEquals("{20} count", 21, fmt.getArgumentLimit()); CharSequence[] values = new CharSequence[21]; values[20] = "b"; assertEquals("{20}=b", "abc", fmt.format(values)); } - public void TestGetTextWithNoPlaceholders() { + public void TestGetTextWithNoArguments() { assertEquals( "", "Templates and are here.", - SimplePatternFormatter.compile( - "Templates {1}{2} and {3} are here.").getTextWithNoPlaceholders()); + SimpleFormatter.compile( + "Templates {1}{2} and {3} are here.").getTextWithNoArguments()); } - public void TestTooFewPlaceholderValues() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile( + public void TestTooFewArgumentValues() { + SimpleFormatter fmt = SimpleFormatter.compile( "Templates {2}{1} and {4} are out of order."); try { fmt.format("freddy", "tommy", "frog", "leg"); @@ -129,13 +129,13 @@ public class SimplePatternFormatterTest extends TestFmwk { } } - public void TestWithPlaceholders() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile( + public void TestWithArguments() { + SimpleFormatter fmt = SimpleFormatter.compile( "Templates {2}{1} and {4} are out of order."); assertEquals( - "getPlaceholderCount", + "getArgumentLimit", 5, - fmt.getPlaceholderCount()); + fmt.getArgumentLimit()); assertEquals( "toString", "Templates {2}{1} and {4} are out of order.", @@ -153,9 +153,9 @@ public class SimplePatternFormatterTest extends TestFmwk { verifyOffsets(expectedOffsets, offsets); } - public void TestFormatUseAppendToAsPlaceholder() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile( - "Placeholders {0} and {1}"); + public void TestFormatUseAppendToAsArgument() { + SimpleFormatter fmt = SimpleFormatter.compile( + "Arguments {0} and {1}"); StringBuilder appendTo = new StringBuilder("previous:"); try { fmt.formatAndAppend(appendTo, null, appendTo, "frog"); @@ -166,7 +166,7 @@ public class SimplePatternFormatterTest extends TestFmwk { } public void TestFormatReplaceNoOptimization() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}"); + SimpleFormatter fmt = SimpleFormatter.compile("{2}, {0}, {1} and {3}"); int[] offsets = new int[4]; StringBuilder result = new StringBuilder("original"); assertEquals( @@ -183,7 +183,7 @@ public class SimplePatternFormatterTest extends TestFmwk { public void TestFormatReplaceNoOptimizationLeadingText() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("boo {2}, {0}, {1} and {3}"); + SimpleFormatter fmt = SimpleFormatter.compile("boo {2}, {0}, {1} and {3}"); int[] offsets = new int[4]; StringBuilder result = new StringBuilder("original"); assertEquals( @@ -199,7 +199,7 @@ public class SimplePatternFormatterTest extends TestFmwk { } public void TestFormatReplaceOptimization() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}"); + SimpleFormatter fmt = SimpleFormatter.compile("{2}, {0}, {1} and {3}"); int[] offsets = new int[4]; StringBuilder result = new StringBuilder("original"); assertEquals( @@ -215,7 +215,7 @@ public class SimplePatternFormatterTest extends TestFmwk { } public void TestFormatReplaceOptimizationNoOffsets() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}"); + SimpleFormatter fmt = SimpleFormatter.compile("{2}, {0}, {1} and {3}"); StringBuilder result = new StringBuilder("original"); assertEquals( "format", @@ -228,17 +228,17 @@ public class SimplePatternFormatterTest extends TestFmwk { } public void TestFormatReplaceNoOptimizationNoOffsets() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile( - "Placeholders {0} and {1}"); + SimpleFormatter fmt = SimpleFormatter.compile( + "Arguments {0} and {1}"); StringBuilder result = new StringBuilder("previous:"); assertEquals( "", - "Placeholders previous: and frog", + "Arguments previous: and frog", fmt.formatAndReplace(result, null, result, "frog").toString()); } - public void TestFormatReplaceNoOptimizationLeadingPlaceholderUsedTwice() { - SimplePatternFormatter fmt = SimplePatternFormatter.compile( + public void TestFormatReplaceNoOptimizationLeadingArgumentUsedTwice() { + SimpleFormatter fmt = SimpleFormatter.compile( "{2}, {0}, {1} and {3} {2}"); StringBuilder result = new StringBuilder("original"); int[] offsets = new int[4]; @@ -255,11 +255,11 @@ public class SimplePatternFormatterTest extends TestFmwk { public void TestQuotingLikeMessageFormat() { String pattern = "{0} don't can''t '{5}''}{a' again '}'{1} to the '{end"; - SimplePatternFormatter spf = SimplePatternFormatter.compile(pattern); + SimpleFormatter spf = SimpleFormatter.compile(pattern); MessageFormat mf = new MessageFormat(pattern, ULocale.ROOT); String expected = "X don't can't {5}'}{a again }Y to the {end"; assertEquals("MessageFormat", expected, mf.format(new Object[] { "X", "Y" })); - assertEquals("SimplePatternFormatter", expected, spf.format("X", "Y")); + assertEquals("SimpleFormatter", expected, spf.format("X", "Y")); } void verifyOffsets(int[] expected, int[] actual) {