From b72adf6b291204f44caa6c775eaf90bc494bec16 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 28 Jun 2026 23:30:24 -0700 Subject: [PATCH 1/2] fix: recognize single-letter suffix_not_acronyms (e.g. 'V') in suffix-comma format (closes #136) Roman numeral suffixes like 'V' are single uppercase letters, which the initial regex also matches. In suffix-comma format ("John Ingram, V") the post-comma position is unambiguous, so the initial guard should not block recognition. Add are_suffixes_after_comma() that bypasses the initial check for suffix_not_acronyms members and use it at the suffix-comma detection site, leaving is_suffix() unchanged for the no-comma path where 'V' mid-name should still be treated as an initial. Co-Authored-By: Claude Sonnet 4.6 --- nameparser/parser.py | 17 ++++++++++++++++- tests/test_suffixes.py | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/nameparser/parser.py b/nameparser/parser.py index a651cf6..182e34d 100644 --- a/nameparser/parser.py +++ b/nameparser/parser.py @@ -487,6 +487,21 @@ def are_suffixes(self, pieces: Iterable[str]) -> bool: return False return True + def are_suffixes_after_comma(self, pieces: Iterable[str]) -> bool: + """Like are_suffixes but suffix_not_acronyms members are always + recognized, even when they look like single-letter initials. + + Used when detecting suffix-comma format (e.g. "John Ingram, V") where + the post-comma position is unambiguous: a single uppercase roman + numeral like 'V' is definitionally a suffix there, not an initial. + """ + for piece in pieces: + if lc(piece) in self.C.suffix_not_acronyms: + continue + if not self.is_suffix(piece): + return False + return True + def is_rootname(self, piece: str) -> bool: """ Is not a known title, suffix or prefix. Just first, middle, last names. @@ -686,7 +701,7 @@ def parse_full_name(self) -> None: post_comma_pieces = self.parse_pieces(parts[1].split(' '), 1) - if self.are_suffixes(parts[1].split(' ')) \ + if self.are_suffixes_after_comma(parts[1].split(' ')) \ and len(parts[0].split(' ')) > 1: # suffix comma: diff --git a/tests/test_suffixes.py b/tests/test_suffixes.py index 115543d..5773968 100644 --- a/tests/test_suffixes.py +++ b/tests/test_suffixes.py @@ -34,6 +34,15 @@ def test_two_suffixes_lastname_comma_format(self) -> None: # NOTE: this adds a comma when the original format did not have one. self.m(hn.suffix, "Jr., MD", hn) + def test_roman_numeral_v_suffix_comma_format(self) -> None: + # 'V' is a single uppercase letter, so it was incorrectly matched as an + # initial and not recognized as a suffix (issue #136) + hn = HumanName("John W. Ingram, V") + self.m(hn.first, "John", hn) + self.m(hn.middle, "W.", hn) + self.m(hn.last, "Ingram", hn) + self.m(hn.suffix, "V", hn) + def test_two_suffixes_suffix_comma_format(self) -> None: hn = HumanName("Franklin Washington, Jr. MD") self.m(hn.first, "Franklin", hn) From c54b2cf7f723a2467d8ad3e9d7820369d690fe85 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 28 Jun 2026 23:36:48 -0700 Subject: [PATCH 2/2] test: add coverage for suffix_not_acronyms members in suffix-comma format Add tests for 'I' (same single-letter ambiguity as 'V'), a mixed suffix-comma case (V MD), and two suffix_not_acronyms members together (V Jr.). Also tighten the are_suffixes_after_comma docstring to describe the short-circuit mechanism accurately and enumerate the full scope of affected suffix_not_acronyms entries. Co-Authored-By: Claude Sonnet 4.6 --- nameparser/parser.py | 10 ++++++---- tests/test_suffixes.py | 24 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/nameparser/parser.py b/nameparser/parser.py index 182e34d..f8dd297 100644 --- a/nameparser/parser.py +++ b/nameparser/parser.py @@ -488,12 +488,14 @@ def are_suffixes(self, pieces: Iterable[str]) -> bool: return True def are_suffixes_after_comma(self, pieces: Iterable[str]) -> bool: - """Like are_suffixes but suffix_not_acronyms members are always - recognized, even when they look like single-letter initials. + """Like are_suffixes, but pieces found in suffix_not_acronyms are + accepted unconditionally without passing through is_suffix(). Used when detecting suffix-comma format (e.g. "John Ingram, V") where - the post-comma position is unambiguous: a single uppercase roman - numeral like 'V' is definitionally a suffix there, not an initial. + the post-comma position is unambiguous. This covers all + suffix_not_acronyms members (i, ii, iii, iv, v, jr, sr, etc.), + case-insensitively, including single-letter entries that is_suffix() + would otherwise reject via is_an_initial(). """ for piece in pieces: if lc(piece) in self.C.suffix_not_acronyms: diff --git a/tests/test_suffixes.py b/tests/test_suffixes.py index 5773968..221e803 100644 --- a/tests/test_suffixes.py +++ b/tests/test_suffixes.py @@ -35,14 +35,34 @@ def test_two_suffixes_lastname_comma_format(self) -> None: self.m(hn.suffix, "Jr., MD", hn) def test_roman_numeral_v_suffix_comma_format(self) -> None: - # 'V' is a single uppercase letter, so it was incorrectly matched as an - # initial and not recognized as a suffix (issue #136) + # suffix-comma position is unambiguous: 'V' must be a suffix, not a single-letter initial hn = HumanName("John W. Ingram, V") self.m(hn.first, "John", hn) self.m(hn.middle, "W.", hn) self.m(hn.last, "Ingram", hn) self.m(hn.suffix, "V", hn) + def test_roman_numeral_i_suffix_comma_format(self) -> None: + # 'I' has the same single-letter ambiguity as 'V' + hn = HumanName("John W. Smith, I") + self.m(hn.first, "John", hn) + self.m(hn.middle, "W.", hn) + self.m(hn.last, "Smith", hn) + self.m(hn.suffix, "I", hn) + + def test_suffix_not_acronym_then_acronym_suffix_comma_format(self) -> None: + # single-letter suffix_not_acronyms entry followed by an acronym suffix + hn = HumanName("John Smith, V MD") + self.m(hn.first, "John", hn) + self.m(hn.last, "Smith", hn) + self.m(hn.suffix, "V MD", hn) + + def test_two_suffix_not_acronyms_suffix_comma_format(self) -> None: + hn = HumanName("John Smith, V Jr.") + self.m(hn.first, "John", hn) + self.m(hn.last, "Smith", hn) + self.m(hn.suffix, "V Jr.", hn) + def test_two_suffixes_suffix_comma_format(self) -> None: hn = HumanName("Franklin Washington, Jr. MD") self.m(hn.first, "Franklin", hn)