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.Locale; 6 import java.util.StringTokenizer; 7 8 import com.ibm.icu.lang.UCharacter; 9 import com.ibm.icu.text.BreakIterator; 10 import com.ibm.icu.text.CaseMap; 11 import com.ibm.icu.text.PersonName; 12 import com.ibm.icu.text.SimpleFormatter; 13 14 /** 15 * Parent class for classes that implement field-modifier behavior. 16 */ 17 abstract class FieldModifierImpl { modifyField(String fieldValue)18 public abstract String modifyField(String fieldValue); 19 forName(PersonName.FieldModifier modifierID, PersonNameFormatterImpl formatterImpl)20 public static FieldModifierImpl forName(PersonName.FieldModifier modifierID, PersonNameFormatterImpl formatterImpl) { 21 switch (modifierID) { 22 case INFORMAL: 23 return NOOP_MODIFIER; 24 case PREFIX: 25 return NULL_MODIFIER; 26 case CORE: 27 return NOOP_MODIFIER; 28 case ALL_CAPS: 29 return new AllCapsModifier(formatterImpl.getLocale()); 30 case INITIAL_CAP: 31 return new InitialCapModifier(formatterImpl.getLocale()); 32 case INITIAL: 33 return new InitialModifier(formatterImpl.getInitialPattern(), formatterImpl.getInitialSequencePattern()); 34 case MONOGRAM: 35 return MONOGRAM_MODIFIER; 36 default: 37 throw new IllegalArgumentException("Invalid modifier ID " + modifierID); 38 } 39 } 40 41 /** 42 * A field modifier that just returns the field value unmodified. This is used to implement the default 43 * behavior of the "informal" and "core" modifiers ("real" informal or core variants have to be supplied or 44 * calculated by the PersonName object). 45 */ 46 private static final FieldModifierImpl NOOP_MODIFIER = new FieldModifierImpl() { 47 @Override 48 public String modifyField(String fieldValue) { 49 return fieldValue; 50 } 51 }; 52 53 /** 54 * A field modifier that just returns the empty string. This is used to implement the default behavior of the 55 * "prefix" modifier ("real" prefix variants have to be supplied to calculated by the PersonName object). 56 */ 57 private static final FieldModifierImpl NULL_MODIFIER = new FieldModifierImpl() { 58 @Override 59 public String modifyField(String fieldValue) { 60 return ""; 61 } 62 }; 63 64 /** 65 * A field modifier that returns the field value converted to ALL CAPS. This is the default behavior 66 * for the "allCaps" modifier. 67 */ 68 private static class AllCapsModifier extends FieldModifierImpl { 69 private final Locale locale; 70 AllCapsModifier(Locale locale)71 public AllCapsModifier(Locale locale) { 72 this.locale = locale; 73 } 74 75 @Override modifyField(String fieldValue)76 public String modifyField(String fieldValue) { 77 return UCharacter.toUpperCase(locale, fieldValue); 78 } 79 } 80 81 /** 82 * A field modifier that returns the field value with the first letter of each word capitalized. This is 83 * the default behavior of the "initialCap" modifier. 84 */ 85 private static class InitialCapModifier extends FieldModifierImpl { 86 private final Locale locale; 87 private static final CaseMap.Title TO_TITLE_WHOLE_STRING_NO_LOWERCASE = CaseMap.toTitle().wholeString().noLowercase(); 88 InitialCapModifier(Locale locale)89 public InitialCapModifier(Locale locale) { 90 this.locale = locale; 91 } 92 93 @Override modifyField(String fieldValue)94 public String modifyField(String fieldValue) { 95 return TO_TITLE_WHOLE_STRING_NO_LOWERCASE.apply(locale, null, fieldValue); 96 } 97 } 98 99 /** 100 * A field modifier that returns the field value converted into one or more initials. This is the first grapheme 101 * cluster of each word in the field value, modified using the initialPattern/initial resource value from the 102 * locale data, and strung together using the initialPattern/initialSequence resource value from the locale data. 103 * (In English, these patterns put periods after each initial and connect them with spaces.) 104 * This is default behavior of the "initial" modifier. 105 */ 106 private static class InitialModifier extends FieldModifierImpl { 107 private final SimpleFormatter initialFormatter; 108 private final SimpleFormatter initialSequenceFormatter; 109 InitialModifier(String initialPattern, String initialSequencePattern)110 public InitialModifier(String initialPattern, String initialSequencePattern) { 111 this.initialFormatter = SimpleFormatter.compile(initialPattern); 112 this.initialSequenceFormatter = SimpleFormatter.compile(initialSequencePattern); 113 } 114 115 @Override modifyField(String fieldValue)116 public String modifyField(String fieldValue) { 117 String result = null; 118 StringTokenizer tok = new StringTokenizer(fieldValue, " "); 119 while (tok.hasMoreTokens()) { 120 String curInitial = getFirstGrapheme(tok.nextToken()); 121 if (result == null) { 122 result = initialFormatter.format(curInitial); 123 } else { 124 result = initialSequenceFormatter.format(result, initialFormatter.format(curInitial)); 125 } 126 } 127 return result; 128 } 129 } 130 131 /** 132 * A field modifier that simply returns the first grapheme cluster in the field value. 133 * This is the default implementation of the "monogram" modifier. 134 */ 135 private static final FieldModifierImpl MONOGRAM_MODIFIER = new FieldModifierImpl() { 136 @Override 137 public String modifyField(String fieldValue) { 138 return getFirstGrapheme(fieldValue); 139 } 140 }; 141 142 /** 143 * A utility function that just returns the first grapheme cluster in the string. 144 */ getFirstGrapheme(String s)145 private static String getFirstGrapheme(String s) { 146 // early out if the string is empty to avoid StringIndexOutOfBoundsException 147 if (s.isEmpty()) { 148 return ""; 149 } 150 151 // (currently, no locale overrides the grapheme-break rules, so we just use "root" instead of passing in the locale) 152 BreakIterator bi = BreakIterator.getCharacterInstance(Locale.ROOT); 153 bi.setText(s); 154 return s.substring(0, bi.next()); 155 } 156 } 157