Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions nameparser/_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
33 changes: 33 additions & 0 deletions tests/test_capitalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
1 change: 1 addition & 0 deletions tools/differential/corpus_rules.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down