--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.impl.personname;
+
+import com.ibm.icu.lang.UCharacter;
+import com.ibm.icu.text.BreakIterator;
+import com.ibm.icu.text.CaseMap;
+import com.ibm.icu.text.PersonNameFormatter;
+import com.ibm.icu.text.SimpleFormatter;
+import com.ibm.icu.util.ULocale;
+
+import java.util.StringTokenizer;
+
+/**
+ * Parent class for classes that implement field-modifier behavior.
+ */
+abstract class FieldModifierImpl {
+ public abstract String modifyField(String fieldValue);
+
+ public static FieldModifierImpl forName(PersonNameFormatter.FieldModifier modifierID, PersonNameFormatterImpl formatterImpl) {
+ switch (modifierID) {
+ case INFORMAL:
+ return NOOP_MODIFIER;
+ case PREFIX:
+ return NULL_MODIFIER;
+ case CORE:
+ return NOOP_MODIFIER;
+ case ALL_CAPS:
+ return new AllCapsModifier(formatterImpl.getLocale());
+ case INITIAL_CAP:
+ return new InitialCapModifier(formatterImpl.getLocale());
+ case INITIAL:
+ return new InitialModifier(formatterImpl.getInitialPattern(), formatterImpl.getInitialSequencePattern());
+ case MONOGRAM:
+ return MONOGRAM_MODIFIER;
+ default:
+ throw new IllegalArgumentException("Invalid modifier ID " + modifierID);
+ }
+ }
+
+ /**
+ * A field modifier that just returns the field value unmodified. This is used to implement the default
+ * behavior of the "informal" and "core" modifiers ("real" informal or core variants have to be supplied or
+ * calculated by the PersonName object).
+ */
+ private static final FieldModifierImpl NOOP_MODIFIER = new FieldModifierImpl() {
+ @Override
+ public String modifyField(String fieldValue) {
+ return fieldValue;
+ }
+ };
+
+ /**
+ * A field modifier that just returns the empty string. This is used to implement the default behavior of the
+ * "prefix" modifier ("real" prefix variants have to be supplied to calculated by the PersonName object).
+ */
+ private static final FieldModifierImpl NULL_MODIFIER = new FieldModifierImpl() {
+ @Override
+ public String modifyField(String fieldValue) {
+ return "";
+ }
+ };
+
+ /**
+ * A field modifier that returns the field value converted to ALL CAPS. This is the default behavior
+ * for the "allCaps" modifier.
+ */
+ private static class AllCapsModifier extends FieldModifierImpl {
+ private final ULocale locale;
+
+ public AllCapsModifier(ULocale locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ public String modifyField(String fieldValue) {
+ return UCharacter.toUpperCase(locale, fieldValue);
+ }
+ }
+
+ /**
+ * A field modifier that returns the field value with the first letter of each word capitalized. This is
+ * the default behavior of the "initialCap" modifier.
+ */
+ private static class InitialCapModifier extends FieldModifierImpl {
+ private final ULocale locale;
+ private static final CaseMap.Title TO_TITLE_WHOLE_STRING_NO_LOWERCASE = CaseMap.toTitle().wholeString().noLowercase();
+
+ public InitialCapModifier(ULocale locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ public String modifyField(String fieldValue) {
+ return TO_TITLE_WHOLE_STRING_NO_LOWERCASE.apply(locale.toLocale(), null, fieldValue);
+ }
+ }
+
+ /**
+ * A field modifier that returns the field value converted into one or more initials. This is the first grapheme
+ * cluster of each word in the field value, modified using the initialPattern/initial resource value from the
+ * locale data, and strung together using the initialPattern/initialSequence resource value from the locale data.
+ * (In English, these patterns put periods after each initial and connect them with spaces.)
+ * This is default behavior of the "initial" modifier.
+ */
+ private static class InitialModifier extends FieldModifierImpl {
+ private final SimpleFormatter initialFormatter;
+ private final SimpleFormatter initialSequenceFormatter;
+
+ public InitialModifier(String initialPattern, String initialSequencePattern) {
+ this.initialFormatter = SimpleFormatter.compile(initialPattern);
+ this.initialSequenceFormatter = SimpleFormatter.compile(initialSequencePattern);
+ }
+
+ @Override
+ public String modifyField(String fieldValue) {
+ String result = null;
+ StringTokenizer tok = new StringTokenizer(fieldValue, " ");
+ while (tok.hasMoreTokens()) {
+ String curInitial = getFirstGrapheme(tok.nextToken());
+ if (result == null) {
+ result = initialFormatter.format(curInitial);
+ } else {
+ result = initialSequenceFormatter.format(result, initialFormatter.format(curInitial));
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * A field modifier that simply returns the first grapheme cluster in the field value.
+ * This is the default implementation of the "monogram" modifier.
+ */
+ private static final FieldModifierImpl MONOGRAM_MODIFIER = new FieldModifierImpl() {
+ @Override
+ public String modifyField(String fieldValue) {
+ return getFirstGrapheme(fieldValue);
+ }
+ };
+
+ /**
+ * A utility function that just returns the first grapheme cluster in the string.
+ */
+ private static String getFirstGrapheme(String s) {
+ // early out if the string is empty to avoid StringIndexOutOfBoundsException
+ if (s.isEmpty()) {
+ return "";
+ }
+
+ // (currently, no locale overrides the grapheme-break rules, so we just use "root" instead of passing in the locale)
+ BreakIterator bi = BreakIterator.getCharacterInstance(ULocale.ROOT);
+ bi.setText(s);
+ return s.substring(0, bi.next());
+ }
+}
--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.impl.personname;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.lang.UScript;
+import com.ibm.icu.text.FormattedValue;
+import com.ibm.icu.text.PersonNameFormatter;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+import java.util.*;
+
+import static com.ibm.icu.util.UResourceBundle.ARRAY;
+import static com.ibm.icu.util.UResourceBundle.STRING;
+
+/**
+ * Actual implementation class for PersonNameFormatter.
+ */
+public class PersonNameFormatterImpl {
+ private final ULocale locale;
+ private final PersonNamePattern[] gnFirstPatterns;
+ private final PersonNamePattern[] snFirstPatterns;
+ private final Set<String> gnFirstLocales;
+ private final Set<String> snFirstLocales;
+ private final String initialPattern;
+ private final String initialSequencePattern;
+ private final boolean capitalizeSurname;
+ private final String foreignSpaceReplacement;
+ private final boolean formatterLocaleUsesSpaces;
+ private final PersonNameFormatter.Length length;
+ private final PersonNameFormatter.Usage usage;
+ private final PersonNameFormatter.Formality formality;
+ private final Set<PersonNameFormatter.Options> options;
+
+ public PersonNameFormatterImpl(ULocale locale,
+ PersonNameFormatter.Length length,
+ PersonNameFormatter.Usage usage,
+ PersonNameFormatter.Formality formality,
+ Set<PersonNameFormatter.Options> options) {
+ // null for `options` is the same as the empty set
+ if (options == null) {
+ options = new HashSet<>();
+ }
+
+ // save off our creation parameters (these are only used if we have to create a second formatter)
+ this.length = length;
+ this.usage = usage;
+ this.formality = formality;
+ this.options = options;
+
+ // load simple property values from the resource bundle (or the options set)
+ ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
+ this.locale = locale;
+ this.initialPattern = rb.getStringWithFallback("personNames/initialPattern/initial");
+ this.initialSequencePattern = rb.getStringWithFallback("personNames/initialPattern/initialSequence");
+ this.capitalizeSurname = options.contains(PersonNameFormatter.Options.SURNAME_ALLCAPS);
+ this.foreignSpaceReplacement = rb.getStringWithFallback("personNames/foreignSpaceReplacement");
+ this.formatterLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(locale.getLanguage());
+
+ // asjust for combinations of parameters that don't make sense in practice
+ if (usage == PersonNameFormatter.Usage.MONOGRAM) {
+ // we don't support SORTING in conjunction with MONOGRAM; if the caller passes in SORTING, remove it from
+ // the options list
+ options.remove(PersonNameFormatter.Options.SORTING);
+ } else if (options.contains(PersonNameFormatter.Options.SORTING)) {
+ // we only support SORTING in conjunction with REFERRING; if the caller passes in ADDRESSING, treat it
+ // the same as REFERRING
+ usage = PersonNameFormatter.Usage.REFERRING;
+ }
+
+ // load the actual formatting patterns-- since we don't know the name order until formatting time (it can be
+ // different for different names), load patterns for both GN-first and SN-first names. (If the user has
+ // specified SORTING, we don't need to do this-- we just load the "sorting" patterns and ignore the name's order.)
+ final String RESOURCE_PATH_PREFIX = "personNames/namePattern/";
+ String resourceNameBody = length.toString().toLowerCase() + "-" + usage.toString().toLowerCase() + "-"
+ + formality.toString().toLowerCase();
+ if (!options.contains(PersonNameFormatter.Options.SORTING)) {
+ ICUResourceBundle gnFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "givenFirst-" + resourceNameBody);
+ ICUResourceBundle snFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "surnameFirst-" + resourceNameBody);
+
+ gnFirstPatterns = PersonNamePattern.makePatterns(asStringArray(gnFirstResource), this);
+ snFirstPatterns = PersonNamePattern.makePatterns(asStringArray(snFirstResource), this);
+
+ gnFirstLocales = new HashSet<>();
+ Collections.addAll(gnFirstLocales, asStringArray(rb.getWithFallback("personNames/nameOrderLocales/givenFirst")));
+ snFirstLocales = new HashSet<>();
+ Collections.addAll(snFirstLocales, asStringArray(rb.getWithFallback("personNames/nameOrderLocales/surnameFirst")));
+ } else {
+ ICUResourceBundle patternResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "sorting-" + resourceNameBody);
+
+ gnFirstPatterns = PersonNamePattern.makePatterns(asStringArray(patternResource), this);
+ snFirstPatterns = null;
+ gnFirstLocales = null;
+ snFirstLocales = null;
+ }
+ }
+
+ public String format(PersonNameFormatter.PersonName name) {
+ // TODO: Should probably return a FormattedPersonName object
+
+ // if the formatter is for a language that doesn't use spaces between words and the name is from a language
+ // that does, create a formatter for the NAME'S locale and use THAT to format the name
+ ULocale nameLocale = name.getNameLocale();
+ boolean nameLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(nameLocale.getLanguage());
+ if (!formatterLocaleUsesSpaces && nameLocaleUsesSpaces) {
+ PersonNameFormatterImpl nativeFormatter = new PersonNameFormatterImpl(nameLocale, this.length,
+ this.usage, this.formality, this.options);
+ String result = nativeFormatter.format(name);
+
+ // BUT, if the name is actually written in the formatter locale's script, replace any spaces in the name
+ // with the foreignSpaceReplacement character
+ if (!foreignSpaceReplacement.equals(" ") && scriptMatchesLocale(result, this.locale)) {
+ result = result.replace(" ", this.foreignSpaceReplacement);
+ }
+ return result;
+ }
+
+ // if we get down to here, we're just doing normal formatting-- if we have both GN-first and SN-first rules,
+ // choose which one to use based on the name's locale and preferred field order
+ if (snFirstPatterns == null || nameIsGnFirst(name)) {
+ return getBestPattern(gnFirstPatterns, name).format(name);
+ } else {
+ return getBestPattern(snFirstPatterns, name).format(name);
+ }
+ }
+
+ public ULocale getLocale() {
+ return locale;
+ }
+
+ public String getInitialPattern() {
+ return initialPattern;
+ }
+
+ public String getInitialSequencePattern() {
+ return initialSequencePattern;
+ }
+
+ public boolean shouldCapitalizeSurname() {
+ return capitalizeSurname;
+ }
+
+ private final Set<String> LOCALES_THAT_DONT_USE_SPACES = new HashSet<>(Arrays.asList("ja", "zh", "th", "yue"));
+
+ /**
+ * Returns the value of the resource, as a string array.
+ * @param resource An ICUResourceBundle of type STRING or ARRAY. If ARRAY, this function just returns it
+ * as a string array. If STRING, it returns a one-element array containing that string.
+ * @return The resource's value, as an array of Strings.
+ */
+ private String[] asStringArray(ICUResourceBundle resource) {
+ if (resource.getType() == STRING) {
+ return new String[] { resource.getString() };
+ } else if (resource.getType() == ARRAY){
+ return resource.getStringArray();
+ } else {
+ throw new IllegalStateException("Unsupported resource type " + resource.getType());
+ }
+ }
+
+ /**
+ * Returns the field order to use when formatting this name, taking into account the name's preferredOrder
+ * field, as well as the name and formatter's respective locales.
+ * @param name The name to be formatted.
+ * @return If true, use GN-first order to format the name; if false, use SN-first order.
+ */
+ private boolean nameIsGnFirst(PersonNameFormatter.PersonName name) {
+ // the name can declare its order-- check that first (it overrides any locale-based calculation)
+ Set<PersonNameFormatter.FieldModifier> modifiers = new HashSet<>();
+ String preferredOrder = name.getFieldValue(PersonNameFormatter.NameField.PREFERRED_ORDER, modifiers);
+ if (preferredOrder != null) {
+ if (preferredOrder.equals("givenFirst")) {
+ return true;
+ } else if (preferredOrder.equals("surnameFirst")) {
+ return false;
+ } else {
+ throw new IllegalArgumentException("Illegal preferredOrder value " + preferredOrder);
+ }
+ }
+
+ String localeStr = name.getNameLocale().toString();
+ do {
+ if (gnFirstLocales.contains(localeStr)) {
+ return true;
+ } else if (snFirstLocales.contains(localeStr)) {
+ return false;
+ }
+
+ int lastUnderbarPos = localeStr.lastIndexOf("_");
+ if (lastUnderbarPos >= 0) {
+ localeStr = localeStr.substring(0, lastUnderbarPos);
+ } else {
+ localeStr = "root";
+ }
+ } while (!localeStr.equals("root"));
+
+ // should never get here-- "root" should always be in one of the locales
+ return true;
+ }
+
+ private PersonNamePattern getBestPattern(PersonNamePattern[] patterns, PersonNameFormatter.PersonName name) {
+ // early out if there's only one pattern
+ if (patterns.length == 1) {
+ return patterns[0];
+ } else {
+ // if there's more than one pattern, return the one that contains the greatest number of fields that
+ // actually have values in `name`. If there's a tie, return the pattern that contains the lowest number
+ // of fields that DON'T have values in `name`.
+ int maxPopulatedFields = 0;
+ int minEmptyFields = Integer.MAX_VALUE;
+ PersonNamePattern bestPattern = null;
+
+ for (PersonNamePattern pattern : patterns) {
+ int populatedFields = pattern.numPopulatedFields(name);
+ int emptyFields = pattern.numEmptyFields(name);
+ if (populatedFields > maxPopulatedFields) {
+ maxPopulatedFields = populatedFields;
+ minEmptyFields = emptyFields;
+ bestPattern = pattern;
+ } else if (populatedFields == maxPopulatedFields && emptyFields < minEmptyFields) {
+ minEmptyFields = emptyFields;
+ bestPattern = pattern;
+ }
+ }
+ return bestPattern;
+ }
+ }
+
+ /**
+ * Returns true if the script of `s` is one of the default scripts for `locale`.
+ * This function only checks the script of the first character whose script isn't "common,"
+ * so it probably won't work right on mixed-script strings.
+ */
+ private boolean scriptMatchesLocale(String s, ULocale locale) {
+ int[] localeScripts = UScript.getCode(locale);
+ int stringScript = UScript.COMMON;
+ for (int i = 0; stringScript == UScript.COMMON && i < s.length(); i++) {
+ char c = s.charAt(i);
+ stringScript = UScript.getScript(c);
+ }
+
+ for (int localeScript : localeScripts) {
+ if (localeScript == stringScript) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.impl.personname;
+
+import com.ibm.icu.text.PersonNameFormatter;
+
+import java.util.*;
+
+/**
+ * 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;
+ }
+
+ 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(PersonNameFormatter.PersonName name) {
+ StringBuilder result = new StringBuilder();
+ boolean seenLeadingField = false;
+ boolean seenEmptyLeadingField = false;
+ boolean seenEmptyField = false;
+ StringBuilder textBefore = new StringBuilder();
+ StringBuilder textAfter = new StringBuilder();
+
+ // 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(PersonNameFormatter.PersonName name) {
+ int result = 0;
+ for (Element element : patternElements) {
+ result += element.isPopulated(name) ? 1 : 0;
+ }
+ return result;
+ }
+
+ public int numEmptyFields(PersonNameFormatter.PersonName name) {
+ int result = 0;
+ for (Element element : patternElements) {
+ result += element.isPopulated(name) ? 0 : 1;
+ }
+ 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) {
+ // 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;
+ }
+
+ /**
+ * 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(PersonNameFormatter.PersonName name);
+ boolean isPopulated(PersonNameFormatter.PersonName name);
+ }
+
+ /**
+ * Literal text from a name pattern.
+ */
+ private static class LiteralText implements Element {
+ private String text;
+
+ public LiteralText(String text) {
+ this.text = text;
+ }
+
+ public boolean isLiteral() {
+ return true;
+ }
+
+ public String format(PersonNameFormatter.PersonName name) {
+ return text;
+ }
+
+ public boolean isPopulated(PersonNameFormatter.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 PersonNameFormatter.NameField fieldID;
+ private Map<PersonNameFormatter.FieldModifier, FieldModifierImpl> modifiers;
+
+ public NameFieldImpl(String fieldNameAndModifiers, PersonNameFormatterImpl formatterImpl) {
+ List<PersonNameFormatter.FieldModifier> modifierIDs = new ArrayList<>();
+ StringTokenizer tok = new StringTokenizer(fieldNameAndModifiers, "-");
+
+ this.fieldID = PersonNameFormatter.NameField.forString(tok.nextToken());
+ while (tok.hasMoreTokens()) {
+ modifierIDs.add(PersonNameFormatter.FieldModifier.forString(tok.nextToken()));
+ }
+ if (this.fieldID == PersonNameFormatter.NameField.SURNAME && formatterImpl.shouldCapitalizeSurname()) {
+ modifierIDs.add(PersonNameFormatter.FieldModifier.ALL_CAPS);
+ }
+
+ this.modifiers = new HashMap<>();
+ for (PersonNameFormatter.FieldModifier modifierID : modifierIDs) {
+ this.modifiers.put(modifierID, FieldModifierImpl.forName(modifierID, formatterImpl));
+ }
+ }
+
+ public boolean isLiteral() {
+ return false;
+ }
+
+ public String format(PersonNameFormatter.PersonName name) {
+ Set<PersonNameFormatter.FieldModifier> modifierIDs = new HashSet<>(modifiers.keySet());
+ String result = name.getFieldValue(fieldID, modifierIDs);
+ if (result != null) {
+ for (PersonNameFormatter.FieldModifier modifierID : modifierIDs) {
+ result = modifiers.get(modifierID).modifyField(result);
+ }
+ }
+ return result;
+ }
+
+ public boolean isPopulated(PersonNameFormatter.PersonName name) {
+ // just check whether the unmodified field contains a value
+ Set<PersonNameFormatter.FieldModifier> modifierIDs = new HashSet<>();
+ String fieldValue = name.getFieldValue(fieldID, modifierIDs);
+ return fieldValue != null && !fieldValue.isEmpty();
+ }
+ }
+}
--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.text;
+
+import com.ibm.icu.impl.personname.PersonNameFormatterImpl;
+import com.ibm.icu.util.ULocale;
+
+import java.util.Set;
+
+/**
+ * A class for formatting names of people. Takes raw name data for a person and renders it into a string according to
+ * the caller's specifications, taking into account how people's names are rendered in the caller's locale.
+ *
+ * The Length, Usage, and Formality options can be used to get a wide variety of results. In English, they would
+ * produce results along these lines:
+ *
+ * | | REFERRING | REFERRING | ADDRESSING | ADDRESSING | MONOGRAM | MONOGRAM |
+ * | | FORMAL | INFORMAL | FORMAL | INFORMAL | FORMAL | INFORMAL |
+ * |--------|-----------------------|--------------|------------|------------|----------|----------|
+ * | LONG | James Earl Carter Jr. | Jimmy Carter | Mr. Carter | Jimmy | JEC | JC |
+ * | MEDIUM | James E. Carter Jr. | Jimmy Carter | Mr. Carter | Jimmy | C | J |
+ * | SHORT | J. E. Carter | Jimmy Carter | Mr. Carter | Jimmy | C | J |
+ *
+ * @internal
+ */
+public class PersonNameFormatter {
+ //==============================================================================
+ // Parameters that control formatting behavior
+
+ /**
+ * Specifies the desired length of the formatted name.
+ * @internal
+ */
+ public enum Length {
+ /**
+ * The longest name length. Generally uses most of the fields in the name object.
+ * @internal
+ */
+ LONG,
+
+ /**
+ * The most typical name length. Generally includes the given name and surname, but generally
+ * nost most of the other fields.
+ * @internal
+ */
+ MEDIUM,
+
+ /**
+ * A shortened name. Skips most fields and may abbreviate some name fields to just their initials.
+ * When Formality is INFORMAL, may only include one field.
+ */
+ SHORT
+ }
+
+ /**
+ * Specifies the intended usage of the formatted name.
+ * @internal
+ */
+ public enum Usage {
+ /**
+ * Used for when the name is going to be used to address the user directly: "Turn left here, John."
+ * @internal
+ */
+ ADDRESSING,
+
+ /**
+ * Used in general cases, when the name is used to refer to somebody else.
+ * @internal
+ */
+ REFERRING,
+
+ /**
+ * Used to generate monograms, short 1 to 3-character versions of the name suitable for use in things
+ * like chat avatars. In English, this is usually the person's initials, but this isn't true in all
+ * languages. When the caller specifies Usage.MONOGRAM, the Length parameter can be used to get different
+ * lengths of monograms: Length.SHORT is generally a single letter; Length.LONG may be as many as three or four.
+ * @internal
+ */
+ MONOGRAM
+ }
+
+ /**
+ * Specifies the intended formality of the formatted name.
+ * @internal
+ */
+ public enum Formality {
+ /**
+ * The more formal version of the name.
+ * @internal
+ */
+ FORMAL,
+
+ /**
+ * The more informal version of the name. In English, this might omit fields or use the "informal" variant
+ * of the given name.
+ * @internal
+ */
+ INFORMAL
+ }
+
+ /**
+ * Additional options to customize the behavior of the formatter.
+ * @internal
+ */
+ public enum Options {
+ /**
+ * Causes the formatter to generate results suitable for inclusion in a sorted list. For GN-first languages,
+ * this generally means moving the surname to the beginning of the string, with a comma between it and
+ * the rest of the name: e.g., "Carter, James E. Jr.".
+ * @internal
+ */
+ SORTING,
+
+ /**
+ * Requests that the surname in the formatted result be rendered in ALL CAPS. This is often done with
+ * Japanese names to highlight which name is the surname.
+ * @internal
+ */
+ SURNAME_ALLCAPS
+ }
+
+ //==============================================================================
+ // Identifiers used to request field values from the PersonName object
+
+ /**
+ * Identifiers for the name fields supported by the PersonName object.
+ * @internal
+ */
+ public enum NameField {
+ /**
+ * Contains titles and other words that precede the actual name, such as "Mr."
+ * @internal
+ */
+ PREFIX("prefix"),
+
+ /**
+ * The given name. May contain more than one token.
+ * @internal
+ */
+ GIVEN("given"),
+
+ /**
+ * Additional given names. (In English, this is usually the "middle name" and
+ * may contain more than one word.)
+ * @internal
+ */
+ GIVEN2("given2"),
+
+ /**
+ * The surname. In Spanish, this is the patronymic surname.
+ * @internal
+ */
+ SURNAME("surname"),
+
+ /**
+ * Additional surnames. This is only used in a few languages, such as Spanish,
+ * where it is the matronymic surname. (In most languages, multiple surnames all
+ * just go in the SURNAME field.)
+ * @internal
+ */
+ SURNAME2("surname2"),
+
+ /**
+ * Generational and professional qualifiers that generally follow the actual name,
+ * such as "Jr." or "M.D."
+ * @internal
+ */
+ SUFFIX("suffix"),
+
+ /**
+ * The preferred field order for the name. PersonName objects generally shouldn't provide
+ * this field, allowing the PersonNameFormatter to deduce the proper field order based on
+ * the locales of the name of the formatter. But this can be used to force a particular
+ * field order, generally in cases where the deduction logic in PersonNameFormatter would
+ * guess wrong. When used, the only valid values are "givenFirst" and "surnameFirst".
+ * @internal
+ */
+ PREFERRED_ORDER("preferredOrder");
+
+ private final String name;
+
+ private NameField(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the NameField's display name.
+ * @internal
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Returns the appropriate NameField for its display name.
+ * @internal
+ */
+ public static NameField forString(String name) {
+ for (NameField field : values()) {
+ if (field.name.equals(name)) {
+ return field;
+ }
+ }
+ throw new IllegalArgumentException("Invalid field name " + name);
+ }
+ }
+
+ /**
+ * Identifiers for the name field modifiers supported by the PersonName and PersonNameFormatter objects.
+ * @internal
+ */
+ public enum FieldModifier {
+ /**
+ * Requests an "informal" variant of the field, generally a nickname of some type:
+ * if "given" is "James", "given-informal" might be "Jimmy". Only applied to the "given"
+ * field. If the PersonName object doesn't apply this modifier, PersonNameFormatter just
+ * uses the unmodified version of "given".
+ * @internal
+ */
+ INFORMAL("informal"),
+
+ /**
+ * If the field contains a main word with one or more separate prefixes, such as
+ * "van den Hul", this requests just the prefixes ("van den"). Only applied to the "surname"
+ * field. If the PersonName object doesn't apply this modifier, PersonNameFormatter
+ * assumes there are no prefixes.
+ * @internal
+ */
+ PREFIX("prefix"),
+
+ /**
+ * If the field contains a main word with one or more separate prefixes, such as
+ * "van den Hul", this requests just the main word ("Hul"). Only applied to the "surname"
+ * field. If the implementing class doesn't apply this modifier, PersonNameFormatter
+ * assumes the entire "surname" field is the "core".
+ * @internal
+ */
+ CORE("core"),
+
+ /**
+ * Requests an initial for the specified field. PersonNameFormatter will do
+ * this algorithmically, but a PersonName object can apply this modifier itself if it wants
+ * different initial-generation logic (or stores the initial separately).
+ * @internal
+ */
+ INITIAL("initial"),
+
+ /**
+ * Requests an initial for the specified field, suitable for use in a monogram
+ * (this usually differs from "initial" in that "initial" adds a period and "monogram" doesn't).
+ * PersonNameFormatter will do this algorithmically, but a PersonName object can apply
+ * this modifier itself if it wants different monogram-generation logic.
+ * @internal
+ */
+ MONOGRAM("monogram"),
+
+ /**
+ * Requests the field value converted to ALL CAPS. PersonName objects
+ * generally won't need to handle this modifier themselves.
+ * @internal
+ */
+ ALL_CAPS("allCaps"),
+
+ /**
+ * Requests the field value with the first letter of each word capitalized.
+ * A PersonName object might handle this modifier itself to capitalize words more
+ * selectively.
+ * @internal
+ */
+ INITIAL_CAP("initialCap");
+
+ private final String name;
+
+ private FieldModifier(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the FieldModifier's display name.
+ * @internal
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Returns the appropriate fieldModifier for its display name.
+ * @internal
+ */
+ public static FieldModifier forString(String name) {
+ for (FieldModifier modifier : values()) {
+ if (modifier.name.equals(name)) {
+ return modifier;
+ }
+ }
+ throw new IllegalArgumentException("Invalid modifier name " + name);
+ }
+ }
+
+ //==============================================================================
+ // The PersonName object
+
+ /**
+ * An object used to provide name data to the PersonNameFormatter for formatting.
+ * Clients can implement this interface to talk directly to some other subsystem
+ * that actually contains the name data (instead of having to copy it into a separate
+ * object just for formatting) or to override the default modifier behavior described
+ * above. A concrete SimplePersonName object that does store the field values directly
+ * is provided.
+ * @internal
+ * @see SimplePersonName
+ */
+ public interface PersonName {
+ /**
+ * Returns the locale of the name-- that is, the language or country of origin for the person being named.
+ * @return The name's locale.
+ * @internal
+ */
+ public ULocale getNameLocale();
+
+ /**
+ * Returns one field of the name, possibly in a modified form.
+ * @param identifier The identifier of the requested field.
+ * @param modifiers An **IN/OUT** parameter that specifies modifiers to apply to the basic field value.
+ * An implementing class can choose to handle or ignore any modifiers; it should modify
+ * this parameter so that on exit, it contains only the requested modifiers that it
+ * DIDN'T handle.
+ * @return The value of the requested field, optionally modified by some or all of the requested modifiers, or
+ * null if the requested field isn't present in the name.
+ * @internal
+ */
+ public String getFieldValue(NameField identifier, Set<FieldModifier> modifiers);
+ }
+
+ private final PersonNameFormatterImpl impl;
+
+ //==============================================================================
+ // Public API on PersonNameFormatter
+
+ /**
+ * Constructs a PersonNameFormatter.
+ * @param locale The target locale for formatted names.
+ * @param length The requested length.
+ * @param usage The requested usage.
+ * @param formality The requested formality.
+ * @param options A set containing additional formatting options. May be null.
+ * @see Length
+ * @see Usage
+ * @see Formality
+ * @see Options
+ * @internal
+ */
+ public PersonNameFormatter(ULocale locale, Length length, Usage usage, Formality formality, Set<Options> options) {
+ this.impl = new PersonNameFormatterImpl(locale, length, usage, formality, options);
+ }
+
+ /**
+ * Formats a name.
+ * @param name A PersonName object that supplies individual field values (optionally, with modifiers applied)
+ * to the formatter for formatting.
+ * @return The name, formatted according to the locale and other parameters passed to the formatter's constructor.
+ * @internal
+ */
+ public String format(PersonName name) {
+ // TODO: Should probably return a FormattedPersonName object
+ return impl.format(name);
+ }
+}
--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.text;
+
+import com.ibm.icu.util.ULocale;
+
+import java.util.*;
+
+/**
+ * A concrete implementation of PersonNameFormatter.PersonName that simply stores the field
+ * values in a Map.
+ *
+ * A caller can store both raw field values (such as "given") and modified field values (such as "given-informal")
+ * in a SimplePersonName. But beyond storing and returning modified field values provided to it by the caller,
+ * SimplePersonName relies on the PersonNameFormatter's default handling of field modifiers.
+ * @internal
+ */
+public class SimplePersonName implements PersonNameFormatter.PersonName {
+ /**
+ * Simple constructor.
+ * @param nameLocale The locale of the name (i.e., its ethnic or national origin).
+ * @param fieldValues A Map mapping from field names to field values. The field names
+ * are the values returned by NameField.toString().
+ * @internal
+ */
+ public SimplePersonName(ULocale nameLocale, Map<String, String> fieldValues) {
+ this.nameLocale = nameLocale;
+ this.fieldValues = new HashMap<>(fieldValues);
+ }
+
+ /**
+ * A constructor that takes the locale ID and field values as a single String. This constructor is really
+ * intended only for the use of the PersonNameFormatter unit tests.
+ * @param keysAndValues A single string containing the locale ID and field values. This string is organized
+ * into key-value pairs separated by commas. The keys are separated from the values
+ * by equal signs. The keys themselves are field names, as returned by
+ * NameField.toString(), optionally followed by a hyphen-delimited set of modifier names,
+ * as returned by FieldModifier.toString().
+ * @internal
+ */
+ public SimplePersonName(String keysAndValues) {
+ this.fieldValues = new HashMap<>();
+
+ StringTokenizer tok = new StringTokenizer(keysAndValues, ",");
+ ULocale tempLocale = null;
+ while (tok.hasMoreTokens()) {
+ String entry = tok.nextToken();
+ int equalPos = entry.indexOf('=');
+ if (equalPos < 0) {
+ throw new IllegalArgumentException("No = found in name field entry");
+ }
+ String fieldName = entry.substring(0, equalPos);
+ String fieldValue = entry.substring(equalPos + 1);
+
+ if (fieldName.equals("locale")) {
+ tempLocale = new ULocale(fieldValue);
+ } else {
+ this.fieldValues.put(fieldName, fieldValue);
+ }
+ }
+ this.nameLocale = tempLocale;
+
+ // special-case code for the "surname" field-- if it isn't specified, but "surname-prefix" and
+ // "surname-core" both are, let "surname" be the other two fields joined with a space
+ if (this.fieldValues.get("surname") == null) {
+ String surnamePrefix = this.fieldValues.get("surname-prefix");
+ String surnameCore = this.fieldValues.get("surname-core");
+ if (surnamePrefix != null && surnameCore != null) {
+ this.fieldValues.put("surname", surnamePrefix + " " + surnameCore);
+ }
+ }
+ }
+
+ /**
+ * Returns the locale of the name-- that is, the language or country of origin for the person being named.
+ * @return The name's locale.
+ * @internal
+ */
+ @Override
+ public ULocale getNameLocale() {
+ return nameLocale;
+ }
+
+ /**
+ * Returns one field of the name, possibly in a modified form. This class can store modified versions of fields,
+ * provided at construction time, and this function will return them. Otherwise, it ignores modifiers and
+ * relies on PersonNameFormat's default modifier handling.
+ * @param nameField The identifier of the requested field.
+ * @param modifiers An **IN/OUT** parameter that specifies modifiers to apply to the basic field value.
+ * On return, this list will contain any modifiers that this object didn't handle. This class
+ * will always return this set unmodified, unless a modified version of the requested field
+ * was provided at construction time.
+ * @return The value of the requested field, optionally modified by some or all of the requested modifiers, or
+ * null if the requested field isn't present in the name.
+ * @internal
+ */
+ @Override
+ public String getFieldValue(PersonNameFormatter.NameField nameField, Set<PersonNameFormatter.FieldModifier> modifiers) {
+ // first look for the fully modified name in the internal table
+ String fieldName = nameField.toString();
+ String result = fieldValues.get(makeModifiedFieldName(nameField, modifiers));
+ if (result != null) {
+ modifiers.clear();
+ return result;
+ }
+
+ // if we don't find it, check the fully unmodified name. If it's not there, nothing else will be
+ result = fieldValues.get(fieldName);
+ if (result == null) {
+ return null;
+ } else if (modifiers.size() == 1) {
+ // and if it IS there and there's only one modifier, we're done
+ return result;
+ }
+
+ // but if there are two or more modifiers, then we have to go through the whole list of fields and look for the best match
+ String winningKey = fieldName;
+ int winningScore = 0;
+ for (String key : fieldValues.keySet()) {
+ if (key.startsWith(fieldName)) {
+ Set<PersonNameFormatter.FieldModifier> keyModifiers = makeModifiersFromName(key);
+ if (modifiers.containsAll(keyModifiers)) {
+ if (keyModifiers.size() > winningScore || (keyModifiers.size() == winningScore && key.compareTo(winningKey) < 0)) {
+ winningKey = key;
+ winningScore = keyModifiers.size();
+ }
+ }
+ }
+ }
+ result = fieldValues.get(winningKey);
+ modifiers.removeAll(makeModifiersFromName(winningKey));
+ return result;
+ }
+
+ private static String makeModifiedFieldName(PersonNameFormatter.NameField fieldName,
+ Collection<PersonNameFormatter.FieldModifier> modifiers) {
+ StringBuilder result = new StringBuilder();
+ result.append(fieldName);
+
+ TreeSet<String> sortedModifierNames = new TreeSet<>();
+ for (PersonNameFormatter.FieldModifier modifier : modifiers) {
+ sortedModifierNames.add(modifier.toString());
+ }
+ for (String modifierName : sortedModifierNames) {
+ result.append("-");
+ result.append(modifierName);
+ }
+ return result.toString();
+ }
+
+ private static Set<PersonNameFormatter.FieldModifier> makeModifiersFromName(String modifiedName) {
+ StringTokenizer tok = new StringTokenizer(modifiedName, "-");
+ Set<PersonNameFormatter.FieldModifier> result = new HashSet<>();
+ String fieldName = tok.nextToken(); // throw away the field name
+ while (tok.hasMoreTokens()) {
+ result.add(PersonNameFormatter.FieldModifier.forString(tok.nextToken()));
+ }
+ return result;
+ }
+
+ private final ULocale nameLocale;
+ private final Map<String, String> fieldValues;
+}
\ No newline at end of file
--- /dev/null
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package com.ibm.icu.dev.test.format;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.text.PersonNameFormatter;
+import com.ibm.icu.text.SimplePersonName;
+import com.ibm.icu.util.ULocale;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.*;
+
+@RunWith(JUnit4.class)
+public class PersonNameFormatterTest extends TestFmwk{
+ private static class NameAndTestCases {
+ public String nameFields;
+ public String[][] testCases;
+
+ public NameAndTestCases(String nameFields, String[][] testCases) {
+ this.nameFields = nameFields;
+ this.testCases = testCases;
+ }
+ }
+
+ private void executeTestCases(NameAndTestCases[] namesAndTestCases, boolean forDebugging) {
+ for (NameAndTestCases nameAndTestCases : namesAndTestCases) {
+ SimplePersonName name = new SimplePersonName(nameAndTestCases.nameFields);
+ if (forDebugging) {
+ System.out.println(nameAndTestCases.nameFields);
+ }
+
+ for (String[] testCase : nameAndTestCases.testCases) {
+ ULocale formatterLocale = new ULocale(testCase[0]);
+ PersonNameFormatter.Length formatterLength = PersonNameFormatter.Length.valueOf(testCase[1]);
+ PersonNameFormatter.Usage formatterUsage = PersonNameFormatter.Usage.valueOf(testCase[2]);
+ PersonNameFormatter.Formality formatterFormality = PersonNameFormatter.Formality.valueOf(testCase[3]);
+ Set<PersonNameFormatter.Options> formatterOptions = makeOptionsSet(testCase[4]);
+ String expectedResult = testCase[5];
+
+ PersonNameFormatter formatter = new PersonNameFormatter(formatterLocale, formatterLength, formatterUsage, formatterFormality, formatterOptions);
+ String actualResult = formatter.format(name);
+
+ if (forDebugging) {
+ System.out.println(" " + formatterLocale + "," + formatterLength + "," + formatterUsage + "," + formatterFormality + "," + formatterOptions + " => " + actualResult);
+ } else {
+ assertEquals("Wrong formatting result for " + nameAndTestCases.nameFields + "," + Arrays.toString(testCase), expectedResult, actualResult);
+ }
+ }
+ }
+ }
+
+ private static Set<PersonNameFormatter.Options> makeOptionsSet(String optionsStr) {
+ Set<PersonNameFormatter.Options> result = new HashSet<>();
+ StringTokenizer tok = new StringTokenizer(optionsStr, ",");
+ while (tok.hasMoreTokens()) {
+ String optionStr = tok.nextToken();
+ PersonNameFormatter.Options option = PersonNameFormatter.Options.valueOf(optionStr);
+ result.add(option);
+ }
+ return result;
+ }
+
+ @Test
+ public void TestEnglishName() {
+ executeTestCases(new NameAndTestCases[]{
+ new NameAndTestCases("locale=en_US,prefix=Mr.,given=Richard,given-informal=Rich,given2=Theodore,surname=Gillam", new String[][] {
+ // test all the different combinations of parameters with the normal name order
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Richard Theodore Gillam" },
+ { "en_US", "LONG", "REFERRING", "INFORMAL", "", "Rich Gillam" },
+ { "en_US", "LONG", "ADDRESSING", "FORMAL", "", "Mr. Gillam" },
+ { "en_US", "LONG", "ADDRESSING", "INFORMAL", "", "Rich" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "", "Richard T. Gillam" },
+ { "en_US", "MEDIUM", "REFERRING", "INFORMAL", "", "Rich Gillam" },
+ { "en_US", "MEDIUM", "ADDRESSING", "FORMAL", "", "Mr. Gillam" },
+ { "en_US", "MEDIUM", "ADDRESSING", "INFORMAL", "", "Rich" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "R. T. Gillam" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "Rich G." },
+ { "en_US", "SHORT", "ADDRESSING", "FORMAL", "", "Mr. Gillam" },
+ { "en_US", "SHORT", "ADDRESSING", "INFORMAL", "", "Rich" },
+
+ // test all the different combinations of parameters for "sorting" order
+ { "en_US", "LONG", "REFERRING", "FORMAL", "SORTING", "Gillam, Richard Theodore" },
+ { "en_US", "LONG", "REFERRING", "INFORMAL", "SORTING", "Gillam, Rich" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "SORTING", "Gillam, Richard T." },
+ { "en_US", "MEDIUM", "REFERRING", "INFORMAL", "SORTING", "Gillam, Rich" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "SORTING", "Gillam, R. T." },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "SORTING", "Gillam, Rich" },
+
+ // we don't really support ADDRESSING in conjunction with SORTING-- it should always
+ // do the same thing as REFERRING
+ { "en_US", "LONG", "ADDRESSING", "FORMAL", "SORTING", "Gillam, Richard Theodore" },
+ { "en_US", "LONG", "ADDRESSING", "INFORMAL", "SORTING", "Gillam, Rich" },
+ { "en_US", "MEDIUM", "ADDRESSING", "FORMAL", "SORTING", "Gillam, Richard T." },
+ { "en_US", "MEDIUM", "ADDRESSING", "INFORMAL", "SORTING", "Gillam, Rich" },
+ { "en_US", "SHORT", "ADDRESSING", "FORMAL", "SORTING", "Gillam, R. T." },
+ { "en_US", "SHORT", "ADDRESSING", "INFORMAL", "SORTING", "Gillam, Rich" },
+
+ // finally, try the different variations of MONOGRAM
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "RTG" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "RG" },
+ { "en_US", "MEDIUM", "MONOGRAM", "FORMAL", "", "G" },
+ { "en_US", "MEDIUM", "MONOGRAM", "INFORMAL", "", "R" },
+ { "en_US", "SHORT", "MONOGRAM", "FORMAL", "", "G" },
+ { "en_US", "SHORT", "MONOGRAM", "INFORMAL", "", "R" },
+
+ // and again, we don't support SORTING for monograms, so it should also do the
+ // same thing as GIVEN_FIRST
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "SORTING", "RTG" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "SORTING", "RG" },
+ { "en_US", "MEDIUM", "MONOGRAM", "FORMAL", "SORTING", "G" },
+ { "en_US", "MEDIUM", "MONOGRAM", "INFORMAL", "SORTING", "R" },
+ { "en_US", "SHORT", "MONOGRAM", "FORMAL", "SORTING", "G" },
+ { "en_US", "SHORT", "MONOGRAM", "INFORMAL", "SORTING", "R" },
+ })
+ }, false);
+ }
+
+ @Test
+ public void TestPrefixCore() {
+ executeTestCases(new NameAndTestCases[]{
+ new NameAndTestCases("locale=en_US,given=Willem,surname-prefix=van der,surname-core=Plas", new String[][] {
+ // for normal formatting, the {surname} field is just "{surname-prefix} {surname-core}"
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Willem van der Plas" },
+ { "en_US", "LONG", "REFERRING", "INFORMAL", "", "Willem van der Plas" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "", "Willem van der Plas" },
+ { "en_US", "MEDIUM", "REFERRING", "INFORMAL", "", "Willem van der Plas" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "W. van der Plas" },
+
+ // for FORMAL SORTING, we sort by "surname-core", with "surname-prefix" at the end
+ { "en_US", "LONG", "REFERRING", "FORMAL", "SORTING", "Plas, Willem van der" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "SORTING", "Plas, Willem van der" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "SORTING", "Plas, W. van der" },
+
+ // but for INFORMAL SORTING, we keep the surname together and sort by the prefix
+ { "en_US", "LONG", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "MEDIUM", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+
+ // the default (English) logic for initials doesn't do anything special with the surname-prefix--
+ // it gets initials too, which is probably wrong
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "Willem v. d. P." },
+
+ // and (English) monogram generation doesn't do anything special with the prefix either
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "WV" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "WV" },
+
+ // but Dutch monogram generation _does_ handle the prefix specially
+ { "nl_NL", "LONG", "MONOGRAM", "FORMAL", "", "WvP" },
+ { "nl_NL", "LONG", "MONOGRAM", "INFORMAL", "", "WvP" },
+ }),
+ new NameAndTestCases("locale=en_US,given=Willem,surname=van der Plas", new String[][] {
+ // if we just use the "surname" field instead of "surname-prefix" and "surname-core", everything's
+ // the same, except (obviously) for the cases where we were doing something special with the
+ // prefix and core
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Willem van der Plas" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "W. van der Plas" },
+
+ // for example, SORTING works the same way regardless of formality
+ { "en_US", "LONG", "REFERRING", "FORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "SORTING", "van der Plas, W." },
+ { "en_US", "LONG", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "MEDIUM", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "SORTING", "van der Plas, Willem" },
+
+ // and monogram generation works the same in English and Dutch
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "WV" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "WV" },
+ { "nl_NL", "LONG", "MONOGRAM", "FORMAL", "", "WV" },
+ { "nl_NL", "LONG", "MONOGRAM", "INFORMAL", "", "WV" },
+ }),
+ new NameAndTestCases("locale=en_US,given=Willem,surname-prefix=van der,surname-core=Plas,surname-initial=vdP.,surname-monogram=vdP", new String[][] {
+ // we can work around the initial generation by providing a "surname-initial" field in the name object
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "Willem vdP." },
+
+ // we could also (theoretically) work around the monogram-generation problem in English in the same way
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "WVDP" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "WVDP" },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestInitialGeneration() {
+ executeTestCases(new NameAndTestCases[]{
+ new NameAndTestCases("locale=en_US,given=George,given2=Herbert Walker,surname=Bush", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "George Herbert Walker Bush" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "", "George H. W. Bush" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "G. H. W. Bush" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "George B." },
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "GHB" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "GB" },
+ }),
+ new NameAndTestCases("locale=en_US,given=Ralph,surname=Vaughan Williams", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Ralph Vaughan Williams" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "", "Ralph Vaughan Williams" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "R. Vaughan Williams" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "Ralph V. W." },
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "RV" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "RV" },
+ }),
+ new NameAndTestCases("locale=en_US,given=John Paul,given2=Stephen David George,surname=Smith", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "John Paul Stephen David George Smith" },
+ { "en_US", "MEDIUM", "REFERRING", "FORMAL", "", "John Paul S. D. G. Smith" },
+ { "en_US", "SHORT", "REFERRING", "FORMAL", "", "J. P. S. D. G. Smith" },
+ { "en_US", "SHORT", "REFERRING", "INFORMAL", "", "John Paul S." },
+ { "en_US", "LONG", "MONOGRAM", "FORMAL", "", "JSS" },
+ { "en_US", "LONG", "MONOGRAM", "INFORMAL", "", "JS" },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestLiteralTextElision() {
+ executeTestCases(new NameAndTestCases[]{
+ // literal text elision is difficult to test with the real locale data, although this is a start
+ // perhaps we could add an API for debugging that lets us pass in real pattern strings, but I'd like to stay away from that
+ new NameAndTestCases("locale=en_US,given=John,given2=Paul,surname=Smith,suffix=Jr.", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "John Paul Smith Jr." },
+ }),
+ new NameAndTestCases("locale=en_US,given=John,given2=Paul,surname=Smith", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "John Paul Smith" },
+ }),
+ new NameAndTestCases("locale=en_US,given2=Paul,surname=Smith", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Paul Smith" },
+ }),
+ new NameAndTestCases("locale=en_US,given2=Paul", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Paul" },
+ }),
+ new NameAndTestCases("locale=en_US,given=John", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "John" },
+ }),
+ new NameAndTestCases("locale=en_US,given=John,suffix=Jr.", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "John Jr." },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestMultiplePatterns() {
+ executeTestCases(new NameAndTestCases[]{
+ // the Spanish rules have two name patterns for many of the sorting cases: one to use if the surname2
+ // field is populated and one to use if not-- these allow the comma between the fields to be displayed
+ // in the right place. This test checks to make sure we're using the right pattern based on which
+ // fields are present in the actual name
+ new NameAndTestCases("locale=es_ES,given=Andrés,given2=Manuel,surname=López,surname2=Obrador", new String[][] {
+ { "es_ES", "LONG", "REFERRING", "FORMAL", "", "Andrés Manuel López Obrador" },
+ { "es_ES", "LONG", "REFERRING", "FORMAL", "SORTING" , "López Obrador, Andrés Manuel" },
+ }),
+ new NameAndTestCases("locale=es_ES,given=Andrés,given2=Manuel,surname=López", new String[][] {
+ { "es_ES", "LONG", "REFERRING", "FORMAL", "", "Andrés Manuel López" },
+ { "es_ES", "LONG", "REFERRING", "FORMAL", "SORTING" , "López, Andrés Manuel" },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestNameOrder() {
+ executeTestCases(new NameAndTestCases[]{
+ // the name's locale is used to determine the field order. For the English name formatter, if the
+ // name is English, the order is GN first. If it's Japanese, it's SN first. This is true whether the
+ // Japanese name is written in Latin letters or Han characters
+ new NameAndTestCases("locale=en_US,given=Shinzo,surname=Abe", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Shinzo Abe" },
+ }),
+ new NameAndTestCases("locale=ja_JP,given=Shinzo,surname=Abe", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Abe Shinzo" },
+ }),
+ new NameAndTestCases("locale=ja_JP,given=晋三,surname=安倍", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "安倍 晋三" },
+ }),
+
+ // the name can also declare its order directly, with the optional "preferredOrder" field. If it does this,
+ // the value of that field holds for all formatter locales and overrides determining the order
+ // by looking at the name's locale
+ new NameAndTestCases("locale=en_US,given=Shinzo,surname=Abe,preferredOrder=surnameFirst", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Abe Shinzo" },
+ }),
+ new NameAndTestCases("locale=ja_JP,given=Shinzo,surname=Abe,preferredOrder=givenFirst", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Shinzo Abe" },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestCapitalizedSurname() {
+ executeTestCases(new NameAndTestCases[]{
+ // the SURNAME_ALLCAPS option does just what it says: it causes the surname field
+ // to be displayed in all caps
+ new NameAndTestCases("locale=en_US,given=Shinzo,surname=Abe", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Shinzo Abe" },
+ { "en_US", "LONG", "REFERRING", "FORMAL", "SURNAME_ALLCAPS", "Shinzo ABE" },
+ }),
+ new NameAndTestCases("locale=ja_JP,given=Shinzo,surname=Abe", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Abe Shinzo" },
+ { "en_US", "LONG", "REFERRING", "FORMAL", "SURNAME_ALLCAPS", "ABE Shinzo" },
+ }),
+ }, false);
+ }
+
+ @Test
+ public void TestNameSpacing() {
+ executeTestCases(new NameAndTestCases[]{
+ // if the formatter locale uses spaces, the result will use its formats (complete with spaces),
+ // regardless of locale
+ new NameAndTestCases("locale=ja_JP,given=Hayao,surname=Miyazaki", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "Miyazaki Hayao" },
+ }),
+ new NameAndTestCases("locale=ja_JP,given=駿,surname=宮崎", new String[][] {
+ { "en_US", "LONG", "REFERRING", "FORMAL", "", "宮崎 駿" },
+ }),
+
+ // if the formatter locale doesn't use spaces and the name's locale doesn't either, just use
+ // the native formatter
+ new NameAndTestCases("locale=ja_JP,given=駿,surname=宮崎", new String[][] {
+ // (the Japanese name formatter actually inserts a space even for native names)
+ { "ja_JP", "LONG", "REFERRING", "FORMAL", "", "宮崎 駿" },
+ { "zh_CN", "LONG", "REFERRING", "FORMAL", "", "宮崎駿" },
+ }),
+
+ // if the formatter locale doesn't use spaces and the name's locale does, use the name locale's formatter,
+ // but if the name is still using the formatter locale's script, use the native formatter's
+ // "foreign space replacement" character instead of spaces
+ new NameAndTestCases("locale=en_US,given=Albert,surname=Einstein", new String[][] {
+ { "ja_JP", "LONG", "REFERRING", "FORMAL", "", "Albert Einstein" },
+ { "zh_CN", "LONG", "REFERRING", "FORMAL", "", "Albert Einstein" },
+ }),
+ new NameAndTestCases("locale=en_US,given=アルベルト,surname=アインシュタイン", new String[][] {
+ { "ja_JP", "LONG", "REFERRING", "FORMAL", "", "アルベルト・アインシュタイン" },
+ }),
+ new NameAndTestCases("locale=en_US,given=阿尔伯特,surname=爱因斯坦", new String[][] {
+ { "zh_CN", "LONG", "REFERRING", "FORMAL", "", "阿尔伯特·爱因斯坦" },
+ }),
+ }, false);
+ }
+
+ // need tests (and implementation?) for:
+ // - foreign space replacement
+}