Skip to content

Commit 805bcf8

Browse files
authored
Merge pull request #463 from derek73/fix/458-461-render-honors-the-parse
Render views honor the parser's decisions instead of re-deciding (#458)
2 parents 82a7bd1 + 122e75d commit 805bcf8

11 files changed

Lines changed: 578 additions & 64 deletions

File tree

docs/design/decisions.md

Lines changed: 9 additions & 1 deletion
Large diffs are not rendered by default.

docs/design/mechanisms.md

Lines changed: 5 additions & 1 deletion
Large diffs are not rendered by default.

docs/design/rules.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,17 @@ R3. Rationale: initials abbreviate the person's name words; titles,
11011101
contribute nothing — except the particles of a part whose every
11021102
word is one, which are not acting as particles there (R2) and
11031103
initial like any other name word. A CONJUNCTION never initials,
1104-
so a base that is one contributes nothing even then.
1104+
so a base that is one contributes nothing even then. That
1105+
carve-out is stated for the middle and base family words; the
1106+
GIVEN group is not settled here. A conjunction written among
1107+
given names does initial today, and this document does not yet
1108+
say whether it should — because two of its own rules answer
1109+
differently and neither answer has been taken: this rule counts
1110+
name words, while P3 makes a connective and its neighbours ONE
1111+
name word, so a joined given group owes one initial under P3 and
1112+
one per joined name word under the carve-out. Until that is
1113+
decided the given group's answer is pinned-but-undocumented
1114+
rather than specified, and no line below asserts it.
11051115
"Dr. Juan Q. Xavier de la Vega III" → initials="J. Q. X. V."
11061116
"Anh Do" → initials="A. D."
11071117
"Nguyen, Van Le" → initials="V. L. N."
@@ -1110,7 +1120,32 @@ R3. Rationale: initials abbreviate the person's name words; titles,
11101120
rather than nothing: they are the base (R2), so they initial.
11111121
"Juan van der" → initials="J. v. d."
11121122
"Juan de y" → initials="J."
1113-
history: decisions.md#R2 · interacts: R2 · implemented: nameparser/_render.py, nameparser/_facade.py
1123+
Accepted: this rule reads a part the parser read. A field set as
1124+
raw text after the parse carries no reading, and this view is
1125+
handed no vocabulary to supply one — it takes a format spec and
1126+
two separators and nothing else — so every word of such a field
1127+
initials, particles and conjunctions alike: a family set that way
1128+
to "de la vega" gives "j. d. l. v." where parsing the same
1129+
name gives "j. v.". Case repair IS handed a vocabulary, so it falls
1130+
back for the one question a word can answer on its own, and R4
1131+
says which. Revising the field through the parser classifies it
1132+
and matches the parse in both views. Stated without an example
1133+
line because every line here names an input string, and this
1134+
shape needs a field edited after the parse.
1135+
Accepted: the unsettled given-group answer above is neither rare
1136+
nor hypothetical — 25 of the corpus names carry a conjunction
1137+
among the given names, every one of them reachable from the
1138+
default vocabulary, and it has initialed since 1.4.0. It carries
1139+
no marked deviation, for the reason that mechanism exists: a
1140+
marker states the INTENDED value, and one name, "John and Jane
1141+
Smith", has four candidates. Today gives "J. a. J. S."; the
1142+
carve-out read as written gives "J. J. S."; P3's one-name-word
1143+
join gives just "J. S."; and 1.4.0 gave "J a J. S.". Marking it
1144+
would put an invented value in a normative document and hold
1145+
the parser to it. #461 asks the neighbouring question about the
1146+
all-particle base and does not own this one; decisions.md#R2
1147+
carries the population and the measurements.
1148+
history: decisions.md#R2 · interacts: P3, R2, R4 · implemented: nameparser/_render.py, nameparser/_facade.py
11141149

11151150
R4. Rationale: case repair is a display concern, applied only on
11161151
request and never destructively.
@@ -1145,6 +1180,27 @@ R4. Rationale: case repair is a display concern, applied only on
11451180
repaired exactly this way before the clause existed. Stated
11461181
without an example line because every line here names an input
11471182
string, and this shape needs a field edited after the parse.
1183+
Accepted, and the reason the boundary is drawn per question
1184+
rather than per field: the conjunction carve-out reaches a word
1185+
the parse read as a conjunction, and where the parse read nothing
1186+
at all it reaches what the vocabulary says. A word of that
1187+
vocabulary standing inside a longer written word is not a
1188+
conjunction, because the parse read that word as one ordinary
1189+
name word — but a field spliced in as raw text was read by
1190+
nobody, so repair asks the vocabulary and a family set to "de y"
1191+
keeps its "y" lowercase. Whether a word is the conjunction or an
1192+
initial is a property of the word, which a vocabulary can answer;
1193+
whether a particle is acting as a particle is a property of the
1194+
whole part, which the parse settles and records; re-deriving it
1195+
needs a reading on every word of the part, and a spliced field
1196+
has none on any, so that half falls through to particle treatment
1197+
and the "de la" boundary above stands. Initials are the contrast
1198+
worth knowing, and R3 states it: that view is handed no
1199+
vocabulary at all, so it falls back on neither question and a
1200+
spliced field's every word initials. revise() classifies the
1201+
value and crosses both questions, in both views: a middle revised
1202+
to "e-f" repairs to "E-F" as the parsed name does, where splicing
1203+
the same text in gives "e-F".
11481204
history: decisions.md#R4 · interacts: R2, R3, R5 · implemented: nameparser/_render.py
11491205

11501206
R5. Rationale: mixed case is evidence that the writer cased the name

docs/release_log.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Release Log
6767

6868
- Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words: ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do`` where it gave ``Anh van do``. A particle earns its name by joining forward to the word it modifies, so a part whose every word is particle vocabulary leaves none of them anything to join; the fix above already made those words anchor ``family_base`` and contribute initials, and case repair now agrees with them rather than reading the same word two ways. The test is the whole part, not a particle standing alone, which is why the two-word family in ``"anh van do"`` moves along with the one-word family in ``"ANH DO"`` -- the same Vietnamese surname, and a standing-alone test would have read it one way behind a given name and another way alone. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``, lowercasing on vocabulary membership alone. The accepted cost is that a degenerate family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der`` where 1.4.0 gave ``Juan van der``. A conjunction is untouched by any of this, so ``"der, y van"`` gives ``y Van Der`` -- the family capitalizing while the conjunction keeps the lowercase it always had; and where the particles DO join a name word nothing changes, ``"juan de la vega"`` still giving ``Juan de la Vega``. Case repair is not one of the seven role fields the differential harness compares, so no gate run can see this change either way and none of its counts move; the ``rules.md#R4`` examples and the v1 capitalization tests are what pin it (closes #407)
6969

70+
- Change case repair to read the parser's own ``conjunction`` tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial. The parse answers that question already -- ``"Scott E. Werner"`` reads ``E.`` as an initial rather than the Italian conjunction -- and the other views honor the answer; case repair asked again, with a shape test applied to each word of a token's text rather than to the token. Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now, ``e-f`` being a middle name and no conjunction of the parse's reading. A conjunction written as a word of its own is untouched, and so is the one-letter carve-out where it applies -- ``"juan y garcia"`` still repairs to ``Juan y Garcia``, ``"JUAN Y GARCIA"`` still to ``Juan Y Garcia``. A field assigned after the parse is unaffected: its text was never classified, so there is no reading to honor and repair asks the vocabulary, applying v1's own predicate the way every earlier version applied it everywhere -- ``h.last = "velasquez y garcia"`` still repairs to ``Velasquez y Garcia`` and ``h.middle = "e."`` to ``E.``. That is the predicate over TODAY's vocabulary, which is narrower than parity with 1.4.0 and the difference is real: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here, because the Cyrillic ``и`` is a 2.x conjunction and was not a 1.4.0 one. What decides which path a token takes is a mark the assignment leaves, not the absence of a span: a value revised through ``Parser.revise()`` is classified by a sub-parse and keeps its tags, so it repairs as the parse does. One reading does change for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary is now an ordinary name word and capitalizes, where 2.1 lowercased it -- tags are what the views read, and a hand-built token that carries none is a token with nothing to declare. Case repair is not one of the seven role fields the differential harness compares, so no gate run can see this change either way and none of its counts move; measured directly instead, no name of the 1094-name differential corpus moves under ``capitalized()`` or ``capitalized(force=True)``, its uppercased and lowercased spellings included -- 6564 name/spelling/lexicon rows and 13128 calls (closes #458)
71+
7072
- Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing. ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name, and the parser has to pick one -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently, and lost the given name doing it. The attachment now reports the fork it decides, in the kind that names the reading it declined. A particle that could be an ordinary name reports ``PARTICLE_OR_GIVEN``: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain one, the ``detail`` naming the ambiguous word. A particle the parser had already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead, because the credential reading is what the attachment overrode: ``"Berg, Jan vd"`` gains one, ``vd`` being read as *van der* rather than as the Volunteer Decoration. Which kind you get follows the reading that was overridden rather than the word's vocabulary, so ``"Berg, Jan do"`` reports ``PARTICLE_OR_GIVEN`` even though ``do`` is a postnominal too -- it was already being read as a name word, so no credential reading was overridden. A particle where nothing was overridden reports nothing at all: ``"Jong, Piet de"`` and ``"Jong, Anke de"`` are unchanged, ``de`` being no name in any reading and no postnominal either, and so is ``"Berg, Jan de vd"``, whose run was read as name words whole. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- the textbook Dutch listing, read exactly right -- now carries a report too. It is the same string shape over the same vocabulary as ``"Nguyen, Thi Van"``, and nothing in the input separates them, so a report on one is a report on both. This adds the report and nothing else: every field these names parse to is exactly what the tussenvoegsel fix below already gave them, and ``ambiguities`` is the only value that grows. Seven differential corpus names gain a kind against the 2.0.0 and 2.1.0 baselines, seven of the nine the tussenvoegsel fix below already moved -- five of ``fix(#379)``'s seven and both of ``fix(#380)``'s two, now carrying ``_ambiguities`` in their diff as well; the two left out are the pair named unchanged above, ``Jong, Piet de`` and ``Jong, Anke de`` -- and none against 1.4.0, which had no ambiguity reporting at all (closes #405)
7173

7274
- Fix a tussenvoegsel after a family comma being parsed as a middle name. Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and the trailing particle run was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. The run now attaches to the family the comma has already named and renders before it, so those read family ``van Beethoven`` and ``van der Berg`` with the given name unchanged. The derived views move with the parse, so ``family_particles`` is ``van`` and ``family_base`` is ``Beethoven`` where they were empty and ``Beethoven`` before. `#130 <https://github.com/derek73/python-nameparser/issues/130>`_ asked for the split and got it in 1.3.0 as ``last_base``/``last_prefixes``; 2.0 renamed them ``family_base``/``family_particles``. What was wrong until now was the values they reported for this listing. Both halves of the particle vocabulary attach -- never-given ``de`` and may-be-given ``van`` alike -- because after a comma the family is already named and the particle has no other role to take. Two guards bound it. A name whose only given word is the particle keeps it, so ``"Nguyen, Van"`` still reads given ``Van``: the attachment needs a given word to spare. And where the word is BOTH particle and suffix vocabulary the attachment outranks the post-nominal reading, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- a trailing abbreviation after a family comma is the tussenvoegsel far more often than the decoration it collides with, and the same shape sweeps in ``mc``, which 2.1 also read as a suffix. ``do`` is in ``SUFFIX_ACRONYMS_AMBIGUOUS`` and 2.1 already read a trailing one as a name word, so it attaches by the plain rule rather than by the override (closes #379, closes #380). Names without the comma are untouched: ``"Ludwig van Beethoven"`` already read family ``van Beethoven`` and is byte-identical. One of the 751 differential corpus names moves, ``"Vega, Juan de la"``, at the 1.4.0, 2.0.0 and 2.1.0 baselines alike

docs/usage.rst

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -887,9 +887,23 @@ clears the flag, while correcting an unrelated field keeps it.
887887

888888
``replace()`` splits values on whitespace into plain, untagged
889889
tokens — the vocabulary knowledge a parse would have about the new
890-
text is not there. The views that depend on tags degrade: the parser
890+
text is not there. The views that read those tags degrade: the parser
891891
no longer knows ``de la`` are particles, so ``family_particles``
892-
empties and the particles start contributing initials.
892+
empties and ``family_base`` takes the whole field.
893+
894+
A token the parse never saw carries no decision to honor, so a view
895+
that is *handed* a vocabulary can fall back to it —
896+
:meth:`~nameparser.ParsedName.capitalized` is the one that is, and it
897+
falls back for one question only: whether a word is a conjunction or
898+
an initial, which a word answers on its own. Whether a particle is
899+
acting as a particle is a fact about the whole part, and there is no
900+
reading on any word of a spliced field to derive it from, so a family
901+
set to ``de la`` stays lowercase where the same words parsed are
902+
repaired to ``De La``. :meth:`~nameparser.ParsedName.initials` takes
903+
no vocabulary at all, so it falls back on neither question and every
904+
word of a spliced field contributes an initial. ``family_particles``
905+
and ``family_base`` are properties on the parsed name, which holds no
906+
vocabulary of its own either.
893907

894908
.. doctest::
895909

@@ -898,8 +912,12 @@ empties and the particles start contributing initials.
898912
>>> replaced = name.replace(family="de la Vega Smith")
899913
>>> replaced.family_particles
900914
''
915+
>>> replaced.family_base
916+
'de la Vega Smith'
901917
>>> replaced.initials()
902918
'J. d. l. V. S.'
919+
>>> name.replace(family="de la").capitalized(force=True).family
920+
'de la'
903921

904922
:meth:`Parser.revise() <nameparser.Parser.revise>` is the same
905923
operation with each value classified by the parser's vocabulary, so

nameparser/_facade.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
from nameparser._config_shim import CONSTANTS, Constants, _cached_parser
3333
from nameparser._lexicon import _normalize
3434
from nameparser._parser import Parser
35-
from nameparser._types import FOLDED_TAG, ParsedName, Role, Token
35+
from nameparser._types import (FOLDED_TAG, UNCLASSIFIED_TAG, ParsedName,
36+
Role, Token)
3637

3738
_V2_FIELD = {"first": "given", "last": "family"} # v1 name -> v2 name
3839
_V1_SPELLING = {v2: v1 for v1, v2 in _V2_FIELD.items()}
@@ -746,8 +747,17 @@ def __setstate__(self, state: dict[str, Any]) -> None:
746747
f"nameparser"
747748
)
748749
for position, word in enumerate(entry.split()):
749-
tokens.append(Token(
750-
word, None, role,
751-
frozenset({"joined"}) if position else frozenset()))
750+
# UNCLASSIFIED_TAG for the same reason replace()
751+
# stamps it: a pickle carries the *_list STRINGS
752+
# and no tags, so nothing here was read by a parse
753+
# and case repair must ask the vocabulary rather
754+
# than read an absent conjunction tag. Without it a
755+
# restored "juan ortega y gasset" repairs to
756+
# "Ortega Y Gasset", which is neither v1's answer
757+
# nor the same name's unpickled one.
758+
tags = {UNCLASSIFIED_TAG}
759+
if position:
760+
tags.add("joined")
761+
tokens.append(Token(word, None, role, frozenset(tags)))
752762
self._parsed = ParsedName(
753763
original=str(state.get("original", "")), tokens=tuple(tokens))

0 commit comments

Comments
 (0)