396 lines
15 KiB
Java
396 lines
15 KiB
Java
/* GENERATED SOURCE. DO NOT MODIFY. */
|
|
// © 2022 and later: Unicode, Inc. and others.
|
|
// License & terms of use: http://www.unicode.org/copyright.html
|
|
package android.icu.impl.personname;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.StringTokenizer;
|
|
|
|
import android.icu.text.PersonName;
|
|
|
|
/**
|
|
* A single name formatting pattern, corresponding to a single namePattern element in CLDR.
|
|
*/
|
|
class PersonNamePattern {
|
|
private String patternText; // for debugging
|
|
private Element[] patternElements;
|
|
|
|
public static PersonNamePattern[] makePatterns(String[] patternText, PersonNameFormatterImpl formatterImpl) {
|
|
PersonNamePattern[] result = new PersonNamePattern[patternText.length];
|
|
for (int i = 0; i < patternText.length; i++) {
|
|
result[i] = new PersonNamePattern(patternText[i], formatterImpl);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return patternText;
|
|
}
|
|
|
|
private PersonNamePattern(String patternText, PersonNameFormatterImpl formatterImpl) {
|
|
this.patternText = patternText;
|
|
|
|
List<Element> elements = new ArrayList<>();
|
|
boolean inField = false;
|
|
boolean inEscape = false;
|
|
StringBuilder workingString = new StringBuilder();
|
|
for (int i = 0; i < patternText.length(); i++) {
|
|
char c = patternText.charAt(i);
|
|
|
|
if (inEscape) {
|
|
workingString.append(c);
|
|
inEscape = false;
|
|
} else {
|
|
switch (c) {
|
|
case '\\':
|
|
inEscape = true;
|
|
break;
|
|
case '{':
|
|
if (!inField) {
|
|
if (workingString.length() > 0) {
|
|
elements.add(new LiteralText(workingString.toString()));
|
|
workingString = new StringBuilder();
|
|
}
|
|
inField = true;
|
|
} else {
|
|
throw new IllegalArgumentException("Nested braces are not allowed in name patterns");
|
|
}
|
|
break;
|
|
case '}':
|
|
if (inField) {
|
|
if (workingString.length() > 0) {
|
|
elements.add(new NameFieldImpl(workingString.toString(), formatterImpl));
|
|
workingString = new StringBuilder();
|
|
} else {
|
|
throw new IllegalArgumentException("No field name inside braces");
|
|
}
|
|
inField = false;
|
|
} else {
|
|
throw new IllegalArgumentException("Unmatched closing brace in literal text");
|
|
}
|
|
break;
|
|
default:
|
|
workingString.append(c);
|
|
}
|
|
}
|
|
}
|
|
if (workingString.length() > 0) {
|
|
elements.add(new LiteralText(workingString.toString()));
|
|
}
|
|
this.patternElements = elements.toArray(new Element[0]);
|
|
}
|
|
|
|
public String format(PersonName name) {
|
|
StringBuilder result = new StringBuilder();
|
|
boolean seenLeadingField = false;
|
|
boolean seenEmptyLeadingField = false;
|
|
boolean seenEmptyField = false;
|
|
StringBuilder textBefore = new StringBuilder();
|
|
StringBuilder textAfter = new StringBuilder();
|
|
|
|
// if the name doesn't have a surname field and the pattern doesn't have a given-name field,
|
|
// we actually format a modified version of the name object where the contents of the
|
|
// given-name field has been copied into the surname field
|
|
name = hackNameForEmptyFields(name);
|
|
|
|
// the logic below attempts to implement the following algorithm:
|
|
// - If one or more fields at the beginning of the name are empty, also skip all literal text
|
|
// from the beginning of the name up to the first populated field.
|
|
// - If one or more fields at the end of the name are empty, also skip all literal text from
|
|
// the last populated field to the end of the name.
|
|
// - If one or more contiguous fields in the middle of the name are empty, skip the literal text
|
|
// between them, omit characters from the literal text on either side of the empty fields up to
|
|
// the first space on either side, and make sure that the resulting literal text doesn't end up
|
|
// with two spaces in a row.
|
|
for (Element element : patternElements) {
|
|
if (element.isLiteral()) {
|
|
if (seenEmptyLeadingField) {
|
|
// do nothing; throw away the literal text
|
|
} else if (seenEmptyField) {
|
|
textAfter.append(element.format(name));
|
|
} else {
|
|
textBefore.append(element.format(name));
|
|
}
|
|
} else {
|
|
String fieldText = element.format(name);
|
|
if (fieldText == null || fieldText.isEmpty()) {
|
|
if (!seenLeadingField) {
|
|
seenEmptyLeadingField = true;
|
|
textBefore.setLength(0);
|
|
} else {
|
|
seenEmptyField = true;
|
|
textAfter.setLength(0);
|
|
}
|
|
} else {
|
|
seenLeadingField = true;
|
|
seenEmptyLeadingField = false;
|
|
if (seenEmptyField) {
|
|
result.append(coalesce(textBefore, textAfter));
|
|
result.append(fieldText);
|
|
seenEmptyField = false;
|
|
} else {
|
|
result.append(textBefore);
|
|
textBefore.setLength(0);
|
|
result.append(element.format(name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!seenEmptyField) {
|
|
result.append(textBefore);
|
|
}
|
|
return result.toString();
|
|
}
|
|
|
|
public int numPopulatedFields(PersonName name) {
|
|
int result = 0;
|
|
for (Element element : patternElements) {
|
|
result += element.isPopulated(name) ? 1 : 0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public int numEmptyFields(PersonName name) {
|
|
int result = 0;
|
|
for (Element element : patternElements) {
|
|
result += (!element.isLiteral() && !element.isPopulated(name)) ? 1 : 0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Stitches together the literal text on either side of an omitted field by deleting any
|
|
* non-whitespace characters immediately neighboring the omitted field and coalescing any
|
|
* adjacent spaces at the join point down to one.
|
|
* @param s1 The literal text before the omitted field.
|
|
* @param s2 The literal text after the omitted field.
|
|
*/
|
|
private String coalesce(StringBuilder s1, StringBuilder s2) {
|
|
// if the contents of s2 occur at the end of s1, we just use s1
|
|
if (endsWith(s1, s2)) {
|
|
s2.setLength(0);
|
|
}
|
|
|
|
// get the range of non-whitespace characters at the beginning of s1
|
|
int p1 = 0;
|
|
while (p1 < s1.length() && !Character.isWhitespace(s1.charAt(p1))) {
|
|
++p1;
|
|
}
|
|
|
|
// get the range of non-whitespace characters at the end of s2
|
|
int p2 = s2.length() - 1;
|
|
while (p2 >= 0 && !Character.isWhitespace(s2.charAt(p2))) {
|
|
--p2;
|
|
}
|
|
|
|
// also include one whitespace character from s1 or, if there aren't
|
|
// any, one whitespace character from s2
|
|
if (p1 < s1.length()) {
|
|
++p1;
|
|
} else if (p2 >= 0) {
|
|
--p2;
|
|
}
|
|
|
|
// concatenate those two ranges to get the coalesced literal text
|
|
String result = s1.substring(0, p1) + s2.substring(p2 + 1);
|
|
|
|
// clear out s1 and s2 (done here to improve readability in format() above))
|
|
s1.setLength(0);
|
|
s2.setLength(0);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns true if s1 ends with s2.
|
|
*/
|
|
private boolean endsWith(StringBuilder s1, StringBuilder s2) {
|
|
int p1 = s1.length() - 1;
|
|
int p2 = s2.length() - 1;
|
|
|
|
while (p1 >= 0 && p2 >= 0 && s1.charAt(p1) == s2.charAt(p2)) {
|
|
--p1;
|
|
--p2;
|
|
}
|
|
return p2 < 0;
|
|
}
|
|
|
|
private PersonName hackNameForEmptyFields(PersonName originalName) {
|
|
// this is a hack to deal with mononyms (name objects that don't have both a given name and a surname)--
|
|
// if the name object has a given-name field but not a surname field and the pattern either doesn't
|
|
// have a given-name field or only has "{given-initial}", we return a PersonName object that will
|
|
// return the value of the given-name field when asked for the value of the surname field and that
|
|
// will return null when asked for the value of the given-name field (all other field values and
|
|
// properties of the underlying object are returned unchanged)
|
|
PersonName result = originalName;
|
|
if (originalName.getFieldValue(PersonName.NameField.SURNAME, Collections.emptySet()) == null) {
|
|
boolean patternHasNonInitialGivenName = false;
|
|
for (PersonNamePattern.Element element : patternElements) {
|
|
if (!element.isLiteral()
|
|
&& ((NameFieldImpl)element).fieldID == PersonName.NameField.GIVEN
|
|
&& !((NameFieldImpl)element).modifiers.containsKey(PersonName.FieldModifier.INITIAL)) {
|
|
patternHasNonInitialGivenName = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!patternHasNonInitialGivenName) {
|
|
return new GivenToSurnamePersonName(originalName);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* A single element in a NamePattern. This is either a name field or a range of literal text.
|
|
*/
|
|
private interface Element {
|
|
boolean isLiteral();
|
|
String format(PersonName name);
|
|
boolean isPopulated(PersonName name);
|
|
}
|
|
|
|
/**
|
|
* Literal text from a name pattern.
|
|
*/
|
|
private static class LiteralText implements Element {
|
|
private String text;
|
|
|
|
public LiteralText(String text) {
|
|
this.text = text;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return text;
|
|
}
|
|
|
|
public boolean isLiteral() {
|
|
return true;
|
|
}
|
|
|
|
public String format(PersonName name) {
|
|
return text;
|
|
}
|
|
|
|
public boolean isPopulated(PersonName name) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An actual name field in a NamePattern (i.e., the stuff represented in the pattern by text
|
|
* in braces). This class actually handles fetching the value for the field out of a
|
|
* PersonName object and applying any modifiers to it.
|
|
*/
|
|
private static class NameFieldImpl implements Element {
|
|
private PersonName.NameField fieldID;
|
|
private Map<PersonName.FieldModifier, FieldModifierImpl> modifiers;
|
|
|
|
public NameFieldImpl(String fieldNameAndModifiers, PersonNameFormatterImpl formatterImpl) {
|
|
List<PersonName.FieldModifier> modifierIDs = new ArrayList<>();
|
|
StringTokenizer tok = new StringTokenizer(fieldNameAndModifiers, "-");
|
|
|
|
this.fieldID = PersonName.NameField.forString(tok.nextToken());
|
|
while (tok.hasMoreTokens()) {
|
|
modifierIDs.add(PersonName.FieldModifier.forString(tok.nextToken()));
|
|
}
|
|
if (this.fieldID == PersonName.NameField.SURNAME && formatterImpl.shouldCapitalizeSurname()) {
|
|
modifierIDs.add(PersonName.FieldModifier.ALL_CAPS);
|
|
}
|
|
|
|
this.modifiers = new HashMap<>();
|
|
for (PersonName.FieldModifier modifierID : modifierIDs) {
|
|
this.modifiers.put(modifierID, FieldModifierImpl.forName(modifierID, formatterImpl));
|
|
}
|
|
|
|
if (this.modifiers.containsKey(PersonName.FieldModifier.RETAIN)
|
|
&& this.modifiers.containsKey(PersonName.FieldModifier.INITIAL)) {
|
|
FieldModifierImpl.InitialModifier initialModifier
|
|
= (FieldModifierImpl.InitialModifier) this.modifiers.get(PersonName.FieldModifier.INITIAL);
|
|
initialModifier.setRetainPunctuation(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("{");
|
|
sb.append(fieldID);
|
|
for (PersonName.FieldModifier modifier : modifiers.keySet()) {
|
|
sb.append("-");
|
|
sb.append(modifier.toString());
|
|
}
|
|
sb.append("}");
|
|
return sb.toString();
|
|
}
|
|
|
|
public boolean isLiteral() {
|
|
return false;
|
|
}
|
|
|
|
public String format(PersonName name) {
|
|
Set<PersonName.FieldModifier> modifierIDs = new HashSet<>(modifiers.keySet());
|
|
String result = name.getFieldValue(fieldID, modifierIDs);
|
|
if (result != null) {
|
|
for (PersonName.FieldModifier modifierID : modifierIDs) {
|
|
result = modifiers.get(modifierID).modifyField(result);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public boolean isPopulated(PersonName name) {
|
|
String result = this.format(name);
|
|
return result != null && ! result.isEmpty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal class used when formatting a mononym (a PersonName object that only has
|
|
* a given-name field). If the name doesn't have a surname field and the pattern
|
|
* doesn't have a given-name field (or only has one that produces an initial), we
|
|
* use this class to behave as though the value supplied in the given-name field
|
|
* had instead been supplied in the surname field.
|
|
*/
|
|
private static class GivenToSurnamePersonName implements PersonName {
|
|
private PersonName underlyingPersonName;
|
|
|
|
public GivenToSurnamePersonName(PersonName underlyingPersonName) {
|
|
this.underlyingPersonName = underlyingPersonName;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Inverted version of " + underlyingPersonName.toString();
|
|
}
|
|
@Override
|
|
public Locale getNameLocale() {
|
|
return underlyingPersonName.getNameLocale();
|
|
}
|
|
|
|
@Override
|
|
public PreferredOrder getPreferredOrder() {
|
|
return underlyingPersonName.getPreferredOrder();
|
|
}
|
|
|
|
@Override
|
|
public String getFieldValue(NameField identifier, Set<FieldModifier> modifiers) {
|
|
if (identifier == NameField.SURNAME) {
|
|
return underlyingPersonName.getFieldValue(NameField.GIVEN, modifiers);
|
|
} else if (identifier == NameField.GIVEN) {
|
|
return null;
|
|
} else {
|
|
return underlyingPersonName.getFieldValue(identifier, modifiers);
|
|
}
|
|
}
|
|
}
|
|
}
|