• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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