fix: enhance name parsing to support honorifics and various formats

Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
pull/62122/head
Enjeck C. 4 weeks ago committed by Enjeck
parent bae33c323f
commit 7facf744ad
  1. 102
      apps/dav/lib/CardDAV/Converter.php
  2. 13
      apps/dav/tests/unit/CardDAV/ConverterTest.php

@ -21,6 +21,18 @@ use Sabre\VObject\Property\Text;
use Sabre\VObject\Property\VCard\Date;
class Converter {
private const NAME_SUFFIXES = [
'i', 'ii', 'iii', 'iv', 'v',
'senior', 'junior', 'jr', 'sr',
'phd', 'apr', 'rph', 'pe', 'md', 'ma', 'msc', 'bsc', 'ba', 'bs',
'dmd', 'cme', 'bsn', 'mba',
'ceo', 'cto', 'cfo', 'coo',
];
private const NAME_SALUTATIONS = [
'mr', 'mrs', 'ms', 'miss', 'master', 'mister', 'dr', 'rev', 'fr', 'prof',
'herr', 'frau', 'mme', 'mlle', 'me', 'pr',
];
public function __construct(
private IAccountManager $accountManager,
private IUserManager $userManager,
@ -158,33 +170,87 @@ class Converter {
}
public function splitFullName(string $fullName): array {
// Very basic western style parsing. I'm not gonna implement
// https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
// Handle "Lastname, Firstname" format
if (str_contains($fullName, ',')) {
[$family, $given] = array_map('trim', explode(',', $fullName, 2));
if ($family !== '' && $given !== '') {
return [$family, $given, '', '', ''];
// Based on https://github.com/joshfraser/PHP-Name-Parser
$prefix = [];
$suffix = [];
$cleanedName = preg_replace('/\([^()]*\)|\[[^[\]]*\]|\{[^{}]*\}/', ' ', $fullName) ?? $fullName;
$cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName) ?? $cleanedName);
if ($cleanedName === '') {
$cleanedName = trim($fullName);
}
$segments = array_values(array_filter(
array_map($this->splitNameWords(...), explode(',', $cleanedName)),
static fn (array $segment): bool => $segment !== [],
));
while (count($segments) > 1) {
$lastSegment = $segments[count($segments) - 1];
$knownSuffix = array_filter(
$lastSegment,
fn (string $word): bool => !$this->isNameSuffix($word),
) === [];
if (!$knownSuffix) {
break;
}
array_unshift($suffix, implode(' ', array_pop($segments)));
}
$elements = explode(' ', $fullName);
$result = ['', '', '', '', ''];
if (count($elements) > 2) {
$result[0] = implode(' ', array_slice($elements, count($elements) - 1));
$result[1] = $elements[0];
$result[2] = implode(' ', array_slice($elements, 1, count($elements) - 2));
} elseif (count($elements) === 2) {
$result[0] = $elements[1];
$result[1] = $elements[0];
if (count($segments) > 1 && count($segments[0]) === 1) {
$result = [$segments[0][0], '', '', '', ''];
$nameWords = array_merge(...array_slice($segments, 1));
} else {
$result[0] = $elements[0];
$result = ['', '', '', '', ''];
$nameWords = $segments[0] ?? $this->splitNameWords($cleanedName);
if (count($segments) > 1) {
$suffix[] = implode(', ', array_map(static fn (array $segment): string => implode(' ', $segment), array_slice($segments, 1)));
}
}
while (count($nameWords) > 1 && $this->isNameSalutation($nameWords[0])) {
$prefix[] = array_shift($nameWords);
}
while (count($nameWords) > 1 && $this->isNameSuffix($nameWords[count($nameWords) - 1])) {
array_unshift($suffix, array_pop($nameWords));
}
if ($result[0] !== '') {
$result[1] = $nameWords[0] ?? '';
$result[2] = implode(' ', array_slice($nameWords, 1));
} elseif (count($nameWords) > 2) {
$result[0] = implode(' ', array_slice($nameWords, count($nameWords) - 1));
$result[1] = $nameWords[0];
$result[2] = implode(' ', array_slice($nameWords, 1, count($nameWords) - 2));
} elseif (count($nameWords) === 2) {
$result[0] = $nameWords[1];
$result[1] = $nameWords[0];
} elseif (count($nameWords) === 1) {
$result[0] = $nameWords[0];
}
$result[3] = implode(' ', $prefix);
$result[4] = implode(', ', array_filter($suffix));
return $result;
}
private function splitNameWords(string $name): array {
return preg_split('/\s+/', trim($name), -1, PREG_SPLIT_NO_EMPTY) ?: [];
}
private function isNameSuffix(string $word): bool {
return in_array($this->normalizeNameWord($word), self::NAME_SUFFIXES, true);
}
private function isNameSalutation(string $word): bool {
return in_array($this->normalizeNameWord($word), self::NAME_SALUTATIONS, true);
}
private function normalizeNameWord(string $word): string {
return strtolower(preg_replace('/[.,]/', '', trim($word)) ?? $word);
}
private function getAvatarImage(IUser $user): ?IImage {
try {
return $user->getAvatarImage(512);

@ -201,6 +201,19 @@ class ConverterTest extends TestCase {
['Sauron;;;;', 'Sauron'],
['Baggins;Bilbo;;;', 'Bilbo Baggins'],
['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
['Doe;Jane;;;MD', 'Jane Doe, MD'],
['Williams;Mary;;;MD, PhD', 'Mary Williams, MD, PhD'],
['King;Martin;Luther;;Jr.', 'Martin Luther King, Jr.'],
['King;Martin;Luther;;Jr.', 'King, Martin Luther, Jr.'],
['Doe;Jane;;Dr.;', 'Dr. Jane Doe'],
['Doe;Jane;;Prof. Dr.;', 'Prof. Dr. Jane Doe'],
['Lastname;Firstname;;;', 'Lastname, Firstname'],
['Lastname;Firstname;Middlename;;', 'Lastname, Firstname Middlename'],
['b;a;;;c', 'a b, c'],
['a;b;c;;', 'a, b c'],
['Doe;Jane;;;', 'Jane Doe (Contracting)'],
['Smith;Jane;;;Ph.D.', 'Jane Smith Ph.D.'],
['Smith;R.;Jason;;', 'R. Jason Smith'],
];
}

Loading…
Cancel
Save