Skip to content

Commit fe82637

Browse files
authored
Merge pull request #62122 from nextcloud/fix/comma-seperated-names
fix: enhance name splitting to handle "Lastname, Firstname" format
2 parents 2f25b8b + 4060e13 commit fe82637

2 files changed

Lines changed: 100 additions & 13 deletions

File tree

apps/dav/lib/CardDAV/Converter.php

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@
2121
use Sabre\VObject\Property\VCard\Date;
2222

2323
class Converter {
24+
private const array 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 array NAME_SALUTATIONS = [
32+
'mr', 'mrs', 'ms', 'miss', 'master', 'mister', 'dr', 'rev', 'fr', 'prof',
33+
'herr', 'frau', 'mme', 'mlle', 'me', 'pr',
34+
];
35+
2436
public function __construct(
2537
private IAccountManager $accountManager,
2638
private IUserManager $userManager,
@@ -158,25 +170,87 @@ public function createCardFromUser(IUser $user): ?VCard {
158170
}
159171

160172
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-
$elements = explode(' ', $fullName);
165-
$result = ['', '', '', '', ''];
166-
if (count($elements) > 2) {
167-
$result[0] = implode(' ', array_slice($elements, count($elements) - 1));
168-
$result[1] = $elements[0];
169-
$result[2] = implode(' ', array_slice($elements, 1, count($elements) - 2));
170-
} elseif (count($elements) === 2) {
171-
$result[0] = $elements[1];
172-
$result[1] = $elements[0];
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;
196+
}
197+
array_unshift($suffix, implode(' ', array_pop($segments)));
198+
}
199+
200+
if (count($segments) > 1 && count($segments[0]) === 1) {
201+
$result = [$segments[0][0], '', '', '', ''];
202+
$nameWords = array_merge(...array_slice($segments, 1));
173203
} else {
174-
$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));
175216
}
176217

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];
230+
}
231+
232+
$result[3] = implode(' ', $prefix);
233+
$result[4] = implode(', ', array_filter($suffix));
234+
177235
return $result;
178236
}
179237

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+
180254
private function getAvatarImage(IUser $user): ?IImage {
181255
try {
182256
return $user->getAvatarImage(512);

apps/dav/tests/unit/CardDAV/ConverterTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,19 @@ public static function providesNames(): array {
201201
['Sauron;;;;', 'Sauron'],
202202
['Baggins;Bilbo;;;', 'Bilbo Baggins'],
203203
['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
204+
['Doe;Jane;;;MD', 'Jane Doe, MD'],
205+
['Williams;Mary;;;MD, PhD', 'Mary Williams, MD, PhD'],
206+
['King;Martin;Luther;;Jr.', 'Martin Luther King, Jr.'],
207+
['King;Martin;Luther;;Jr.', 'King, Martin Luther, Jr.'],
208+
['Doe;Jane;;Dr.;', 'Dr. Jane Doe'],
209+
['Doe;Jane;;Prof. Dr.;', 'Prof. Dr. Jane Doe'],
210+
['Lastname;Firstname;;;', 'Lastname, Firstname'],
211+
['Lastname;Firstname;Middlename;;', 'Lastname, Firstname Middlename'],
212+
['b;a;;;c', 'a b, c'],
213+
['a;b;c;;', 'a, b c'],
214+
['Doe;Jane;;;', 'Jane Doe (Contracting)'],
215+
['Smith;Jane;;;Ph.D.', 'Jane Smith Ph.D.'],
216+
['Smith;R.;Jason;;', 'R. Jason Smith'],
204217
];
205218
}
206219

0 commit comments

Comments
 (0)