Compare commits

...
Author SHA1 Message Date
Félix Malfait 492e74f18e fix(contact-creation): handle multi-comma "Last, First, Suffix" display names
The comma-inverted swap previously required exactly one comma — names
like "Smith, Jane, Jr.", "O'Brien, Mary, MD" or "Doe, John, Patrick"
fell through to the space-split fallback, which stored the comma in
firstName ("Smith,") and produced a garbled display.

Now we split on the first comma and treat the remainder as the first
name, collapsing any further commas to spaces. The stored firstName
never contains a stray comma, and the existing :GROUP tag stripping
still applies on top.
2026-05-28 17:56:25 +02:00
2 changed files with 74 additions and 5 deletions
@@ -63,6 +63,62 @@ describe('getParsedNameFromDisplayName', () => {
expected: { firstName: 'John', lastName: 'Doe' },
},
},
{
title:
'should keep a multi-word first name intact when paired with a last name',
context: {
displayName: 'Smith, Mary Jane',
expected: { firstName: 'Mary Jane', lastName: 'Smith' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
});
});
describe('multi-comma comma-inverted forms', () => {
const testCases: TestCase[] = [
{
title:
'should treat the segment before the first comma as the last name and merge the rest into the first name',
context: {
displayName: 'Smith, Jane, Jr.',
expected: { firstName: 'Jane Jr.', lastName: 'Smith' },
},
},
{
title: 'should keep credential suffixes attached to the first name',
context: {
displayName: "O'Brien, Mary, MD",
expected: { firstName: 'Mary MD', lastName: "O'Brien" },
},
},
{
title:
'should fold a three-part "Last, First, Middle" form into a multi-word first name',
context: {
displayName: 'Doe, John, Patrick',
expected: { firstName: 'John Patrick', lastName: 'Doe' },
},
},
{
title:
'should collapse extra whitespace around the inner commas as it merges',
context: {
displayName: 'Smith , Jane , Jr.',
expected: { firstName: 'Jane Jr.', lastName: 'Smith' },
},
},
{
title:
'should still strip a trailing :GROUP tag after a multi-comma swap',
context: {
displayName: 'Smith, Jane, Jr.:GROUP',
expected: { firstName: 'Jane Jr.', lastName: 'Smith' },
},
},
];
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
@@ -24,13 +24,26 @@ export const getParsedNameFromDisplayName = (
lastName: stripTrailingGroupTag(parsed.lastName),
});
const commaMatch = cleaned.match(/^([^,]+),\s*([^,]+)$/);
// Comma-inverted forms: "Last, First", "Last, First Middle", and
// multi-comma shapes like "Last, First, Jr." or "Last, First, MD" that some
// address books and ATSes emit. We split on the first comma and treat the
// remainder as the first name, collapsing any further commas to spaces so
// the stored firstName never contains a stray comma.
const commaMatch = cleaned.match(/^([^,]+),\s*(.+)$/);
if (isDefined(commaMatch)) {
return withGroupTagsStripped({
firstName: commaMatch[2].trim(),
lastName: commaMatch[1].trim(),
});
const lastName = commaMatch[1].trim();
const firstName = commaMatch[2]
.trim()
.replace(/\s*,\s*/g, ' ')
.replace(/\s+/g, ' ');
if (isNonEmptyString(firstName) && isNonEmptyString(lastName)) {
return withGroupTagsStripped({
firstName,
lastName,
});
}
}
const [firstToken, ...rest] = cleaned.split(/\s+/);