ICU-21351 Don't coalesce adjacent list formatter fields in ICU4J
diff --git a/icu4c/source/i18n/formattedval_sbimpl.cpp b/icu4c/source/i18n/formattedval_sbimpl.cpp
index 84c2d00..64601f4 100644
--- a/icu4c/source/i18n/formattedval_sbimpl.cpp
+++ b/icu4c/source/i18n/formattedval_sbimpl.cpp
@@ -170,7 +170,10 @@
auto elementField = fString.getFieldPtr()[i-1];
if (elementField == Field(UFIELD_CATEGORY_LIST, ULISTFMT_ELEMENT_FIELD)
&& cfpos.matchesField(elementField.getCategory(), elementField.getField())
- && (cfpos.getLimit() < i - fString.fZero || cfpos.getCategory() != elementField.getCategory())) {
+ && (
+ cfpos.getLimit() < i - fString.fZero
+ || cfpos.getCategory() != elementField.getCategory()
+ || cfpos.getField() != elementField.getField())) {
int64_t si = cfpos.getInt64IterationContext() - 1;
cfpos.setState(
elementField.getCategory(),
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 43f0c4d..938eb13 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
@@ -34,6 +34,7 @@
public UFormat.SpanField spanField;
public Field normalField;
public Object value;
+ public int length;
}
/**
@@ -142,12 +143,6 @@
if (currField != null) {
if (currField != _field) {
int end = i - self.zero;
- // Handle span fields; don't trim them
- if (currField instanceof SpanFieldPlaceholder) {
- boolean handleResult = handleSpan(currField, cfpos, fieldStart, end);
- assert handleResult;
- return true;
- }
// Grouping separators can be whitespace; don't throw them out!
if (isTrimmable(currField)) {
end = trimBack(self, end);
@@ -188,6 +183,7 @@
&& (i - self.zero > cfpos.getLimit() || cfpos.getField() != numericField)
&& isNumericField(self.fields[i - 1])
&& !isNumericField(_field)) {
+ // Re-wind to the beginning of the field and then emit it
int j = i - 1;
for (; j >= self.zero && isNumericField(self.fields[j]); j--) {}
cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero);
@@ -196,9 +192,11 @@
// 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)) {
+ SpanFieldPlaceholder ph = (SpanFieldPlaceholder) self.fields[i-1];
+ if (cfpos.matchesField(ph.normalField, null)
+ && (cfpos.getLimit() < i - self.zero
+ || cfpos.getField() != ph.normalField)) {
+ cfpos.setState(ph.normalField, null, i - self.zero - ph.length, i - self.zero);
return true;
}
}
@@ -214,9 +212,15 @@
// Case 3a: SpanField placeholder
if (_field instanceof SpanFieldPlaceholder) {
SpanFieldPlaceholder ph = (SpanFieldPlaceholder) _field;
- if (cfpos.matchesField(ph.normalField, null) || cfpos.matchesField(ph.spanField, ph.value)) {
+ if (cfpos.matchesField(ph.spanField, ph.value)) {
fieldStart = i - self.zero;
- currField = _field;
+ int end = fieldStart + ph.length;
+ cfpos.setState(ph.spanField, ph.value, fieldStart, end);
+ return true;
+ } else {
+ // Failed to match; jump ahead
+ i += ph.length - 1;
+ continue;
}
}
// Case 3b: No SpanField
@@ -258,19 +262,4 @@
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/text/ListFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
index 10503d4..e746e8f 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
@@ -724,14 +724,16 @@
}
private void appendElement(Object element, int position) {
+ String elementString = element.toString();
if (needsFields) {
SpanFieldPlaceholder field = new SpanFieldPlaceholder();
field.spanField = SpanField.LIST_SPAN;
field.normalField = Field.ELEMENT;
field.value = position;
- string.append(element.toString(), field);
+ field.length = elementString.length();
+ string.append(elementString, field);
} else {
- string.append(element.toString(), null);
+ string.append(elementString, null);
}
}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java
index 51a39f4..227abdd 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java
@@ -148,6 +148,11 @@
public static void checkFormattedValue(String message, FormattedValue fv, String expectedString,
Object[][] expectedFieldPositions) {
+ checkFormattedValue(message, fv, expectedString, expectedFieldPositions, false);
+ }
+
+ public static void checkFormattedValue(String message, FormattedValue fv, String expectedString,
+ Object[][] expectedFieldPositions, boolean skipAttributedCharacterIterator) {
// Calculate some initial expected values
int stringLength = fv.toString().length();
HashSet<Format.Field> uniqueFields = new HashSet<>();
@@ -169,6 +174,12 @@
assertEquals(baseMessage + "Iterator should have length of string output", stringLength, fpi.getEndIndex());
int i = 0;
for (char c = fpi.first(); c != AttributedCharacterIterator.DONE; c = fpi.next(), i++) {
+ // Strings with adjacent fields cannot be disambiguated using AttributedCharacterIterator,
+ // so skip this part of the test for strings with adjacent fields.
+ if (skipAttributedCharacterIterator) {
+ i = stringLength;
+ break;
+ }
Set<AttributedCharacterIterator.Attribute> currentAttributes = fpi.getAttributes().keySet();
int attributesRemaining = currentAttributes.size();
for (Object[] cas : expectedFieldPositions) {
@@ -181,22 +192,22 @@
continue;
}
- assertTrue(baseMessage + "Character at " + i + " should have field " + expectedField,
+ assertTrue(baseMessage + "A Character at " + i + " should have field " + expectedField,
currentAttributes.contains(expectedField));
- assertTrue(baseMessage + "Field " + expectedField + " should be a known attribute",
+ assertTrue(baseMessage + "A Field " + expectedField + " should be a known attribute",
allAttributes.contains(expectedField));
int actualBeginIndex = fpi.getRunStart(expectedField);
int actualEndIndex = fpi.getRunLimit(expectedField);
Object actualValue = fpi.getAttribute(expectedField);
- assertEquals(baseMessage + expectedField + " begin @" + i, expectedBeginIndex, actualBeginIndex);
- assertEquals(baseMessage + expectedField + " end @" + i, expectedEndIndex, actualEndIndex);
- assertEquals(baseMessage + expectedField + " value @" + i, expectedValue, actualValue);
+ assertEquals(baseMessage + expectedField + " A begin @" + i, expectedBeginIndex, actualBeginIndex);
+ assertEquals(baseMessage + expectedField + " A end @" + i, expectedEndIndex, actualEndIndex);
+ assertEquals(baseMessage + expectedField + " A value @" + i, expectedValue, actualValue);
attributesRemaining--;
}
- assertEquals(baseMessage + "Should have looked at every field: " + i + ": " + currentAttributes,
+ assertEquals(baseMessage + "A Should have looked at every field: " + i + ": " + currentAttributes,
0, attributesRemaining);
}
- assertEquals(baseMessage + "Should have looked at every character", stringLength, i);
+ assertEquals(baseMessage + "A Should have looked at every character", stringLength, i);
// Check nextPosition over all fields
ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition();
@@ -207,16 +218,16 @@
int expectedStart = (Integer) cas[1];
int expectedLimit = (Integer) cas[2];
Object expectedValue = cas.length == 4 ? cas[3] : null;
- assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
- assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
- assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
- assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
+ assertEquals(baseMessage + "B field " + i, expectedField, cfpos.getField());
+ assertEquals(baseMessage + "B start " + i, expectedStart, cfpos.getStart());
+ assertEquals(baseMessage + "B limit " + i, expectedLimit, cfpos.getLimit());
+ assertEquals(baseMessage + "B value " + i, expectedValue, cfpos.getFieldValue());
i++;
}
boolean afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "B after loop: " + cfpos, afterLoopResult);
afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop again: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "B after loop again: " + cfpos, afterLoopResult);
// Check nextPosition constrained over each class one at a time
for (Class<?> classConstraint : uniqueFieldClasses) {
@@ -232,22 +243,22 @@
int expectedStart = (Integer) cas[1];
int expectedLimit = (Integer) cas[2];
Object expectedValue = cas.length == 4 ? cas[3] : null;
- assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
- assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
- assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
- assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
+ assertEquals(baseMessage + "C field " + i, expectedField, cfpos.getField());
+ assertEquals(baseMessage + "C start " + i, expectedStart, cfpos.getStart());
+ assertEquals(baseMessage + "C limit " + i, expectedLimit, cfpos.getLimit());
+ assertEquals(baseMessage + "C value " + i, expectedValue, cfpos.getFieldValue());
i++;
}
afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "C after loop: " + cfpos, afterLoopResult);
afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop again: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "C after loop again: " + cfpos, afterLoopResult);
}
// Check nextPosition constrained over an unrelated class
cfpos.reset();
cfpos.constrainClass(HashSet.class);
- assertFalse(baseMessage + "unrelated class", fv.nextPosition(cfpos));
+ assertFalse(baseMessage + "C unrelated class", fv.nextPosition(cfpos));
// Check nextPosition constrained over each field one at a time
for (Format.Field field : uniqueFields) {
@@ -263,16 +274,16 @@
int expectedStart = (Integer) cas[1];
int expectedLimit = (Integer) cas[2];
Object expectedValue = cas.length == 4 ? cas[3] : null;
- assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
- assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
- assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
- assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
+ assertEquals(baseMessage + "D field " + i, expectedField, cfpos.getField());
+ assertEquals(baseMessage + "D start " + i, expectedStart, cfpos.getStart());
+ assertEquals(baseMessage + "D limit " + i, expectedLimit, cfpos.getLimit());
+ assertEquals(baseMessage + "D value " + i, expectedValue, cfpos.getFieldValue());
i++;
}
afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "D after loop: " + cfpos, afterLoopResult);
afterLoopResult = fv.nextPosition(cfpos);
- assertFalse(baseMessage + "after loop again: " + cfpos, afterLoopResult);
+ assertFalse(baseMessage + "D after loop again: " + cfpos, afterLoopResult);
}
}
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 9297752..9407bd5 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
@@ -262,15 +262,15 @@
{ListFormatter.Field.ELEMENT, 2, 9},
{ListFormatter.SpanField.LIST_SPAN, 9, 12, 2},
{ListFormatter.Field.ELEMENT, 9, 12}};
- if (!logKnownIssue("21351", "Java still coalesces adjacent elements")) {
- FormattedValueTest.checkFormattedValue(
- message,
- result,
- expectedString,
- expectedFieldPositions);
- }
+ FormattedValueTest.checkFormattedValue(
+ message,
+ result,
+ expectedString,
+ expectedFieldPositions,
+ // Adjacent fields: skip AttributedCharacterIterator test
+ true);
}
-
+
{
ListFormatter fmt = ListFormatter.getInstance(ULocale.ENGLISH, Type.UNITS, Width.SHORT);
String message = "ICU-21383 Long list";