From c44d92e3cd7318c9916206bb808aa18e60ba771f Mon Sep 17 00:00:00 2001 From: IMGillusion Date: Thu, 10 Sep 2026 12:45:55 +0800 Subject: [PATCH] fix(#459): repair one-case suffix acronyms to all-caps Gated on the SUFFIX role; exceptions map consulted first so md/phd keep their special casing. R4 example added; corpus regenerated. --- docs/design/rules.md | 1 + nameparser/_render.py | 10 ++++++++ tests/test_capitalization.py | 33 +++++++++++++++++++++++++++ tools/differential/corpus_rules.jsonl | 1 + 4 files changed, 45 insertions(+) diff --git a/docs/design/rules.md b/docs/design/rules.md index ba455a15..e60a8fe4 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -1676,6 +1676,7 @@ R4. Rationale: case repair is a display concern, applied only on "ANH DO" → capitalized="Anh Do" "anh van do" → capitalized="Anh Van Do" "john smith phd" → capitalized="John Smith Ph.D." + "john smith mba" → capitalized="John Smith MBA" "juan de la vega" → capitalized="Juan de la Vega" · boundary Accepted: the clause reaches a part the parser read. A field spliced in as raw text after the parse carries no reading of its diff --git a/nameparser/_render.py b/nameparser/_render.py index bf3786ca..6952af19 100644 --- a/nameparser/_render.py +++ b/nameparser/_render.py @@ -247,6 +247,16 @@ def _cap_word(word: str, role: Role, tags: frozenset[str], exception = lex.capitalization_exceptions_map.get(key) if exception is not None: return exception + # A credential acronym the exceptions map doesn't carry (mba, jd, + # qc, mp, ...) is an initialism, not a word to title-case. The map + # only special-cases the five that need non-all-caps spelling + # (md, phd, ii, iii, iv); every other suffix_acronyms entry is + # all-caps by definition, so a one-case name repairs to the + # acronym's caps instead of 'Mba' (#459). Gated on the SUFFIX role + # so a word that is a family name only happens to be in the + # vocabulary (anh van DO) still repairs as an ordinary name word. + if role is Role.SUFFIX and normalized.replace(".", "") in lex.suffix_acronyms: + return word.upper() if _MAC.match(word): return _MAC.sub( lambda m: m.group(1).capitalize() + m.group(2).capitalize(), diff --git a/tests/test_capitalization.py b/tests/test_capitalization.py index 4c8f0576..0ba500a2 100644 --- a/tests/test_capitalization.py +++ b/tests/test_capitalization.py @@ -110,6 +110,39 @@ def test_capitalize_suffix_acronym_with_dots(self) -> None: hn.capitalize() self.assertEqual(hn.suffix, 'M.D.') + # A credential acronym the exceptions map doesn't carry is an + # initialism, so a one-case suffix repairs to all-caps instead of + # title-case (issue #459). + def test_capitalize_suffix_acronym_is_all_caps(self) -> None: + for src, expect in [ + ('JOHN SMITH MBA', 'John Smith MBA'), + ('john smith jd', 'John Smith JD'), + ('JOSE LUIS CPA', 'Jose Luis CPA'), + ('john smith pmp', 'John Smith PMP'), + ]: + hn = HumanName(src) + hn.capitalize() + self.m(str(hn), expect, hn) + + # The exceptions map's five keep their special casing; the new + # all-caps path must not shadow them (#459). + def test_capitalize_exceptions_still_win_over_acronyms(self) -> None: + for src, expect in [ + ('john smith md', 'John Smith M.D.'), + ('john smith phd', 'John Smith Ph.D.'), + ]: + hn = HumanName(src) + hn.capitalize() + self.m(str(hn), expect, hn) + + # A word in the acronym vocabulary that parses as a family name + # still repairs as an ordinary name word, not an acronym (#459). + def test_capitalize_family_name_in_acronym_vocab_stays_title_case(self) -> None: + hn = HumanName('anh van do') + hn.capitalize() + self.m(str(hn), 'Anh Van Do', hn) + + # Leaving already-capitalized names alone def test_no_change_to_mixed_chase(self) -> None: hn = HumanName('Shirley Maclaine') diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index b30b894f..79c7a7c1 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -247,6 +247,7 @@ "de la Vega y Santos Juan" "de los Santos" "ibn Awf abdul Rahman" +"john smith mba" "john smith phd" "juan de la vega" "juan mcdonald"