1 // © 2022 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 package com.ibm.icu.impl.personname; 4 5 import java.util.ArrayList; 6 import java.util.HashMap; 7 import java.util.HashSet; 8 import java.util.List; 9 import java.util.Map; 10 import java.util.Set; 11 import java.util.StringTokenizer; 12 13 import com.ibm.icu.text.PersonName; 14 15 /** 16 * A single name formatting pattern, corresponding to a single namePattern element in CLDR. 17 */ 18 class PersonNamePattern { 19 private String patternText; // for debugging 20 private Element[] patternElements; 21 makePatterns(String[] patternText, PersonNameFormatterImpl formatterImpl)22 public static PersonNamePattern[] makePatterns(String[] patternText, PersonNameFormatterImpl formatterImpl) { 23 PersonNamePattern[] result = new PersonNamePattern[patternText.length]; 24 for (int i = 0; i < patternText.length; i++) { 25 result[i] = new PersonNamePattern(patternText[i], formatterImpl); 26 } 27 return result; 28 } 29 PersonNamePattern(String patternText, PersonNameFormatterImpl formatterImpl)30 private PersonNamePattern(String patternText, PersonNameFormatterImpl formatterImpl) { 31 this.patternText = patternText; 32 33 List<Element> elements = new ArrayList<>(); 34 boolean inField = false; 35 boolean inEscape = false; 36 StringBuilder workingString = new StringBuilder(); 37 for (int i = 0; i < patternText.length(); i++) { 38 char c = patternText.charAt(i); 39 40 if (inEscape) { 41 workingString.append(c); 42 inEscape = false; 43 } else { 44 switch (c) { 45 case '\\': 46 inEscape = true; 47 break; 48 case '{': 49 if (!inField) { 50 if (workingString.length() > 0) { 51 elements.add(new LiteralText(workingString.toString())); 52 workingString = new StringBuilder(); 53 } 54 inField = true; 55 } else { 56 throw new IllegalArgumentException("Nested braces are not allowed in name patterns"); 57 } 58 break; 59 case '}': 60 if (inField) { 61 if (workingString.length() > 0) { 62 elements.add(new NameFieldImpl(workingString.toString(), formatterImpl)); 63 workingString = new StringBuilder(); 64 } else { 65 throw new IllegalArgumentException("No field name inside braces"); 66 } 67 inField = false; 68 } else { 69 throw new IllegalArgumentException("Unmatched closing brace in literal text"); 70 } 71 break; 72 default: 73 workingString.append(c); 74 } 75 } 76 } 77 if (workingString.length() > 0) { 78 elements.add(new LiteralText(workingString.toString())); 79 } 80 this.patternElements = elements.toArray(new Element[0]); 81 } 82 format(PersonName name)83 public String format(PersonName name) { 84 StringBuilder result = new StringBuilder(); 85 boolean seenLeadingField = false; 86 boolean seenEmptyLeadingField = false; 87 boolean seenEmptyField = false; 88 StringBuilder textBefore = new StringBuilder(); 89 StringBuilder textAfter = new StringBuilder(); 90 91 // the logic below attempts to implement the following algorithm: 92 // - If one or more fields at the beginning of the name are empty, also skip all literal text 93 // from the beginning of the name up to the first populated field. 94 // - If one or more fields at the end of the name are empty, also skip all literal text from 95 // the last populated field to the end of the name. 96 // - If one or more contiguous fields in the middle of the name are empty, skip the literal text 97 // between them, omit characters from the literal text on either side of the empty fields up to 98 // the first space on either side, and make sure that the resulting literal text doesn't end up 99 // with two spaces in a row. 100 for (Element element : patternElements) { 101 if (element.isLiteral()) { 102 if (seenEmptyLeadingField) { 103 // do nothing; throw away the literal text 104 } else if (seenEmptyField) { 105 textAfter.append(element.format(name)); 106 } else { 107 textBefore.append(element.format(name)); 108 } 109 } else { 110 String fieldText = element.format(name); 111 if (fieldText == null || fieldText.isEmpty()) { 112 if (!seenLeadingField) { 113 seenEmptyLeadingField = true; 114 textBefore.setLength(0); 115 } else { 116 seenEmptyField = true; 117 textAfter.setLength(0); 118 } 119 } else { 120 seenLeadingField = true; 121 seenEmptyLeadingField = false; 122 if (seenEmptyField) { 123 result.append(coalesce(textBefore, textAfter)); 124 result.append(fieldText); 125 seenEmptyField = false; 126 } else { 127 result.append(textBefore); 128 textBefore.setLength(0); 129 result.append(element.format(name)); 130 } 131 } 132 } 133 } 134 if (!seenEmptyField) { 135 result.append(textBefore); 136 } 137 return result.toString(); 138 } 139 numPopulatedFields(PersonName name)140 public int numPopulatedFields(PersonName name) { 141 int result = 0; 142 for (Element element : patternElements) { 143 result += element.isPopulated(name) ? 1 : 0; 144 } 145 return result; 146 } 147 numEmptyFields(PersonName name)148 public int numEmptyFields(PersonName name) { 149 int result = 0; 150 for (Element element : patternElements) { 151 result += element.isPopulated(name) ? 0 : 1; 152 } 153 return result; 154 } 155 156 /** 157 * Stitches together the literal text on either side of an omitted field by deleting any 158 * non-whitespace characters immediately neighboring the omitted field and coalescing any 159 * adjacent spaces at the join point down to one. 160 * @param s1 The literal text before the omitted field. 161 * @param s2 The literal text after the omitted field. 162 */ coalesce(StringBuilder s1, StringBuilder s2)163 private String coalesce(StringBuilder s1, StringBuilder s2) { 164 // get the range of non-whitespace characters at the beginning of s1 165 int p1 = 0; 166 while (p1 < s1.length() && !Character.isWhitespace(s1.charAt(p1))) { 167 ++p1; 168 } 169 170 // get the range of non-whitespace characters at the end of s2 171 int p2 = s2.length() - 1; 172 while (p2 >= 0 && !Character.isWhitespace(s2.charAt(p2))) { 173 --p2; 174 } 175 176 // also include one whitespace character from s1 or, if there aren't 177 // any, one whitespace character from s2 178 if (p1 < s1.length()) { 179 ++p1; 180 } else if (p2 >= 0) { 181 --p2; 182 } 183 184 // concatenate those two ranges to get the coalesced literal text 185 String result = s1.substring(0, p1) + s2.substring(p2 + 1); 186 187 // clear out s1 and s2 (done here to improve readability in format() above)) 188 s1.setLength(0); 189 s2.setLength(0); 190 191 return result; 192 } 193 194 /** 195 * A single element in a NamePattern. This is either a name field or a range of literal text. 196 */ 197 private interface Element { isLiteral()198 boolean isLiteral(); format(PersonName name)199 String format(PersonName name); isPopulated(PersonName name)200 boolean isPopulated(PersonName name); 201 } 202 203 /** 204 * Literal text from a name pattern. 205 */ 206 private static class LiteralText implements Element { 207 private String text; 208 LiteralText(String text)209 public LiteralText(String text) { 210 this.text = text; 211 } 212 isLiteral()213 public boolean isLiteral() { 214 return true; 215 } 216 format(PersonName name)217 public String format(PersonName name) { 218 return text; 219 } 220 isPopulated(PersonName name)221 public boolean isPopulated(PersonName name) { 222 return false; 223 } 224 } 225 226 /** 227 * An actual name field in a NamePattern (i.e., the stuff represented in the pattern by text 228 * in braces). This class actually handles fetching the value for the field out of a 229 * PersonName object and applying any modifiers to it. 230 */ 231 private static class NameFieldImpl implements Element { 232 private PersonName.NameField fieldID; 233 private Map<PersonName.FieldModifier, FieldModifierImpl> modifiers; 234 NameFieldImpl(String fieldNameAndModifiers, PersonNameFormatterImpl formatterImpl)235 public NameFieldImpl(String fieldNameAndModifiers, PersonNameFormatterImpl formatterImpl) { 236 List<PersonName.FieldModifier> modifierIDs = new ArrayList<>(); 237 StringTokenizer tok = new StringTokenizer(fieldNameAndModifiers, "-"); 238 239 this.fieldID = PersonName.NameField.forString(tok.nextToken()); 240 while (tok.hasMoreTokens()) { 241 modifierIDs.add(PersonName.FieldModifier.forString(tok.nextToken())); 242 } 243 if (this.fieldID == PersonName.NameField.SURNAME && formatterImpl.shouldCapitalizeSurname()) { 244 modifierIDs.add(PersonName.FieldModifier.ALL_CAPS); 245 } 246 247 this.modifiers = new HashMap<>(); 248 for (PersonName.FieldModifier modifierID : modifierIDs) { 249 this.modifiers.put(modifierID, FieldModifierImpl.forName(modifierID, formatterImpl)); 250 } 251 } 252 isLiteral()253 public boolean isLiteral() { 254 return false; 255 } 256 format(PersonName name)257 public String format(PersonName name) { 258 Set<PersonName.FieldModifier> modifierIDs = new HashSet<>(modifiers.keySet()); 259 String result = name.getFieldValue(fieldID, modifierIDs); 260 if (result != null) { 261 for (PersonName.FieldModifier modifierID : modifierIDs) { 262 result = modifiers.get(modifierID).modifyField(result); 263 } 264 } 265 return result; 266 } 267 isPopulated(PersonName name)268 public boolean isPopulated(PersonName name) { 269 // just check whether the unmodified field contains a value 270 Set<PersonName.FieldModifier> modifierIDs = new HashSet<>(); 271 String fieldValue = name.getFieldValue(fieldID, modifierIDs); 272 return fieldValue != null && !fieldValue.isEmpty(); 273 } 274 } 275 } 276