|
21 | 21 | use Sabre\VObject\Property\VCard\Date; |
22 | 22 |
|
23 | 23 | class Converter { |
| 24 | + private const NAME_SUFFIXES = [ |
| 25 | + 'i', 'ii', 'iii', 'iv', 'v', |
| 26 | + 'senior', 'junior', 'jr', 'sr', |
| 27 | + 'phd', 'apr', 'rph', 'pe', 'md', 'ma', 'msc', 'bsc', 'ba', 'bs', |
| 28 | + 'dmd', 'cme', 'bsn', 'mba', |
| 29 | + 'ceo', 'cto', 'cfo', 'coo', |
| 30 | + ]; |
| 31 | + private const NAME_SALUTATIONS = [ |
| 32 | + 'mr', 'mrs', 'ms', 'miss', 'master', 'mister', 'dr', 'rev', 'fr', 'prof', |
| 33 | + 'herr', 'frau', 'mme', 'mlle', 'me', 'pr', |
| 34 | + ]; |
| 35 | + |
24 | 36 | public function __construct( |
25 | 37 | private IAccountManager $accountManager, |
26 | 38 | private IUserManager $userManager, |
@@ -158,33 +170,87 @@ public function createCardFromUser(IUser $user): ?VCard { |
158 | 170 | } |
159 | 171 |
|
160 | 172 | public function splitFullName(string $fullName): array { |
161 | | - // Very basic western style parsing. I'm not gonna implement |
162 | | - // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;) |
163 | | - |
164 | | - // Handle "Lastname, Firstname" format |
165 | | - if (str_contains($fullName, ',')) { |
166 | | - [$family, $given] = array_map('trim', explode(',', $fullName, 2)); |
167 | | - if ($family !== '' && $given !== '') { |
168 | | - return [$family, $given, '', '', '']; |
| 173 | + // Based on https://github.com/joshfraser/PHP-Name-Parser |
| 174 | + |
| 175 | + $prefix = []; |
| 176 | + $suffix = []; |
| 177 | + $cleanedName = preg_replace('/\([^()]*\)|\[[^[\]]*\]|\{[^{}]*\}/', ' ', $fullName) ?? $fullName; |
| 178 | + $cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName) ?? $cleanedName); |
| 179 | + if ($cleanedName === '') { |
| 180 | + $cleanedName = trim($fullName); |
| 181 | + } |
| 182 | + |
| 183 | + $segments = array_values(array_filter( |
| 184 | + array_map($this->splitNameWords(...), explode(',', $cleanedName)), |
| 185 | + static fn (array $segment): bool => $segment !== [], |
| 186 | + )); |
| 187 | + |
| 188 | + while (count($segments) > 1) { |
| 189 | + $lastSegment = $segments[count($segments) - 1]; |
| 190 | + $knownSuffix = array_filter( |
| 191 | + $lastSegment, |
| 192 | + fn (string $word): bool => !$this->isNameSuffix($word), |
| 193 | + ) === []; |
| 194 | + if (!$knownSuffix) { |
| 195 | + break; |
169 | 196 | } |
| 197 | + array_unshift($suffix, implode(' ', array_pop($segments))); |
170 | 198 | } |
171 | 199 |
|
172 | | - $elements = explode(' ', $fullName); |
173 | | - $result = ['', '', '', '', '']; |
174 | | - if (count($elements) > 2) { |
175 | | - $result[0] = implode(' ', array_slice($elements, count($elements) - 1)); |
176 | | - $result[1] = $elements[0]; |
177 | | - $result[2] = implode(' ', array_slice($elements, 1, count($elements) - 2)); |
178 | | - } elseif (count($elements) === 2) { |
179 | | - $result[0] = $elements[1]; |
180 | | - $result[1] = $elements[0]; |
| 200 | + if (count($segments) > 1 && count($segments[0]) === 1) { |
| 201 | + $result = [$segments[0][0], '', '', '', '']; |
| 202 | + $nameWords = array_merge(...array_slice($segments, 1)); |
181 | 203 | } else { |
182 | | - $result[0] = $elements[0]; |
| 204 | + $result = ['', '', '', '', '']; |
| 205 | + $nameWords = $segments[0] ?? $this->splitNameWords($cleanedName); |
| 206 | + if (count($segments) > 1) { |
| 207 | + $suffix[] = implode(', ', array_map(static fn (array $segment): string => implode(' ', $segment), array_slice($segments, 1))); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + while (count($nameWords) > 1 && $this->isNameSalutation($nameWords[0])) { |
| 212 | + $prefix[] = array_shift($nameWords); |
| 213 | + } |
| 214 | + while (count($nameWords) > 1 && $this->isNameSuffix($nameWords[count($nameWords) - 1])) { |
| 215 | + array_unshift($suffix, array_pop($nameWords)); |
| 216 | + } |
| 217 | + |
| 218 | + if ($result[0] !== '') { |
| 219 | + $result[1] = $nameWords[0] ?? ''; |
| 220 | + $result[2] = implode(' ', array_slice($nameWords, 1)); |
| 221 | + } elseif (count($nameWords) > 2) { |
| 222 | + $result[0] = implode(' ', array_slice($nameWords, count($nameWords) - 1)); |
| 223 | + $result[1] = $nameWords[0]; |
| 224 | + $result[2] = implode(' ', array_slice($nameWords, 1, count($nameWords) - 2)); |
| 225 | + } elseif (count($nameWords) === 2) { |
| 226 | + $result[0] = $nameWords[1]; |
| 227 | + $result[1] = $nameWords[0]; |
| 228 | + } elseif (count($nameWords) === 1) { |
| 229 | + $result[0] = $nameWords[0]; |
183 | 230 | } |
184 | 231 |
|
| 232 | + $result[3] = implode(' ', $prefix); |
| 233 | + $result[4] = implode(', ', array_filter($suffix)); |
| 234 | + |
185 | 235 | return $result; |
186 | 236 | } |
187 | 237 |
|
| 238 | + private function splitNameWords(string $name): array { |
| 239 | + return preg_split('/\s+/', trim($name), -1, PREG_SPLIT_NO_EMPTY) ?: []; |
| 240 | + } |
| 241 | + |
| 242 | + private function isNameSuffix(string $word): bool { |
| 243 | + return in_array($this->normalizeNameWord($word), self::NAME_SUFFIXES, true); |
| 244 | + } |
| 245 | + |
| 246 | + private function isNameSalutation(string $word): bool { |
| 247 | + return in_array($this->normalizeNameWord($word), self::NAME_SALUTATIONS, true); |
| 248 | + } |
| 249 | + |
| 250 | + private function normalizeNameWord(string $word): string { |
| 251 | + return strtolower(preg_replace('/[.,]/', '', trim($word)) ?? $word); |
| 252 | + } |
| 253 | + |
188 | 254 | private function getAvatarImage(IUser $user): ?IImage { |
189 | 255 | try { |
190 | 256 | return $user->getAvatarImage(512); |
|
0 commit comments