Skip to content

Commit 021823e

Browse files
committed
Apply suffix_delimiter only at suffix-consumption sites
Previously the delimiter split ran unconditionally on every post-comma segment before the parser had decided which segment is actually a suffix group. That let it leak into first/middle-name segments in inverted format (e.g. "Doe, Mary - Kate, RN"), which was documented as a known limitation. Move the split to the three places that actually treat a segment as suffixes: the suffix-comma vs. lastname-comma format detection, and the two suffix_list += parts[...] consumption points. The detection check now expands parts[1] to correctly recognize delimiter-joined suffixes (e.g. "RN - CRNA") without ever expanding a segment that turns out to be a name.
1 parent 76098cc commit 021823e

3 files changed

Lines changed: 30 additions & 20 deletions

File tree

nameparser/config/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,11 @@ class Constants:
343343
the full name is already split on bare commas first, and each resulting
344344
part is stripped of surrounding whitespace before this step runs.
345345
346-
Known limitation: the expansion is applied to all post-comma parts, not
347-
just suffix groups. In inverted format (``"Last, First, suffix"``), the
348-
first-name part is also split on the delimiter. In practice this is
349-
harmless since first names rarely contain the delimiter string, but a
350-
name like ``"Doe, Mary - Kate, RN"`` with ``suffix_delimiter=" - "``
351-
would misparse.
346+
The delimiter is only applied to parts once they've been identified as
347+
a suffix group, so it never leaks into a first- or middle-name part. For
348+
example, in inverted format (``"Last, First, suffix"``) a hyphenated
349+
given name like ``"Doe, Mary - Kate, RN"`` with ``suffix_delimiter=" - "``
350+
does not get mistaken for a suffix split.
352351
"""
353352

354353
empty_attribute_default = ''

nameparser/parser.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,16 @@ def is_suffix_lenient(self, piece: str) -> bool:
674674
"""
675675
return lc(piece) in self.C.suffix_not_acronyms or self.is_suffix(piece)
676676

677+
def expand_suffix_delimiter(self, part: str) -> list[str]:
678+
"""Split a single post-comma part on :py:attr:`suffix_delimiter`,
679+
if configured. Used only at suffix-consumption sites, where a part
680+
has already been identified as a suffix group, so splitting it
681+
further can't misparse an unrelated name segment.
682+
"""
683+
if not self.suffix_delimiter:
684+
return [part]
685+
return [p for p in (p.strip() for p in part.split(self.suffix_delimiter)) if p]
686+
677687
def are_suffixes_after_comma(self, pieces: Iterable[str]) -> bool:
678688
"""Return True if all pieces are suffixes by the lenient
679689
:py:func:`is_suffix_lenient` test. Used when detecting suffix-comma
@@ -1004,12 +1014,6 @@ def parse_full_name(self) -> None:
10041014
parts = [x.strip() for x in self._full_name.split(",")]
10051015
self._had_comma = len(parts) > 1
10061016

1007-
if self.suffix_delimiter and len(parts) > 1:
1008-
expanded = [parts[0]]
1009-
for part in parts[1:]:
1010-
expanded.extend([p for p in (p.strip() for p in part.split(self.suffix_delimiter)) if p])
1011-
parts = expanded
1012-
10131017
log.debug("full_name: %s", self._full_name)
10141018
log.debug("parts: %s", parts)
10151019

@@ -1063,14 +1067,18 @@ def parse_full_name(self) -> None:
10631067

10641068
post_comma_pieces = self.parse_pieces(parts[1].split(' '), 1)
10651069

1066-
if self.are_suffixes_after_comma(parts[1].split(' ')) \
1070+
suffix_delimiter_pieces = [word for part in self.expand_suffix_delimiter(parts[1])
1071+
for word in part.split(' ')]
1072+
1073+
if self.are_suffixes_after_comma(suffix_delimiter_pieces) \
10671074
and len(parts[0].split(' ')) > 1:
10681075

10691076
# suffix comma:
10701077
# title first middle last [suffix], suffix [suffix] [, suffix]
10711078
# parts[0], parts[1:...]
10721079

1073-
self.suffix_list += parts[1:]
1080+
for part in parts[1:]:
1081+
self.suffix_list += self.expand_suffix_delimiter(part)
10741082
pieces = self.parse_pieces(parts[0].split(' '))
10751083
pieces = self._join_bound_first_name(pieces, reserve_last=True)
10761084
log.debug("pieces: %s", str(pieces))
@@ -1144,7 +1152,8 @@ def parse_full_name(self) -> None:
11441152
self.middle_list.append(piece)
11451153
try:
11461154
if parts[2]:
1147-
self.suffix_list += parts[2:]
1155+
for part in parts[2:]:
1156+
self.suffix_list += self.expand_suffix_delimiter(part)
11481157
except IndexError:
11491158
pass
11501159

tests/test_suffixes.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,14 @@ def test_suffix_delimiter_comma_space_is_noop(self) -> None:
265265
hn = HumanName("John Doe, MD, PhD", suffix_delimiter=", ")
266266
self.m(hn.suffix, "MD, PhD", hn)
267267

268-
def test_suffix_delimiter_inverted_format_known_limitation(self) -> None:
269-
# In inverted format, the first-name part is also split on the delimiter.
270-
# "Mary - Kate" becomes two separate parts, causing a wrong parse.
271-
# This is a documented limitation — do not "fix" it without a broader solution.
268+
def test_suffix_delimiter_inverted_format_not_misparsed(self) -> None:
269+
# The delimiter only expands parts once they're identified as a
270+
# suffix group, so a hyphenated given name in inverted format isn't
271+
# mistaken for a suffix split.
272272
hn = HumanName("Doe, Mary - Kate, RN", suffix_delimiter=" - ")
273-
self.assertNotEqual(hn.first, "Mary - Kate")
273+
self.m(hn.first, "Mary", hn)
274+
self.m(hn.last, "Doe", hn)
275+
self.m(hn.suffix, "RN", hn)
274276

275277
def test_suffix_acronyms_ambiguous_is_customizable(self) -> None:
276278
from nameparser.config import Constants

0 commit comments

Comments
 (0)