Skip to content

Commit b488e97

Browse files
committed
Merge branch 'master' of github.com:derek73/python-nameparser
2 parents 35b9dfc + 2fb327c commit b488e97

5 files changed

Lines changed: 100 additions & 0 deletions

File tree

docs/customize.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Other editable attributes
5959
* :py:obj:`~nameparser.config.Constants.empty_attribute_default` - value returned by empty attributes, defaults to empty string
6060
* :py:obj:`~nameparser.config.Constants.capitalize_name` - If set, applies :py:meth:`~nameparser.parser.HumanName.capitalize` to :py:class:`~nameparser.parser.HumanName` instance.
6161
* :py:obj:`~nameparser.config.Constants.force_mixed_case_capitalization` - If set, forces the capitalization of mixed case strings when :py:meth:`~nameparser.parser.HumanName.capitalize` is called.
62+
* :py:obj:`~nameparser.config.Constants.suffix_delimiter` - additional delimiter used to split suffix groups after comma-splitting, e.g. ``" - "`` for names like ``"Jane Smith, RN - CRNA"``. Defaults to ``None`` (disabled).
6263

6364

6465

docs/release_log.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Release Log
22
===========
3+
* 1.2.2 - June 28, 2026
4+
- Add ``suffix_delimiter`` to ``Constants`` and ``HumanName`` for parsing suffixes separated by arbitrary delimiters, e.g. ``"RN - CRNA"`` (#156)
35
* 1.2.1 - June 19, 2026
46
- Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default``
57
- Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name

nameparser/config/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,25 @@ class Constants:
228228
spacing from the template is still applied.
229229
"""
230230

231+
suffix_delimiter = None
232+
"""
233+
If set, an additional delimiter used to split suffix groups after
234+
comma-splitting. For example, setting ``suffix_delimiter=" - "`` allows
235+
``"RN - CRNA"`` to be parsed as two separate suffixes. Default is
236+
``None`` (no additional splitting beyond the standard comma split).
237+
238+
Note: setting this to ``","`` or ``", "`` has no additional effect —
239+
the full name is already split on bare commas first, and each resulting
240+
part is stripped of surrounding whitespace before this step runs.
241+
242+
Known limitation: the expansion is applied to all post-comma parts, not
243+
just suffix groups. In inverted format (``"Last, First, suffix"``), the
244+
first-name part is also split on the delimiter. In practice this is
245+
harmless since first names rarely contain the delimiter string, but a
246+
name like ``"Doe, Mary - Kate, RN"`` with ``suffix_delimiter=" - "``
247+
would misparse.
248+
"""
249+
231250
empty_attribute_default = ''
232251
"""
233252
Default return value for empty attributes.

nameparser/parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ class HumanName:
5353
:param str string_format: python string formatting
5454
:param str initials_format: python initials string formatting
5555
:param str initials_delimter: string delimiter for initials
56+
:param str initials_separator: string separator between consecutive initials
57+
:param str suffix_delimiter: additional delimiter to split post-comma parts
58+
before suffix detection, e.g. ``" - "`` for ``"RN - CRNA"``
5659
:param str first: first name
5760
:param str middle: middle name
5861
:param str last: last name
@@ -95,6 +98,7 @@ def __init__(
9598
initials_format: str | None = None,
9699
initials_delimiter: str | None = None,
97100
initials_separator: str | None = None,
101+
suffix_delimiter: str | None = None,
98102
first: str | list[str] | None = None,
99103
middle: str | list[str] | None = None,
100104
last: str | list[str] | None = None,
@@ -111,6 +115,7 @@ def __init__(
111115
self.initials_format = initials_format if initials_format is not None else self.C.initials_format
112116
self.initials_delimiter = initials_delimiter if initials_delimiter is not None else self.C.initials_delimiter
113117
self.initials_separator = initials_separator if initials_separator is not None else self.C.initials_separator
118+
self.suffix_delimiter = suffix_delimiter if suffix_delimiter is not None else self.C.suffix_delimiter
114119
if (first or middle or last or title or suffix or nickname):
115120
self.first = first
116121
self.middle = middle
@@ -623,6 +628,12 @@ def parse_full_name(self) -> None:
623628
# break up full_name by commas
624629
parts = [x.strip() for x in self._full_name.split(",")]
625630

631+
if self.suffix_delimiter and len(parts) > 1:
632+
expanded = [parts[0]]
633+
for part in parts[1:]:
634+
expanded.extend([p for p in (p.strip() for p in part.split(self.suffix_delimiter)) if p])
635+
parts = expanded
636+
626637
log.debug("full_name: %s", self._full_name)
627638
log.debug("parts: %s", parts)
628639

tests/test_suffixes.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,70 @@ def test_suffix_with_periods_with_lastname_comma(self) -> None:
136136
self.m(hn.first, "John", hn)
137137
self.m(hn.last, "Doe", hn)
138138
self.m(hn.suffix, "Msc.Ed.", hn)
139+
140+
def test_suffix_delimiter_default_on_constants(self) -> None:
141+
from nameparser.config import CONSTANTS
142+
self.assertIs(CONSTANTS.suffix_delimiter, None)
143+
144+
def test_suffix_delimiter_kwarg_accepted(self) -> None:
145+
hn = HumanName("Steven Hardman, RN - CRNA", suffix_delimiter=" - ")
146+
self.assertEqual(hn.suffix_delimiter, " - ")
147+
148+
def test_suffix_delimiter_basic(self) -> None:
149+
hn = HumanName("Steven Hardman, RN - CRNA", suffix_delimiter=" - ")
150+
self.m(hn.first, "Steven", hn)
151+
self.m(hn.last, "Hardman", hn)
152+
self.m(hn.suffix, "RN, CRNA", hn)
153+
154+
def test_suffix_delimiter_multiple(self) -> None:
155+
hn = HumanName("John Doe, MD - PhD - FACS", suffix_delimiter=" - ")
156+
self.m(hn.first, "John", hn)
157+
self.m(hn.last, "Doe", hn)
158+
self.m(hn.suffix, "MD, PhD, FACS", hn)
159+
160+
def test_suffix_delimiter_no_effect_without_comma(self) -> None:
161+
# suffix_delimiter only applies after the comma split; space-separated
162+
# suffixes already work via the no-comma parse path
163+
hn = HumanName("John Doe MD PhD", suffix_delimiter=" - ")
164+
self.m(hn.first, "John", hn)
165+
self.m(hn.last, "Doe", hn)
166+
self.m(hn.suffix, "MD, PhD", hn)
167+
168+
def test_suffix_delimiter_constants_level(self) -> None:
169+
from nameparser.config import CONSTANTS
170+
_orig = CONSTANTS.suffix_delimiter
171+
try:
172+
CONSTANTS.suffix_delimiter = " - "
173+
hn = HumanName("Steven Hardman, RN - CRNA")
174+
self.m(hn.first, "Steven", hn)
175+
self.m(hn.last, "Hardman", hn)
176+
self.m(hn.suffix, "RN, CRNA", hn)
177+
finally:
178+
CONSTANTS.suffix_delimiter = _orig
179+
180+
def test_suffix_delimiter_none_by_default_known_limitation(self) -> None:
181+
# Without suffix_delimiter set, " - " between suffixes breaks parsing.
182+
# This test documents the known limitation — do not "fix" it.
183+
hn = HumanName("Steven Hardman, RN - CRNA")
184+
self.m(hn.first, "RN", hn)
185+
self.m(hn.last, "Steven Hardman", hn)
186+
self.m(hn.suffix, "CRNA", hn)
187+
188+
def test_suffix_delimiter_trailing_delimiter_ignored(self) -> None:
189+
# Trailing delimiter produces an empty token that must be filtered out.
190+
# Using a non-whitespace-terminated delimiter so stripping doesn't consume it.
191+
hn = HumanName("John Doe, MD-PhD-", suffix_delimiter="-")
192+
self.m(hn.first, "John", hn)
193+
self.m(hn.last, "Doe", hn)
194+
self.m(hn.suffix, "MD, PhD", hn)
195+
196+
def test_suffix_delimiter_comma_space_is_noop(self) -> None:
197+
hn = HumanName("John Doe, MD, PhD", suffix_delimiter=", ")
198+
self.m(hn.suffix, "MD, PhD", hn)
199+
200+
def test_suffix_delimiter_inverted_format_known_limitation(self) -> None:
201+
# In inverted format, the first-name part is also split on the delimiter.
202+
# "Mary - Kate" becomes two separate parts, causing a wrong parse.
203+
# This is a documented limitation — do not "fix" it without a broader solution.
204+
hn = HumanName("Doe, Mary - Kate, RN", suffix_delimiter=" - ")
205+
self.assertNotEqual(hn.first, "Mary - Kate")

0 commit comments

Comments
 (0)