Skip to content

Commit b0f5f49

Browse files
committed
fix: enhance name parsing to support honorifics and various formats
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
1 parent a7fbeaa commit b0f5f49

2 files changed

Lines changed: 97 additions & 18 deletions

File tree

apps/dav/lib/CardDAV/Converter.php

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
use Sabre\VObject\Property\VCard\Date;
2121

2222
class Converter {
23+
private const NAME_SUFFIXES = [
24+
'i', 'ii', 'iii', 'iv', 'v',
25+
'senior', 'junior', 'jr', 'sr',
26+
'phd', 'apr', 'rph', 'pe', 'md', 'ma', 'msc', 'bsc', 'ba', 'bs',
27+
'dmd', 'cme', 'bsn', 'mba',
28+
'ceo', 'cto', 'cfo', 'coo',
29+
];
30+
private const NAME_SALUTATIONS = [
31+
'mr', 'mrs', 'ms', 'miss', 'master', 'mister', 'dr', 'rev', 'fr', 'prof',
32+
'herr', 'frau', 'mme', 'mlle', 'me', 'pr',
33+
];
34+
2335
public function __construct(
2436
private IAccountManager $accountManager,
2537
private IUserManager $userManager,
@@ -157,33 +169,87 @@ public function createCardFromUser(IUser $user): ?VCard {
157169
}
158170

159171
public function splitFullName(string $fullName): array {
160-
// Very basic western style parsing. I'm not gonna implement
161-
// https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
162-
163-
// Handle "Lastname, Firstname" format
164-
if (str_contains($fullName, ',')) {
165-
[$family, $given] = array_map('trim', explode(',', $fullName, 2));
166-
if ($family !== '' && $given !== '') {
167-
return [$family, $given, '', '', ''];
172+
// Based on https://github.com/joshfraser/PHP-Name-Parser
173+
174+
$prefix = [];
175+
$suffix = [];
176+
$cleanedName = preg_replace('/\([^()]*\)|\[[^[\]]*\]|\{[^{}]*\}/', ' ', $fullName) ?? $fullName;
177+
$cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName) ?? $cleanedName);
178+
if ($cleanedName === '') {
179+
$cleanedName = trim($fullName);
180+
}
181+
182+
$segments = array_values(array_filter(
183+
array_map($this->splitNameWords(...), explode(',', $cleanedName)),
184+
static fn (array $segment): bool => $segment !== [],
185+
));
186+
187+
while (count($segments) > 1) {
188+
$lastSegment = $segments[count($segments) - 1];
189+
$knownSuffix = array_filter(
190+
$lastSegment,
191+
fn (string $word): bool => !$this->isNameSuffix($word),
192+
) === [];
193+
if (!$knownSuffix) {
194+
break;
168195
}
196+
array_unshift($suffix, implode(' ', array_pop($segments)));
169197
}
170198

171-
$elements = explode(' ', $fullName);
172-
$result = ['', '', '', '', ''];
173-
if (count($elements) > 2) {
174-
$result[0] = implode(' ', array_slice($elements, count($elements) - 1));
175-
$result[1] = $elements[0];
176-
$result[2] = implode(' ', array_slice($elements, 1, count($elements) - 2));
177-
} elseif (count($elements) === 2) {
178-
$result[0] = $elements[1];
179-
$result[1] = $elements[0];
199+
if (count($segments) > 1 && count($segments[0]) === 1) {
200+
$result = [$segments[0][0], '', '', '', ''];
201+
$nameWords = array_merge(...array_slice($segments, 1));
180202
} else {
181-
$result[0] = $elements[0];
203+
$result = ['', '', '', '', ''];
204+
$nameWords = $segments[0] ?? $this->splitNameWords($cleanedName);
205+
if (count($segments) > 1) {
206+
$suffix[] = implode(', ', array_map(static fn (array $segment): string => implode(' ', $segment), array_slice($segments, 1)));
207+
}
208+
}
209+
210+
while (count($nameWords) > 1 && $this->isNameSalutation($nameWords[0])) {
211+
$prefix[] = array_shift($nameWords);
212+
}
213+
while (count($nameWords) > 1 && $this->isNameSuffix($nameWords[count($nameWords) - 1])) {
214+
array_unshift($suffix, array_pop($nameWords));
215+
}
216+
217+
if ($result[0] !== '') {
218+
$result[1] = $nameWords[0] ?? '';
219+
$result[2] = implode(' ', array_slice($nameWords, 1));
220+
} elseif (count($nameWords) > 2) {
221+
$result[0] = implode(' ', array_slice($nameWords, count($nameWords) - 1));
222+
$result[1] = $nameWords[0];
223+
$result[2] = implode(' ', array_slice($nameWords, 1, count($nameWords) - 2));
224+
} elseif (count($nameWords) === 2) {
225+
$result[0] = $nameWords[1];
226+
$result[1] = $nameWords[0];
227+
} elseif (count($nameWords) === 1) {
228+
$result[0] = $nameWords[0];
182229
}
183230

231+
$result[3] = implode(' ', $prefix);
232+
$result[4] = implode(', ', array_filter($suffix));
233+
184234
return $result;
185235
}
186236

237+
private function splitNameWords(string $name): array {
238+
return preg_split('/\s+/', trim($name), -1, PREG_SPLIT_NO_EMPTY) ?: [];
239+
}
240+
241+
private function isNameSuffix(string $word): bool {
242+
return in_array($this->normalizeNameWord($word), self::NAME_SUFFIXES, true);
243+
}
244+
245+
private function isNameSalutation(string $word): bool {
246+
return in_array($this->normalizeNameWord($word), self::NAME_SALUTATIONS, true);
247+
}
248+
249+
private function normalizeNameWord(string $word): string {
250+
return strtolower(preg_replace('/[.,]/', '', trim($word)) ?? $word);
251+
}
252+
187253
private function getAvatarImage(IUser $user): ?IImage {
188254
try {
189255
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
@@ -200,6 +200,19 @@ public static function providesNames(): array {
200200
['Sauron;;;;', 'Sauron'],
201201
['Baggins;Bilbo;;;', 'Bilbo Baggins'],
202202
['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
203+
['Doe;Jane;;;MD', 'Jane Doe, MD'],
204+
['Williams;Mary;;;MD, PhD', 'Mary Williams, MD, PhD'],
205+
['King;Martin;Luther;;Jr.', 'Martin Luther King, Jr.'],
206+
['King;Martin;Luther;;Jr.', 'King, Martin Luther, Jr.'],
207+
['Doe;Jane;;Dr.;', 'Dr. Jane Doe'],
208+
['Doe;Jane;;Prof. Dr.;', 'Prof. Dr. Jane Doe'],
209+
['Lastname;Firstname;;;', 'Lastname, Firstname'],
210+
['Lastname;Firstname;Middlename;;', 'Lastname, Firstname Middlename'],
211+
['b;a;;;c', 'a b, c'],
212+
['a;b;c;;', 'a, b c'],
213+
['Doe;Jane;;;', 'Jane Doe (Contracting)'],
214+
['Smith;Jane;;;Ph.D.', 'Jane Smith Ph.D.'],
215+
['Smith;R.;Jason;;', 'R. Jason Smith'],
203216
];
204217
}
205218

0 commit comments

Comments
 (0)