Skip to content

Commit acbfefc

Browse files
committed
Pin the peel's segment choice and its two boundaries (#308)
A mutation pass found one live survivor on a fully-covered line: replacing the peel's `state.segments[0]` with `range(len(state.tokens))` -- the natural simplification, since under NO_COMMA the two runs are usually identical -- changes real output and fails nothing. Extracted delimited content is still in the token stream at this stage with only its role set, so "김민준씨 (Jimmy)" would hand the scan-back Jimmy as its site: no post-nominal to step over, no tail to peel, and the peel lost entirely. The comment above the segment lookup made it worse by saying extracted content is unreachable and that no input can produce it -- true of the surname half it was written for, actively misleading for the peel, where the choice is what keeps Jimmy out. Case row plus both comments. Two boundaries a reader could otherwise meet by surprise: - "田中さん, PhD" keeps the whole 田中さん as the family name while "田中さん PhD" gives family 田中 and suffix "さん, PhD". A one-word name part before a comma is FAMILY_COMMA, which opts the stage out by doctrine -- the one spelling disagreement #308 does not remove, where the 김민준씨 Jr. pair got two rows for removing exactly that. A case row, so it is a recorded decision. - The off-switch docs/customize.rst promises was verified and untested: removing a tail leaves the glued name unsplit while the spaced spelling still routes the honorific. Pinned at parse level, against a peel-is-on baseline so it cannot pass vacuously. And a clause recording why _is_post_nominal is strict rather than lenient: the initial veto makes "田中さん V." decline where "田中さん II" peels, which agrees with what classify does with "V." downstream. Swapping the predicate fails no test; the clause is the record instead.
1 parent 288fef2 commit acbfefc

4 files changed

Lines changed: 73 additions & 2 deletions

File tree

nameparser/_pipeline/_script_segment.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ def _is_post_nominal(state: ParseState, i: int) -> bool:
254254
surname site reads a True as an ANSWER and declines, rather than as
255255
a token to step past.
256256
257+
STRICT, not lenient: the initial veto applies, so "V." is not a
258+
post-nominal here though bare "v" is a suffix word. That agrees
259+
with what classify does with the same token downstream -- "V." is
260+
a middle initial -- and the difference is reachable under the
261+
default lexicon: "田中さん V." stops its scan-back at the initial
262+
and does not peel, while "田中さん II" steps over II and does.
263+
Swapping in is_suffix_lenient fails no test; this clause is the
264+
record instead.
265+
257266
Suffixes only, deliberately. Should a CJK entry ever join titles,
258267
the surname site would want it excluded while the peel site must
259268
not: a title never trails, so widening the peel's scan-back would
@@ -285,6 +294,13 @@ def _peel_honorific_tail(state: ParseState) -> ParseState:
285294
that returns None where the outright index would raise removes an
286295
unpinned reliance on the FAMILY_COMMA gate landing first.
287296
297+
WHICH run is scanned is a separate decision, and the one a reader
298+
is likeliest to undo: segments[0], never state.tokens. The two
299+
agree under NO_COMMA except where extract_delimited has already
300+
claimed a token, which is still in the stream here with only its
301+
role set -- so "김민준씨 (Jimmy)" is the input that tells them
302+
apart. See the note above the segment lookup in script_segment.
303+
288304
That last token is the last of the NAME PART, which under NO_COMMA
289305
reaches into a maiden clause: maiden tokens are still main-stream
290306
here (extract_delimited has masked only bracketed content), so
@@ -386,8 +402,15 @@ def script_segment(state: ParseState) -> ParseState:
386402
# segments[0] is the NAME part under both remaining structures
387403
# (everything, under NO_COMMA); later segments are suffixes. Its
388404
# members are main-stream token indices by construction, so
389-
# extracted nickname/maiden content is unreachable from here --
390-
# no input can produce it, so no test pins it.
405+
# extracted nickname/maiden content is unreachable from here.
406+
# For the surname site that is merely tidy -- the first
407+
# script-written token is the same either way. For the PEEL above
408+
# it is load-bearing, and iterating state.tokens instead is a
409+
# silent regression rather than a refactor: extracted content is
410+
# still IN the token stream at this stage (only its role is set),
411+
# so "김민준씨 (Jimmy)" would give the scan-back Jimmy as its site,
412+
# which is no post-nominal to step over and ends in no tail, and
413+
# the peel would be lost. Pinned by the case row of that name.
391414
i = next((i for i in state.segments[0]
392415
if effective_script(state.tokens[i].text) in scripts), None)
393416
# A post-nominal in the surname's own position is not a site to

tests/v2/cases.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,32 @@ def __post_init__(self) -> None:
914914
"ko_honorific_glued_given_trailing_suffix, whose "
915915
"comma-less spelling of the same name reaches the same "
916916
"answer by the scan-back instead"),
917+
Case("ko_honorific_glued_given_nickname", "김민준씨 (Jimmy)",
918+
{"family": "김", "given": "민준", "suffix": "씨",
919+
"nickname": "Jimmy"},
920+
classification="fix(#308)",
921+
notes="the other half of indexing the NAME part: extracted "
922+
"content is still in the token stream at this stage but "
923+
"NOT in segments[0], so the scan-back never reaches "
924+
"Jimmy. Scanning the tokens instead would take Jimmy as "
925+
"the site -- it is no post-nominal -- and lose the peel "
926+
"entirely, with 씨 back in the given name. Nothing else "
927+
"pins that choice: under NO_COMMA the two are otherwise "
928+
"the same run"),
929+
Case("ko_honorific_glued_family_comma", "田中さん, PhD",
930+
{"family": "田中さん", "title": "PhD"},
931+
classification="fix(#271)",
932+
notes="the comma doctrine outranks the peel, and this is "
933+
"where a reader meets that boundary: a one-word name "
934+
"part before a comma is FAMILY_COMMA, which opts the "
935+
"whole stage out, so 田中さん stays whole where the "
936+
"comma-less 田中さん PhD gives family 田中 and suffix "
937+
"'さん, PhD'. Deliberate -- whoever wrote the comma "
938+
"already said where the family name ends -- and the "
939+
"one spelling disagreement #308 does not remove, unlike "
940+
"the 김민준씨 Jr. pair above. Classified to #271, not "
941+
"#308: the fields here are the CJK order flip's, and "
942+
"the peel never fires on this input"),
917943
Case("zh_honorific_glued_surname", "王先生",
918944
{"family": "王", "suffix": "先生"},
919945
locale="zh",

tests/v2/test_lexicon.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,23 @@ def test_default_lexicon_honorific_tails_are_all_suffix_words() -> None:
608608
# not a content pin.
609609
d = Lexicon.default()
610610
assert d.honorific_tails <= d.suffix_words
611+
612+
613+
def test_removing_a_honorific_tail_is_the_peels_off_switch() -> None:
614+
# The off-switch docs/customize.rst documents, at parse level:
615+
# dropping a tail leaves the glued name unsplit while the SPACED
616+
# spelling still routes the honorific, since suffix_words is a
617+
# separate field and only the peel consults honorific_tails.
618+
# honorific_tails is the one marker field a caller can empty
619+
# without a matching base edit -- an orphan is what raises, and a
620+
# removal cannot make one.
621+
lex = Lexicon.default().remove(honorific_tails={"さん"})
622+
p = Parser(lexicon=lex)
623+
glued = p.parse("田中さん")
624+
assert (glued.family, glued.suffix) == ("田中さん", "")
625+
spaced = p.parse("田中 さん")
626+
assert (spaced.family, spaced.suffix) == ("田中", "さん")
627+
# the baseline the removal is against, so the test cannot pass by
628+
# the peel being broken outright
629+
on = Parser().parse("田中さん")
630+
assert (on.family, on.suffix) == ("田中", "さん")

tools/differential/corpus_cjk.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"田中 殿"
3838
"田中『ハナ』花子"
3939
"田中さん"
40+
"田中さん, PhD"
4041
"田中博士"
4142
"諸葛亮"
4243
"阿明"
@@ -56,6 +57,7 @@
5657
"김민준님"
5758
"김민준박사님"
5859
"김민준씨"
60+
"김민준씨 (Jimmy)"
5961
"김민준씨 Jr."
6062
"김선생님"
6163
"김씨"

0 commit comments

Comments
 (0)