From edf3f9c72790a1e0b28ee60039512ef23908fb98 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Sat, 16 Nov 2019 07:14:54 +0000 Subject: [PATCH] ICU-20099 Implementing Java ListFormatter proposals for ICU 67. See #904 --- .../ibm/icu/impl/FormattedStringBuilder.java | 109 +++- .../impl/FormattedValueStringBuilderImpl.java | 94 +++- .../com/ibm/icu/impl/SimpleFormatterImpl.java | 120 ++++- .../number/ConstantMultiFieldModifier.java | 4 +- .../CurrencySpacingEnabledModifier.java | 4 +- .../ibm/icu/impl/number/SimpleModifier.java | 84 +--- .../com/ibm/icu/text/DateIntervalFormat.java | 12 +- .../src/com/ibm/icu/text/ListFormatter.java | 470 +++++++++++++++--- .../src/com/ibm/icu/text/MeasureFormat.java | 12 +- .../icu/text/RelativeDateTimeFormatter.java | 15 +- .../format/FormattedStringBuilderTest.java | 5 +- .../dev/test/format/ListFormatterTest.java | 83 ++++ .../dev/test/serializable/FormatHandler.java | 31 ++ .../serializable/SerializableTestUtility.java | 2 + 14 files changed, 834 insertions(+), 211 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java index 0eb54f5a695..2249cebbd6d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java @@ -2,7 +2,6 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl; -import java.text.Format.Field; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -24,23 +23,29 @@ import com.ibm.icu.text.NumberFormat; * * @author sffc (Shane Carr) */ -public class FormattedStringBuilder implements CharSequence { +public class FormattedStringBuilder implements CharSequence, Appendable { /** A constant, empty FormattedStringBuilder. Do NOT call mutative operations on this. */ public static final FormattedStringBuilder EMPTY = new FormattedStringBuilder(); char[] chars; - Field[] fields; + Object[] fields; int zero; int length; + /** Number of characters from the end where .append() operations insert. */ + int appendOffset = 0; + + /** Field applied when Appendable methods are used. */ + Object appendableField = null; + public FormattedStringBuilder() { this(40); } public FormattedStringBuilder(int capacity) { chars = new char[capacity]; - fields = new Field[capacity]; + fields = new Object[capacity]; zero = capacity / 2; length = 0; } @@ -72,7 +77,7 @@ public class FormattedStringBuilder implements CharSequence { return chars[zero + index]; } - public Field fieldAt(int index) { + public Object fieldAt(int index) { assert index >= 0; assert index < length; return fields[zero + index]; @@ -106,11 +111,20 @@ public class FormattedStringBuilder implements CharSequence { return this; } - public int appendChar16(char codeUnit, Field field) { - return insertChar16(length, codeUnit, field); + /** + * Sets the index at which append operations insert. Defaults to the end. + * + * @param index The index at which append operations should insert. + */ + public void setAppendIndex(int index) { + appendOffset = length - index; } - public int insertChar16(int index, char codeUnit, Field field) { + public int appendChar16(char codeUnit, Object field) { + return insertChar16(length - appendOffset, codeUnit, field); + } + + public int insertChar16(int index, char codeUnit, Object field) { int count = 1; int position = prepareForInsert(index, count); chars[position] = codeUnit; @@ -123,8 +137,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. */ - public int appendCodePoint(int codePoint, Field field) { - return insertCodePoint(length, codePoint, field); + public int appendCodePoint(int codePoint, Object field) { + return insertCodePoint(length - appendOffset, codePoint, field); } /** @@ -132,7 +146,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. */ - public int insertCodePoint(int index, int codePoint, Field field) { + public int insertCodePoint(int index, int codePoint, Object field) { int count = Character.charCount(codePoint); int position = prepareForInsert(index, count); Character.toChars(codePoint, chars, position); @@ -147,8 +161,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int append(CharSequence sequence, Field field) { - return insert(length, sequence, field); + public int append(CharSequence sequence, Object field) { + return insert(length - appendOffset, sequence, field); } /** @@ -156,7 +170,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int insert(int index, CharSequence sequence, Field field) { + public int insert(int index, CharSequence sequence, Object field) { if (sequence.length() == 0) { // Nothing to insert. return 0; @@ -175,7 +189,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of CharSequence. */ - public int insert(int index, CharSequence sequence, int start, int end, Field field) { + public int insert(int index, CharSequence sequence, int start, int end, Object field) { int count = end - start; int position = prepareForInsert(index, count); for (int i = 0; i < count; i++) { @@ -199,7 +213,7 @@ public class FormattedStringBuilder implements CharSequence { CharSequence sequence, int startOther, int endOther, - Field field) { + Object field) { int thisLength = endThis - startThis; int otherLength = endOther - startOther; int count = otherLength - thisLength; @@ -224,8 +238,8 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of the char array. */ - public int append(char[] chars, Field[] fields) { - return insert(length, chars, fields); + public int append(char[] chars, Object[] fields) { + return insert(length - appendOffset, chars, fields); } /** @@ -234,7 +248,7 @@ public class FormattedStringBuilder implements CharSequence { * * @return The number of chars added, which is the length of the char array. */ - public int insert(int index, char[] chars, Field[] fields) { + public int insert(int index, char[] chars, Object[] fields) { assert fields == null || chars.length == fields.length; int count = chars.length; if (count == 0) @@ -253,7 +267,7 @@ public class FormattedStringBuilder implements CharSequence { * @return The number of chars added, which is the length of the other {@link FormattedStringBuilder}. */ public int append(FormattedStringBuilder other) { - return insert(length, other); + return insert(length - appendOffset, other); } /** @@ -288,6 +302,9 @@ public class FormattedStringBuilder implements CharSequence { * @return The position in the char array to insert the chars. */ private int prepareForInsert(int index, int count) { + if (index == -1) { + index = length; + } if (index == 0 && zero - count >= 0) { // Append to start zero -= count; @@ -309,13 +326,13 @@ public class FormattedStringBuilder implements CharSequence { int oldCapacity = getCapacity(); int oldZero = zero; char[] oldChars = chars; - Field[] oldFields = fields; + Object[] oldFields = fields; if (length + count > oldCapacity) { int newCapacity = (length + count) * 2; int newZero = newCapacity / 2 - (length + count) / 2; char[] newChars = new char[newCapacity]; - Field[] newFields = new Field[newCapacity]; + Object[] newFields = new Object[newCapacity]; // First copy the prefix and then the suffix, leaving room for the new chars that the // caller wants to insert. @@ -408,7 +425,7 @@ public class FormattedStringBuilder implements CharSequence { return new String(chars, zero, length); } - private static final Map fieldToDebugChar = new HashMap<>(); + private static final Map fieldToDebugChar = new HashMap<>(); static { fieldToDebugChar.put(NumberFormat.Field.SIGN, '-'); @@ -459,17 +476,59 @@ public class FormattedStringBuilder implements CharSequence { } /** @return A new array containing the field values of this string builder. */ - public Field[] toFieldArray() { + public Object[] toFieldArray() { return Arrays.copyOfRange(fields, zero, zero + length); } + /** + * Call this method before using any of the Appendable overrides. + * + * @param field The field used when inserting strings. + */ + public void setAppendableField(Object field) { + appendableField = field; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(CharSequence csq) { + assert appendableField != null; + insert(length - appendOffset, csq, appendableField); + return this; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(CharSequence csq, int start, int end) { + assert appendableField != null; + insert(length - appendOffset, csq, start, end, appendableField); + return this; + } + + /** + * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take + * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. + */ + @Override + public Appendable append(char c) { + assert appendableField != null; + insertChar16(length - appendOffset, c, appendableField); + return this; + } + /** * @return Whether the contents and field values of this string builder are equal to the given chars * and fields. * @see #toCharArray * @see #toFieldArray */ - public boolean contentEquals(char[] chars, Field[] fields) { + public boolean contentEquals(char[] chars, Object[] fields) { if (chars.length != length) return false; if (fields.length != length) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java index 8bfe7a5ec5f..0fdd3bed892 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java @@ -8,7 +8,9 @@ import java.text.FieldPosition; import java.text.Format.Field; import com.ibm.icu.text.ConstrainedFieldPosition; +import com.ibm.icu.text.ListFormatter; import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.UFormat; import com.ibm.icu.text.UnicodeSet; /** @@ -24,6 +26,33 @@ import com.ibm.icu.text.UnicodeSet; */ public class FormattedValueStringBuilderImpl { + /** + * Placeholder field used for calculating spans. + * Does not currently support nested fields beyond one level. + */ + public static class SpanFieldPlaceholder { + public UFormat.SpanField spanField; + public Field normalField; + public Object value; + } + + /** + * Finds the index at which a span field begins. + * + * @param value The value of the span field to search for. + * @return The index, or -1 if not found. + */ + public static int findSpan(FormattedStringBuilder self, Object value) { + for (int i = self.zero; i < self.zero + self.length; i++) { + if (!(self.fields[i] instanceof SpanFieldPlaceholder)) { + continue; + } + if (((SpanFieldPlaceholder) self.fields[i]).value.equals(value)) { + return i - self.zero; + } + } + return -1; + } public static boolean nextFieldPosition(FormattedStringBuilder self, FieldPosition fp) { java.text.Format.Field rawField = fp.getFieldAttribute(); @@ -78,7 +107,11 @@ public class FormattedValueStringBuilderImpl { AttributedString as = new AttributedString(self.toString()); while (nextPosition(self, cfpos, numericField)) { // Backwards compatibility: field value = field - as.addAttribute(cfpos.getField(), cfpos.getField(), cfpos.getStart(), cfpos.getLimit()); + Object value = cfpos.getFieldValue(); + if (value == null) { + value = cfpos.getField(); + } + as.addAttribute(cfpos.getField(), value, cfpos.getStart(), cfpos.getLimit()); } return as.getIterator(); } @@ -102,15 +135,20 @@ public class FormattedValueStringBuilderImpl { */ public static boolean nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField) { int fieldStart = -1; - Field currField = null; + Object currField = null; for (int i = self.zero + cfpos.getLimit(); i <= self.zero + self.length; i++) { - Field _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END; + Object _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END; // Case 1: currently scanning a field. if (currField != null) { if (currField != _field) { int end = i - self.zero; + // Handle span fields; don't trim them + if (currField instanceof SpanFieldPlaceholder) { + assert handleSpan(currField, cfpos, fieldStart, end); + return true; + } // Grouping separators can be whitespace; don't throw them out! - if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + if (isTrimmable(currField)) { end = trimBack(self, end); } if (end <= fieldStart) { @@ -121,10 +159,10 @@ public class FormattedValueStringBuilderImpl { continue; } int start = fieldStart; - if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + if (isTrimmable(currField)) { start = trimFront(self, start); } - cfpos.setState(currField, null, start, end); + cfpos.setState((Field) currField, null, start, end); return true; } continue; @@ -154,6 +192,15 @@ public class FormattedValueStringBuilderImpl { cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero); return true; } + // Special case: emit normalField if we are pointing at the end of spanField. + if (i > self.zero + && self.fields[i-1] instanceof SpanFieldPlaceholder) { + int j = i - 1; + for (; j >= self.zero && self.fields[j] == self.fields[i-1]; j--) {} + if (handleSpan(self.fields[i-1], cfpos, j - self.zero + 1, i - self.zero)) { + return true; + } + } // Special case: skip over INTEGER; will be coalesced later. if (_field == NumberFormat.Field.INTEGER) { _field = null; @@ -163,7 +210,16 @@ public class FormattedValueStringBuilderImpl { continue; } // Case 3: check for field starting at this position - if (cfpos.matchesField(_field, null)) { + // Case 3a: SpanField placeholder + if (_field instanceof SpanFieldPlaceholder) { + SpanFieldPlaceholder ph = (SpanFieldPlaceholder) _field; + if (cfpos.matchesField(ph.normalField, null) || cfpos.matchesField(ph.spanField, ph.value)) { + fieldStart = i - self.zero; + currField = _field; + } + } + // Case 3b: No SpanField + else if (cfpos.matchesField((Field) _field, null)) { fieldStart = i - self.zero; currField = _field; } @@ -173,14 +229,19 @@ public class FormattedValueStringBuilderImpl { return false; } - private static boolean isIntOrGroup(Field field) { + private static boolean isIntOrGroup(Object field) { return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR; } - private static boolean isNumericField(Field field) { + private static boolean isNumericField(Object field) { return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass()); } + private static boolean isTrimmable(Object field) { + return field != NumberFormat.Field.GROUPING_SEPARATOR + && !(field instanceof ListFormatter.Field); + } + private static int trimBack(FormattedStringBuilder self, int limit) { return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) .spanBack(self, limit, UnicodeSet.SpanCondition.CONTAINED); @@ -190,4 +251,19 @@ public class FormattedValueStringBuilderImpl { return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) .span(self, start, UnicodeSet.SpanCondition.CONTAINED); } + + private static boolean handleSpan(Object field, ConstrainedFieldPosition cfpos, int start, int limit) { + SpanFieldPlaceholder ph = (SpanFieldPlaceholder) field; + if (cfpos.matchesField(ph.spanField, ph.value) + && cfpos.getLimit() < limit) { + cfpos.setState(ph.spanField, ph.value, start, limit); + return true; + } + if (cfpos.matchesField(ph.normalField, null) + && (cfpos.getLimit() < limit || cfpos.getField() != ph.normalField)) { + cfpos.setState(ph.normalField, null, start, limit); + return true; + } + return false; + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java index 216b58a3fa1..8458534c746 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java @@ -8,6 +8,11 @@ */ package com.ibm.icu.impl; +import java.io.IOException; +import java.text.Format; + +import com.ibm.icu.util.ICUUncheckedIOException; + /** * Formats simple patterns like "{1} was born in {0}". * Internal version of {@link com.ibm.icu.text.SimpleFormatter} @@ -304,17 +309,124 @@ public final class SimpleFormatterImpl { return sb.toString(); } - /** Poor-man's iterator interface. See ICU-20406. */ - public static class Int64Iterator { + /** + * Returns the length of the pattern text with none of the arguments. + * @param compiledPattern Compiled form of a pattern string. + * @param codePoints true to count code points; false to count code units. + * @return The number of code points or code units. + */ + public static int getLength(String compiledPattern, boolean codePoints) { + int result = 0; + for (int i = 1; i < compiledPattern.length();) { + int segmentLength = compiledPattern.charAt(i++) - ARG_NUM_LIMIT; + if (segmentLength > 0) { + int limit = i + segmentLength; + if (codePoints) { + result += Character.codePointCount(compiledPattern, i, limit); + } else { + result += (limit - i); + } + i = limit; + } + } + return result; + } + + /** + * Returns the length in code units of the pattern text up until the first argument. + * @param compiledPattern Compiled form of a pattern string. + * @return The number of code units. + */ + public static int getPrefixLength(String compiledPattern) { + if (compiledPattern.length() == 1) { + return 0; + } else if (compiledPattern.charAt(0) == 0) { + return compiledPattern.length() - 2; + } else if (compiledPattern.charAt(1) <= ARG_NUM_LIMIT) { + return 0; + } else { + return compiledPattern.charAt(1) - ARG_NUM_LIMIT; + } + } + + /** + * Special case for using FormattedStringBuilder with patterns with 0 or 1 argument. + * + * With 1 argument, treat the current contents of the FormattedStringBuilder between + * start and end as the argument {0}. Insert the extra strings from compiledPattern + * to surround the argument in the output. + * + * With 0 arguments, overwrite the entire contents of the FormattedStringBuilder + * between start and end. + * + * @param compiledPattern Compiled form of a pattern string. + * @param field Field to use when adding chars to the output. + * @param start The start index of the argument already in the output string. + * @param end The end index of the argument already in the output string. + * @param output Destination for formatted output. + * @return Net number of characters added to the formatted string. + */ + public static int formatPrefixSuffix( + String compiledPattern, + Format.Field field, + int start, + int end, + FormattedStringBuilder output) { + int argLimit = getArgumentLimit(compiledPattern); + if (argLimit == 0) { + // No arguments in compiled pattern; overwrite the entire segment with our string. + return output.splice(start, end, compiledPattern, 2, compiledPattern.length(), field); + } else { + assert argLimit == 1; + int suffixOffset; + int length = 0; + if (compiledPattern.charAt(1) != '\u0000') { + int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; + length = output.insert(start, compiledPattern, 2, 2 + prefixLength, field); + suffixOffset = 3 + prefixLength; + } else { + suffixOffset = 2; + } + if (suffixOffset < compiledPattern.length()) { + int suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT; + length += output.insert(end + length, compiledPattern, 1 + suffixOffset, + 1 + suffixOffset + suffixLength, field); + } + return length; + } + } + + /** Internal iterator interface for maximum efficiency. + * + * Usage boilerplate: + * + *
+     * long state = 0;
+     * while (true) {
+     *     state = IterInternal.step(state, compiledPattern, output);
+     *     if (state == IterInternal.DONE) {
+     *         break;
+     *     }
+     *     int argIndex = IterInternal.getArgIndex(state);
+     *     // Append the string corresponding to argIndex to output
+     * }
+     * 
+ * + */ + public static class IterInternal { public static final long DONE = -1; - public static long step(CharSequence compiledPattern, long state, StringBuffer output) { + public static long step(long state, CharSequence compiledPattern, Appendable output) { int i = (int) (state >>> 32); assert i < compiledPattern.length(); i++; while (i < compiledPattern.length() && compiledPattern.charAt(i) > ARG_NUM_LIMIT) { int limit = i + compiledPattern.charAt(i) + 1 - ARG_NUM_LIMIT; - output.append(compiledPattern, i + 1, limit); + try { + output.append(compiledPattern, i + 1, limit); + } catch (IOException e) { + throw new ICUUncheckedIOException(e); + } i = limit; } if (i == compiledPattern.length()) { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java index 39d57c4618d..d1c0b48c10d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java @@ -18,8 +18,8 @@ public class ConstantMultiFieldModifier implements Modifier { // value and is treated internally as immutable. protected final char[] prefixChars; protected final char[] suffixChars; - protected final Field[] prefixFields; - protected final Field[] suffixFields; + protected final Object[] prefixFields; + protected final Object[] suffixFields; private final boolean overwrite; private final boolean strong; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java index e0f3be3fb05..66646453ed0 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java @@ -2,8 +2,6 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; -import java.text.Format.Field; - import com.ibm.icu.impl.FormattedStringBuilder; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.NumberFormat; @@ -125,7 +123,7 @@ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix. // This works even if the last code point in the prefix is 2 code units because the // field value gets populated to both indices in the field array. - Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) + Object affixField = (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index); if (affixField != NumberFormat.Field.CURRENCY) { return 0; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java index 6241848a259..b80ef777e48 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java @@ -17,9 +17,6 @@ public class SimpleModifier implements Modifier { private final String compiledPattern; private final Field field; private final boolean strong; - private final int prefixLength; - private final int suffixOffset; - private final int suffixLength; // Parameters: used for number range formatting private final Parameters parameters; @@ -39,53 +36,21 @@ public class SimpleModifier implements Modifier { this.field = field; this.strong = strong; this.parameters = parameters; - - int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern); - if (argLimit == 0) { - // No arguments in compiled pattern - prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; - assert 2 + prefixLength == compiledPattern.length(); - // Set suffixOffset = -1 to indicate no arguments in compiled pattern. - suffixOffset = -1; - suffixLength = 0; - } else { - assert argLimit == 1; - if (compiledPattern.charAt(1) != '\u0000') { - prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; - suffixOffset = 3 + prefixLength; - } else { - prefixLength = 0; - suffixOffset = 2; - } - if (3 + prefixLength < compiledPattern.length()) { - suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT; - } else { - suffixLength = 0; - } - } } @Override public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) { - return formatAsPrefixSuffix(output, leftIndex, rightIndex); + return SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, field, leftIndex, rightIndex, output); } @Override public int getPrefixLength() { - return prefixLength; + return SimpleFormatterImpl.getPrefixLength(compiledPattern); } @Override public int getCodePointCount() { - int count = 0; - if (prefixLength > 0) { - count += Character.codePointCount(compiledPattern, 2, 2 + prefixLength); - } - if (suffixLength > 0) { - count += Character - .codePointCount(compiledPattern, 1 + suffixOffset, 1 + suffixOffset + suffixLength); - } - return count; + return SimpleFormatterImpl.getLength(compiledPattern, true); } @Override @@ -117,49 +82,6 @@ public class SimpleModifier implements Modifier { return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong; } - /** - * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because - * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not - * depend on it. - * - *

- * Formats a value that is already stored inside the StringBuilder result between the - * indices startIndex and endIndex by inserting characters before the start - * index and after the end index. - * - *

- * This is well-defined only for patterns with exactly one argument. - * - * @param result - * The StringBuilder containing the value argument. - * @param startIndex - * The left index of the value within the string builder. - * @param endIndex - * The right index of the value within the string builder. - * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. - */ - public int formatAsPrefixSuffix( - FormattedStringBuilder result, - int startIndex, - int endIndex) { - if (suffixOffset == -1) { - // There is no argument for the inner number; overwrite the entire segment with our string. - return result.splice(startIndex, endIndex, compiledPattern, 2, 2 + prefixLength, field); - } else { - if (prefixLength > 0) { - result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field); - } - if (suffixLength > 0) { - result.insert(endIndex + prefixLength, - compiledPattern, - 1 + suffixOffset, - 1 + suffixOffset + suffixLength, - field); - } - return prefixLength + suffixLength; - } - } - /** * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code. * I put it here so that the SimpleFormatter uses in FormattedStringBuilder are near each other. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java index 3e16328efb1..e009d958663 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java @@ -1032,11 +1032,11 @@ public class DateIntervalFormat extends UFormat { fInfo.getFallbackIntervalPattern(), patternSB, 2, 2); long state = 0; while (true) { - state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo); - if (state == SimpleFormatterImpl.Int64Iterator.DONE) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { break; } - if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) { + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { if (output != null) { output.register(0); } @@ -1090,11 +1090,11 @@ public class DateIntervalFormat extends UFormat { // {1} is single date portion long state = 0; while (true) { - state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo); - if (state == SimpleFormatterImpl.Int64Iterator.DONE) { + state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo); + if (state == SimpleFormatterImpl.IterInternal.DONE) { break; } - if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) { + if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) { fDateFormat.applyPattern(fTimePattern); fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes); } else { 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 2162e62b342..d288e217dce 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 @@ -8,19 +8,25 @@ */ package com.ibm.icu.text; -import java.io.IOException; +import java.io.InvalidObjectException; +import java.text.AttributedCharacterIterator; +import java.text.Format; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Locale; +import com.ibm.icu.impl.FormattedStringBuilder; +import com.ibm.icu.impl.FormattedValueStringBuilderImpl; +import com.ibm.icu.impl.FormattedValueStringBuilderImpl.SpanFieldPlaceholder; import com.ibm.icu.impl.ICUCache; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleCache; import com.ibm.icu.impl.SimpleFormatterImpl; -import com.ibm.icu.util.ICUUncheckedIOException; +import com.ibm.icu.impl.SimpleFormatterImpl.IterInternal; +import com.ibm.icu.impl.Utility; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; @@ -41,6 +47,7 @@ final public class ListFormatter { /** * Indicates the style of Listformatter + * TODO(ICU-20888): Remove this in ICU 68. * @internal * @deprecated This API is ICU internal only. */ @@ -98,6 +105,242 @@ final public class ListFormatter { } + /** + * Type of meaning expressed by the list. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public enum Type { + /** + * Conjunction formatting, e.g. "Alice, Bob, Charlie, and Delta". + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + AND, + + /** + * Disjunction (or alternative, or simply one of) formatting, e.g. + * "Alice, Bob, Charlie, or Delta". + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + OR, + + /** + * Formatting of a list of values with units, e.g. "5 pounds, 12 ounces". + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + UNITS + }; + + /** + * Verbosity level of the list patterns. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public enum Width { + /** + * Use list formatting with full words (no abbreviations) when possible. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + WIDE, + + /** + * Use list formatting of typical length. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + SHORT, + + /** + * Use list formatting of the shortest possible length. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + NARROW, + }; + + /** + * Class for span fields in FormattedDateInterval. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static final class SpanField extends UFormat.SpanField { + private static final long serialVersionUID = 3563544214705634403L; + + /** + * The concrete field used for spans in FormattedList. + * + * Instances of LIST_SPAN should have an associated value, the index + * within the input list that is represented by the span. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static final SpanField LIST_SPAN = new SpanField("list-span"); + + private SpanField(String name) { + super(name); + } + + /** + * serizalization method resolve instances to the constant + * ListFormatter.SpanField values + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(LIST_SPAN.getName())) + return LIST_SPAN; + + throw new InvalidObjectException("An invalid object."); + } + } + + /** + * Field selectors for format fields defined by ListFormatter. + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static final class Field extends Format.Field { + private static final long serialVersionUID = -8071145668708265437L; + + /** + * The literal text in the result which came from the resources. + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static Field LITERAL = new Field("literal"); + + /** + * The element text in the result which came from the input strings. + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static Field ELEMENT = new Field("element"); + + private Field(String name) { + super(name); + } + + /** + * Serizalization method resolve instances to the constant Field values + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(LITERAL.getName())) + return LITERAL; + if (this.getName().equals(ELEMENT.getName())) + return ELEMENT; + + throw new InvalidObjectException("An invalid object."); + } + } + + /** + * An immutable class containing the result of a list formatting operation. + * + * Instances of this class are immutable and thread-safe. + * + * Not intended for public subclassing. + * + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static final class FormattedList implements FormattedValue { + private final FormattedStringBuilder string; + + FormattedList(FormattedStringBuilder string) { + this.string = string; + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public String toString() { + return string.toString(); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int length() { + return string.length(); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public char charAt(int index) { + return string.charAt(index); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public CharSequence subSequence(int start, int end) { + return string.subString(start, end); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public A appendTo(A appendable) { + return Utility.appendTo(string, appendable); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + return FormattedValueStringBuilderImpl.nextPosition(string, cfpos, null); + } + + /** + * {@inheritDoc} + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + @Override + public AttributedCharacterIterator toCharacterIterator() { + return FormattedValueStringBuilderImpl.toCharacterIterator(string, null); + } + } + /** * Internal: Create a ListFormatter from component strings, * with definitions as in LDML. @@ -139,6 +382,50 @@ final public class ListFormatter { return SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); } + /** + * Create a list formatter that is appropriate for a locale. + * + * @param locale + * the locale in question. + * @return ListFormatter + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static ListFormatter getInstance(ULocale locale, Type type, Width width) { + String styleName = typeWidthToStyleString(type, width); + if (styleName == null) { + throw new IllegalArgumentException("Invalid list format type/width"); + } + return cache.get(locale, styleName); + } + + /** + * Create a list formatter that is appropriate for a locale. + * + * @param locale + * the locale in question. + * @return ListFormatter + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public static ListFormatter getInstance(Locale locale, Type type, Width width) { + return getInstance(ULocale.forLocale(locale), type, width); + } + + /** + * Create a list formatter that is appropriate for a locale and style. + * + * @param locale the locale in question. + * @param style the style + * @return ListFormatter + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static ListFormatter getInstance(ULocale locale, Style style) { + return cache.get(locale, style.getName()); + } + /** * Create a list formatter that is appropriate for a locale. * @@ -163,20 +450,6 @@ final public class ListFormatter { return getInstance(ULocale.forLocale(locale), Style.STANDARD); } - /** - * Create a list formatter that is appropriate for a locale and style. - * - * @param locale the locale in question. - * @param style the style - * @return ListFormatter - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public static ListFormatter getInstance(ULocale locale, Style style) { - return cache.get(locale, style.getName()); - } - /** * Create a list formatter that is appropriate for the default FORMAT locale. * @@ -208,30 +481,59 @@ final public class ListFormatter { * @stable ICU 50 */ public String format(Collection items) { - return format(items, -1).toString(); + return formatImpl(items, false).toString(); + } + + /** + * Format a list of objects to a FormattedList. You can access the offsets + * of each element from the FormattedList. + * + * @param items + * items to format. The toString() method is called on each. + * @return items formatted into a FormattedList + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public FormattedList formatToValue(Object... items) { + return formatToValue(Arrays.asList(items)); + } + + + /** + * Format a collection of objects to a FormattedList. You can access the offsets + * of each element from the FormattedList. + * + * @param items + * items to format. The toString() method is called on each. + * @return items formatted into a FormattedList + * @draft ICU 67 + * @provisional This API might change or be removed in a future release. + */ + public FormattedList formatToValue(Collection items) { + return formatImpl(items, true).toValue(); } // 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) { + FormattedListBuilder formatImpl(Collection items, boolean needsFields) { Iterator it = items.iterator(); int count = items.size(); switch (count) { case 0: - return new FormattedListBuilder("", false); + return new FormattedListBuilder("", needsFields); case 1: - return new FormattedListBuilder(it.next(), index == 0); + return new FormattedListBuilder(it.next(), needsFields); case 2: - return new FormattedListBuilder(it.next(), index == 0).append(two, it.next(), index == 1); + return new FormattedListBuilder(it.next(), needsFields).append(two, it.next(), 1); } - FormattedListBuilder builder = new FormattedListBuilder(it.next(), index == 0); - builder.append(start, it.next(), index == 1); + FormattedListBuilder builder = new FormattedListBuilder(it.next(), needsFields); + builder.append(start, it.next(), 1); for (int idx = 2; idx < count - 1; ++idx) { - builder.append(middle, it.next(), index == idx); + builder.append(middle, it.next(), idx); } - return builder.append(end, it.next(), index == count - 1); + return builder.append(end, it.next(), count - 1); } /** @@ -246,7 +548,7 @@ final public class ListFormatter { if (count <= 0) { throw new IllegalArgumentException("count must be > 0"); } - ArrayList list = new ArrayList(); + ArrayList list = new ArrayList<>(); for (int i = 0; i < count; i++) { list.add(String.format("{%d}", i)); } @@ -265,64 +567,74 @@ final public class ListFormatter { // Builds a formatted list static class FormattedListBuilder { - private StringBuilder current; - private int offset; + private FormattedStringBuilder string; + boolean needsFields; - // 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 = new StringBuilder(start.toString()); - this.offset = recordOffset ? 0 : -1; + // Start is the first object in the list; If needsFields is true, enable the slightly + // more expensive code path that records offsets of each element. + public FormattedListBuilder(Object start, boolean needsFields) { + string = new FormattedStringBuilder(); + this.needsFields = needsFields; + string.setAppendableField(Field.LITERAL); + appendElement(start, 0); } // 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(String pattern, Object next, boolean recordOffset) { - int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null; - SimpleFormatterImpl.formatAndReplace( - pattern, current, offsets, current, next.toString()); - if (offsets != null) { - if (offsets[0] == -1 || offsets[1] == -1) { - throw new IllegalArgumentException( - "{0} or {1} missing from pattern " + pattern); + // represents the new object in pattern. next is the object to be added. position is the + // index of the next object in the list of inputs. + public FormattedListBuilder append(String compiledPattern, Object next, int position) { + assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 2; + string.setAppendIndex(0); + long state = 0; + while (true) { + state = IterInternal.step(state, compiledPattern, string); + if (state == IterInternal.DONE) { + break; } - if (recordOffset) { - offset = offsets[1]; + int argIndex = IterInternal.getArgIndex(state); + if (argIndex == 0) { + string.setAppendIndex(string.length()); } else { - offset += offsets[0]; + appendElement(next, position); } } return this; } - public void appendTo(Appendable appendable) { - try { - appendable.append(current); - } catch(IOException e) { - throw new ICUUncheckedIOException(e); + private void appendElement(Object element, int position) { + if (needsFields) { + SpanFieldPlaceholder field = new SpanFieldPlaceholder(); + field.spanField = SpanField.LIST_SPAN; + field.normalField = Field.ELEMENT; + field.value = position; + string.append(element.toString(), field); + } else { + string.append(element.toString(), null); } } + public void appendTo(Appendable appendable) { + Utility.appendTo(string, appendable); + } + + public int getOffset(int fieldPositionFoundIndex) { + return FormattedValueStringBuilderImpl.findSpan(string, fieldPositionFoundIndex); + } + @Override public String toString() { - return current.toString(); + return string.toString(); } - // Gets the last recorded offset or -1 if no offset recorded. - public int getOffset() { - return offset; - } - - private boolean offsetRecorded() { - return offset >= 0; + public FormattedList toValue() { + return new FormattedList(string); } } private static class Cache { private final ICUCache cache = - new SimpleCache(); + new SimpleCache<>(); public ListFormatter get(ULocale locale, String style) { String key = String.format("%s:%s", locale.toString(), style); @@ -348,4 +660,42 @@ final public class ListFormatter { } static Cache cache = new Cache(); + + static String typeWidthToStyleString(Type type, Width width) { + switch (type) { + case AND: + switch (width) { + case WIDE: + return "standard"; + case SHORT: + return "standard-short"; + case NARROW: + return "standard-narrow"; + } + break; + + case OR: + switch (width) { + case WIDE: + return "or"; + case SHORT: + return "or-short"; + case NARROW: + return "or-narrow"; + } + break; + + case UNITS: + switch (width) { + case WIDE: + return "unit"; + case SHORT: + return "unit-short"; + case NARROW: + return "unit-narrow"; + } + } + + return null; + } } 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 4925c63d088..fb7799c9bda 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 @@ -464,7 +464,7 @@ public class MeasureFormat extends UFormat { results[i] = formatMeasureInteger(measures[i]).toString(); } } - FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), -1); + FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), false); builder.appendTo(appendTo); } @@ -811,13 +811,13 @@ public class MeasureFormat extends UFormat { } results[i] = result.toString(); } - ListFormatter.FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), - fieldPositionFoundIndex); + ListFormatter.FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), true); // Fix up FieldPosition indexes if our field is found. - if (builder.getOffset() != -1) { - fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset()); - fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset()); + int offset = builder.getOffset(fieldPositionFoundIndex); + if (offset != -1) { + fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset); + fieldPosition.setEndIndex(fpos.getEndIndex() + offset); } builder.appendTo(appendTo); } 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 949bace5b66..05ca49fbd29 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 @@ -8,7 +8,6 @@ */ package com.ibm.icu.text; -import java.io.IOException; import java.io.InvalidObjectException; import java.text.AttributedCharacterIterator; import java.text.Format; @@ -24,13 +23,12 @@ import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.SoftCache; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.UResource; +import com.ibm.icu.impl.Utility; import com.ibm.icu.impl.number.DecimalQuantity; import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; -import com.ibm.icu.impl.number.SimpleModifier; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.ICUException; -import com.ibm.icu.util.ICUUncheckedIOException; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; @@ -531,13 +529,7 @@ public final class RelativeDateTimeFormatter { */ @Override public A appendTo(A appendable) { - try { - appendable.append(string); - } catch (IOException e) { - // Throw as an unchecked exception to avoid users needing try/catch - throw new ICUUncheckedIOException(e); - } - return appendable; + return Utility.appendTo(string, appendable); } /** @@ -723,8 +715,7 @@ public final class RelativeDateTimeFormatter { StandardPlural pluralForm = StandardPlural.orOtherFromString(pluralKeyword); String compiledPattern = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); - SimpleModifier modifier = new SimpleModifier(compiledPattern, Field.LITERAL, false); - modifier.formatAsPrefixSuffix(output, 0, output.length()); + SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, Field.LITERAL, 0, output.length(), output); return output; } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java index a7398a073a9..095a6d0ccef 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java @@ -8,7 +8,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import java.text.FieldPosition; -import java.text.Format.Field; import org.junit.Test; @@ -170,7 +169,7 @@ public class FormattedStringBuilderTest { FormattedStringBuilder sb = new FormattedStringBuilder(); sb.append(str, null); sb.append(str, NumberFormat.Field.CURRENCY); - Field[] fields = sb.toFieldArray(); + Object[] fields = sb.toFieldArray(); assertEquals(str.length() * 2, fields.length); for (int i = 0; i < str.length(); i++) { assertEquals(null, fields[i]); @@ -198,7 +197,7 @@ public class FormattedStringBuilderTest { int numNull = 0; int numCurr = 0; int numInt = 0; - Field[] oldFields = fields; + Object[] oldFields = fields; fields = sb.toFieldArray(); for (int i = 0; i < sb.length(); i++) { assertEquals(oldFields[i % oldFields.length], fields[i]); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java index 2d0367f3f44..8a1d30a678b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java @@ -9,6 +9,7 @@ package com.ibm.icu.dev.test.format; import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; import org.junit.Test; @@ -17,6 +18,9 @@ import org.junit.runners.JUnit4; import com.ibm.icu.dev.test.TestFmwk; import com.ibm.icu.text.ListFormatter; +import com.ibm.icu.text.ListFormatter.FormattedList; +import com.ibm.icu.text.ListFormatter.Type; +import com.ibm.icu.text.ListFormatter.Width; import com.ibm.icu.util.ULocale; @RunWith(JUnit4.class) @@ -209,4 +213,83 @@ public class ListFormatterTest extends TestFmwk { ULocale defaultLocale = ULocale.getDefault(ULocale.Category.FORMAT); return defaultLocale.equals(ULocale.ENGLISH) || defaultLocale.equals(ULocale.US); } + + @Test + public void TestFormattedValue() { + ListFormatter fmt = ListFormatter.getInstance(ULocale.ENGLISH); + + { + String message = "Field position test 1"; + String expectedString = "hello, wonderful, and world"; + String[] inputs = { + "hello", + "wonderful", + "world" + }; + FormattedList result = fmt.formatToValue(Arrays.asList(inputs)); + Object[][] expectedFieldPositions = new Object[][] { + // field, begin index, end index + {ListFormatter.SpanField.LIST_SPAN, 0, 5, 0}, + {ListFormatter.Field.ELEMENT, 0, 5}, + {ListFormatter.Field.LITERAL, 5, 7}, + {ListFormatter.SpanField.LIST_SPAN, 7, 16, 1}, + {ListFormatter.Field.ELEMENT, 7, 16}, + {ListFormatter.Field.LITERAL, 16, 22}, + {ListFormatter.SpanField.LIST_SPAN, 22, 27, 2}, + {ListFormatter.Field.ELEMENT, 22, 27}}; + FormattedValueTest.checkFormattedValue( + message, + result, + expectedString, + expectedFieldPositions); + } + } + + @Test + public void TestCreateStyled() { + // Locale en has interesting data + Object[][] cases = { + { "pt", Type.AND, Width.WIDE, "A, B e C" }, + { "pt", Type.AND, Width.SHORT, "A, B e C" }, + { "pt", Type.AND, Width.NARROW, "A, B, C" }, + { "pt", Type.OR, Width.WIDE, "A, B ou C" }, + { "pt", Type.OR, Width.SHORT, "A, B ou C" }, + { "pt", Type.OR, Width.NARROW, "A, B ou C" }, + { "pt", Type.UNITS, Width.WIDE, "A, B e C" }, + { "pt", Type.UNITS, Width.SHORT, "A, B e C" }, + { "pt", Type.UNITS, Width.NARROW, "A B C" }, + { "en", Type.AND, Width.WIDE, "A, B, and C" }, + { "en", Type.AND, Width.SHORT, "A, B, & C" }, + { "en", Type.AND, Width.NARROW, "A, B, C" }, + { "en", Type.OR, Width.WIDE, "A, B, or C" }, + { "en", Type.OR, Width.SHORT, "A, B, or C" }, + { "en", Type.OR, Width.NARROW, "A, B, or C" }, + { "en", Type.UNITS, Width.WIDE, "A, B, C" }, + { "en", Type.UNITS, Width.SHORT, "A, B, C" }, + { "en", Type.UNITS, Width.NARROW, "A B C" }, + }; + for (Object[] cas : cases) { + Locale loc = new Locale((String) cas[0]); + ULocale uloc = new ULocale((String) cas[0]); + Type type = (Type) cas[1]; + Width width = (Width) cas[2]; + String expected = (String) cas[3]; + ListFormatter fmt1 = ListFormatter.getInstance(loc, type, width); + ListFormatter fmt2 = ListFormatter.getInstance(uloc, type, width); + String message = "TestCreateStyled loc=" + + loc + " type=" + + type + " width=" + + width; + String[] inputs = { + "A", + "B", + "C" + }; + String result = fmt1.format(Arrays.asList(inputs)); + assertEquals(message, expected, result); + // Coverage for the other factory method overload: + result = fmt2.format(Arrays.asList(inputs)); + assertEquals(message, expected, result); + } + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java index 8d5eeb46d54..1ddf2394340 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java @@ -30,6 +30,7 @@ import com.ibm.icu.text.DateIntervalInfo; import com.ibm.icu.text.DecimalFormat; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.DurationFormat; +import com.ibm.icu.text.ListFormatter; import com.ibm.icu.text.MessageFormat; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralFormat; @@ -1831,6 +1832,36 @@ public class FormatHandler } } + public static class ListFormatterFieldHandler implements SerializableTestUtility.Handler + { + @Override + public Object[] getTestObjects() + { + return new Object[] {ListFormatter.Field.ELEMENT, ListFormatter.Field.LITERAL}; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) + { + return (a == b); + } + } + + public static class ListFormatterSpanFieldHandler implements SerializableTestUtility.Handler + { + @Override + public Object[] getTestObjects() + { + return new Object[] {ListFormatter.SpanField.LIST_SPAN}; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) + { + return (a == b); + } + } + public static class DateFormatHandler implements SerializableTestUtility.Handler { static HashMap cannedPatterns = new HashMap(); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java index 7dba946f038..5d4ef839a61 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java @@ -820,6 +820,8 @@ public class SerializableTestUtility { map.put("com.ibm.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler()); map.put("com.ibm.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler()); map.put("com.ibm.icu.text.DateIntervalFormat$SpanField", new FormatHandler.DateIntervalSpanFieldHandler()); + map.put("com.ibm.icu.text.ListFormatter$Field", new FormatHandler.ListFormatterFieldHandler()); + map.put("com.ibm.icu.text.ListFormatter$SpanField", new FormatHandler.ListFormatterSpanFieldHandler()); map.put("com.ibm.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler()); map.put("com.ibm.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler());