blob: f6d8d1661fe66a3f51ffa7c0b13ed8e22bf1b535 [file] [log] [blame]
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
*******************************************************************************
* Copyright (C) 2012-2016, Google, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.text;
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 java.util.regex.Pattern;
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.impl.SimpleFormatterImpl.IterInternal;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* Immutable class for formatting a list, using data from CLDR (or supplied
* separately). The class is not subclassable.
*
* @author Mark Davis
* @stable ICU 50
*/
final public class ListFormatter {
// Compiled SimpleFormatter patterns.
private final String start;
private final String middle;
private final ULocale locale;
private interface PatternHandler {
public String getTwoPattern(String text);
public String getEndPattern(String text);
}
private final PatternHandler patternHandler;
/**
* Indicates the style of Listformatter
* TODO(ICU-20888): Remove this in ICU 68.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public enum Style {
/**
* Standard, conjunction style.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
STANDARD("standard"),
/**
* Disjunction style.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
OR("or"),
/**
* Style for full units
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
UNIT("unit"),
/**
* Style for units in abbrevated form
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
UNIT_SHORT("unit-short"),
/**
* Style for units in narrow form
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
UNIT_NARROW("unit-narrow");
private final String name;
Style(String name) {
this.name = name;
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String getName() {
return name;
}
}
/**
* 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 FormattedList.
*
* @draft ICU 67
* @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 67
* @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);
}
/**
* serialization 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 extends Appendable> 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);
}
}
/**
* <b>Internal:</b> Create a ListFormatter from component strings,
* with definitions as in LDML.
*
* @param two
* string for two items, containing {0} for the first, and {1}
* for the second.
* @param start
* string for the start of a list items, containing {0} for the
* first, and {1} for the rest.
* @param middle
* string for the start of a list items, containing {0} for the
* first part of the list, and {1} for the rest of the list.
* @param end
* string for the end of a list items, containing {0} for the
* first part of the list, and {1} for the last item.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public ListFormatter(String two, String start, String middle, String end) {
this(
compilePattern(two, new StringBuilder()),
compilePattern(start, new StringBuilder()),
compilePattern(middle, new StringBuilder()),
compilePattern(end, new StringBuilder()),
null);
}
private ListFormatter(String two, String start, String middle, String end, ULocale locale) {
this.start = start;
this.middle = middle;
this.locale = locale;
this.patternHandler = createPatternHandler(two, end);
}
private static String compilePattern(String pattern, StringBuilder sb) {
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.
*
* @param locale
* the locale in question.
* @return ListFormatter
* @stable ICU 50
*/
public static ListFormatter getInstance(ULocale locale) {
return getInstance(locale, Style.STANDARD);
}
/**
* Create a list formatter that is appropriate for a locale.
*
* @param locale
* the locale in question.
* @return ListFormatter
* @stable ICU 50
*/
public static ListFormatter getInstance(Locale locale) {
return getInstance(ULocale.forLocale(locale), Style.STANDARD);
}
/**
* Create a list formatter that is appropriate for the default FORMAT locale.
*
* @return ListFormatter
* @stable ICU 50
*/
public static ListFormatter getInstance() {
return getInstance(ULocale.getDefault(ULocale.Category.FORMAT));
}
/**
* Format a list of objects.
*
* @param items
* items to format. The toString() method is called on each.
* @return items formatted into a string
* @stable ICU 50
*/
public String format(Object... items) {
return format(Arrays.asList(items));
}
/**
* Format a collection of objects. The toString() method is called on each.
*
* @param items
* items to format. The toString() method is called on each.
* @return items formatted into a string
* @stable ICU 50
*/
public String format(Collection<?> items) {
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 formatImpl(Collection<?> items, boolean needsFields) {
Iterator<?> it = items.iterator();
int count = items.size();
switch (count) {
case 0:
return new FormattedListBuilder("", needsFields);
case 1:
return new FormattedListBuilder(it.next(), needsFields);
case 2:
Object first = it.next();
Object second = it.next();
return new FormattedListBuilder(first, needsFields)
.append(patternHandler.getTwoPattern(String.valueOf(second)), second, 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(), idx);
}
Object last = it.next();
return builder.append(patternHandler.getEndPattern(String.valueOf(last)), last, count - 1);
}
// A static handler just returns the pattern without considering the input text.
private static final class StaticHandler implements PatternHandler {
StaticHandler(String two, String end) {
twoPattern = two;
endPattern = end;
}
@Override
public String getTwoPattern(String text) { return twoPattern; }
@Override
public String getEndPattern(String text) { return endPattern; }
private final String twoPattern;
private final String endPattern;
}
// A contextual handler returns one of the two patterns depending on whether the text matched the regexp.
private static final class ContextualHandler implements PatternHandler {
ContextualHandler(Pattern regexp, String thenTwo, String elseTwo, String thenEnd, String elseEnd) {
this.regexp = regexp;
thenTwoPattern = thenTwo;
elseTwoPattern = elseTwo;
thenEndPattern = thenEnd;
elseEndPattern = elseEnd;
}
@Override
public String getTwoPattern(String text) {
if(regexp.matcher(text).matches()) {
return thenTwoPattern;
} else {
return elseTwoPattern;
}
}
@Override
public String getEndPattern(String text) {
if(regexp.matcher(text).matches()) {
return thenEndPattern;
} else {
return elseEndPattern;
}
}
private final Pattern regexp;
private final String thenTwoPattern;
private final String elseTwoPattern;
private final String thenEndPattern;
private final String elseEndPattern;
}
// Pattern in the ICU Data which might be replaced y by e.
private static final String compiledY = compilePattern("{0} y {1}", new StringBuilder());
// The new pattern to replace y to e
private static final String compiledE = compilePattern("{0} e {1}", new StringBuilder());
// Pattern in the ICU Data which might be replaced o by u.
private static final String compiledO = compilePattern("{0} o {1}", new StringBuilder());
// The new pattern to replace u to o
private static final String compiledU = compilePattern("{0} u {1}", new StringBuilder());
// Condition to change to e.
// Starts with "hi" or "i" but not with "hie" nor "hia"a
private static final Pattern changeToE = Pattern.compile("(i.*|hi|hi[^ae].*)", Pattern.CASE_INSENSITIVE);
// Condition to change to u.
// Starts with "o", "ho", and "8". Also "11" by itself.
private static final Pattern changeToU = Pattern.compile("((o|ho|8).*|11)", Pattern.CASE_INSENSITIVE);
// Pattern in the ICU Data which might need to add a DASH after VAV.
private static final String compiledVav = compilePattern("{0} \u05D5{1}", new StringBuilder());
// Pattern to add a DASH after VAV.
private static final String compiledVavDash = compilePattern("{0} \u05D5-{1}", new StringBuilder());
// Condition to change to VAV follow by a dash.
// Starts with non Hebrew letter.
private static final Pattern changeToVavDash = Pattern.compile("^[\\P{InHebrew}].*$");
// A factory function to create function based on locale
// Handle specal case of Spanish and Hebrew
private PatternHandler createPatternHandler(String two, String end) {
if (this.locale != null) {
String language = this.locale.getLanguage();
if (language.equals("es")) {
boolean twoIsY = two.equals(compiledY);
boolean endIsY = end.equals(compiledY);
if (twoIsY || endIsY) {
return new ContextualHandler(
changeToE, twoIsY ? compiledE : two, two, endIsY ? compiledE : end, end);
}
boolean twoIsO = two.equals(compiledO);
boolean endIsO = end.equals(compiledO);
if (twoIsO || endIsO) {
return new ContextualHandler(
changeToU, twoIsO ? compiledU : two, two, endIsO ? compiledU : end, end);
}
} else if (language.equals("he") || language.equals("iw")) {
boolean twoIsVav = two.equals(compiledVav);
boolean endIsVav = end.equals(compiledVav);
if (twoIsVav || endIsVav) {
return new ContextualHandler(changeToVavDash,
twoIsVav ? compiledVavDash : two, two, endIsVav ? compiledVavDash : end, end);
}
}
}
return new StaticHandler(two, end);
}
/**
* Returns the pattern to use for a particular item count.
* @param count the item count.
* @return the pattern with {0}, {1}, {2}, etc. For English,
* getPatternForNumItems(3) == "{0}, {1}, and {2}"
* @throws IllegalArgumentException when count is 0 or negative.
* @stable ICU 52
*/
public String getPatternForNumItems(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must be > 0");
}
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(String.format("{%d}", i));
}
return format(list);
}
/**
* Returns the locale of this object.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public ULocale getLocale() {
return locale;
}
// Builds a formatted list
static class FormattedListBuilder {
private FormattedStringBuilder string;
boolean needsFields;
// 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. 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;
}
int argIndex = IterInternal.getArgIndex(state);
if (argIndex == 0) {
string.setAppendIndex(string.length());
} else {
appendElement(next, position);
}
}
return this;
}
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 string.toString();
}
public FormattedList toValue() {
return new FormattedList(string);
}
}
private static class Cache {
private final ICUCache<String, ListFormatter> cache =
new SimpleCache<>();
public ListFormatter get(ULocale locale, String style) {
String key = String.format("%s:%s", locale.toString(), style);
ListFormatter result = cache.get(key);
if (result == null) {
result = load(locale, style);
cache.put(key, result);
}
return result;
}
private static ListFormatter load(ULocale ulocale, String style) {
ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
StringBuilder sb = new StringBuilder();
return new ListFormatter(
compilePattern(r.getWithFallback("listPattern/" + style + "/2").getString(), sb),
compilePattern(r.getWithFallback("listPattern/" + style + "/start").getString(), sb),
compilePattern(r.getWithFallback("listPattern/" + style + "/middle").getString(), sb),
compilePattern(r.getWithFallback("listPattern/" + style + "/end").getString(), sb),
ulocale);
}
}
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;
}
}