Skip to content
Merged
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
13 changes: 7 additions & 6 deletions nameparser/_pipeline/_assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
from __future__ import annotations

from nameparser._pipeline._state import (
FIELD_READING, NAME_ROLES, Deferred, ParseState, PendingAmbiguity,
FIELD_READING, NAME_ROLES, Beside, Deferred, ParseState,
PendingAmbiguity,
)
from nameparser._types import (
FOLDED_TAG, Ambiguity, AmbiguityKind, ParsedName, Reading, Role, Token,
Expand All @@ -43,12 +44,12 @@ def _alternative(pending: PendingAmbiguity, materialized: tuple[Token, ...],
if role is Deferred.OTHER_PRIMARY:
role = (Role.FAMILY if materialized[0].role is Role.GIVEN
else Role.GIVEN)
elif role is Deferred.BESIDE and alt.beside in final:
front = final[alt.beside]
role = (alt.unfolded
if FOLDED_TAG in front.tags and alt.unfolded is not None
elif isinstance(role, Beside) and role.index in final:
front = final[role.index]
role = (role.unfolded
if FOLDED_TAG in front.tags and role.unfolded is not None
else front.role)
elif isinstance(role, Deferred):
elif isinstance(role, (Deferred, Beside)):
# assign found no name position to name it from
return None
if middle_as_family and role is Role.MIDDLE:
Expand Down
71 changes: 39 additions & 32 deletions nameparser/_pipeline/_assign.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
)
from nameparser._pipeline._state import (
AMBIGUOUS_ACRONYM_TAG, NAME_ROLES, TITLE_AMBIGUOUS_TAG, ParseState,
Deferred, PendingAmbiguity, PendingReading, Structure,
Beside, Deferred, PendingAmbiguity, PendingReading, Structure,
WorkToken, _AMBIGUOUS_CREDENTIAL_TAGS, _NEVER_FLIPPED, copy_with,
)
from nameparser._policy import Policy, Script
Expand All @@ -121,22 +121,43 @@ def _set_roles(tokens: list[WorkToken], piece: tuple[int, ...],
tokens[i] = copy_with(tokens[i], role=role)


def _name_pieces(state: ParseState, tokens: Sequence[WorkToken],
seg_idx: int) -> list[tuple[int, ...]]:
"""The pieces of segment `seg_idx` the parse read as name words."""
return [q for q in state.pieces[seg_idx]
if tokens[q[0]].role in NAME_ROLES]


def _slot_field(state: ParseState, seg_idx: int, pos: int, count: int,
order: tuple[Role, Role, Role] | None) -> Role:
"""The field position `pos` of `count` name words takes in segment
`seg_idx`, every other word read as the parse read it: the family
before a family comma, the given part's given or middle name after
one, and otherwise the order the main segment's read set."""
if state.structure is Structure.FAMILY_COMMA:
return (Role.FAMILY if seg_idx == 0
else Role.GIVEN if pos == 0 else Role.MIDDLE)
assert order is not None # the main segment's read sets it
return _name_positions(order, count)[pos]


def _name_alt(token: WorkToken, role: Role,
front: Sequence[int] | None, tokens: Sequence[WorkToken],
first: bool, family_fixed: bool) -> PendingReading:
first: bool, licensed: bool) -> PendingReading:
"""The name reading of a word read as something else, landing in
`role` unless the name piece `front` it stands behind would take it
in, so that it lands where that piece lands: a particle chain runs
to the end of the part's name words (rules/P2), and a lone particle
stands unchained only for want of a word behind it, unless it opens
the part, where it never chains (rules/P4); and a bound given name
opening the part joins the word after it where a family comma has
fixed the family or a given-name title stands ahead (rules/P5),
opening the part joins the word after it where it is `licensed`, a
family comma having fixed the family or a given-name title standing
ahead (rules/P5),
since the word would otherwise be the family word the join
reserves; and a never-given particle opening the part folds it into
the family (rules/P1). The front's field is named at
assemble, where P6 may since have attached it to the family, which
it would not have done with a word behind it (`Deferred.BESIDE`)."""
it would not have done with a word behind it (`Beside`)."""
alt = name_reading(token, role)
if front is None:
return alt
Expand All @@ -149,11 +170,10 @@ def _name_alt(token: WorkToken, role: Role,
# not a chain, though `abu` is a particle too: the word stands apart
bound = first and "vocab:bound-given" in tags
if ("particle" in tags and (len(front) > 1 or not first) and not bound
or family_fixed and bound and len(front) == 1):
or licensed and bound and len(front) == 1):
# a front P6 has already attached held `role`'s field before it
return copy_with(alt, role=Deferred.BESIDE, beside=front[0],
unfolded=(role if FOLDED_TAG in tags
else tokens[front[0]].role))
return copy_with(alt, role=Beside(
front[0], role if FOLDED_TAG in tags else tokens[front[0]].role))
return alt


Expand Down Expand Up @@ -533,7 +553,8 @@ def as_a_name(i: int) -> PendingReading:
for j in pieces[k]),
state.lexicon.given_name_titles))
return _name_alt(tokens[i],
_name_positions(order, len(name_pieces) + 1)[-1],
_slot_field(state, seg_idx, len(name_pieces),
len(name_pieces) + 1, order),
front, tokens, len(name_pieces) == 1, licensed)

for r in range(trailing_alts, len(ambiguities)):
Expand Down Expand Up @@ -981,14 +1002,9 @@ def _split_name_alt(state: ParseState, tokens: Sequence[WorkToken],
comma, the given part's given or middle name after one, and
otherwise the last of the part's name words -- or wherever the name
piece in front of it would take it in (`_name_alt`)."""
names = [q for q in state.pieces[seg_idx]
if q[0] < piece[0] and tokens[q[0]].role in NAME_ROLES]
if state.structure is Structure.FAMILY_COMMA:
role = (Role.FAMILY if seg_idx == 0
else Role.MIDDLE if names else Role.GIVEN)
else:
assert order is not None # the main segment's read sets it
role = _name_positions(order, len(names) + 1)[-1]
names = [q for q in _name_pieces(state, tokens, seg_idx)
if q[0] < piece[0]]
role = _slot_field(state, seg_idx, len(names), len(names) + 1, order)
return _name_alt(tokens[piece[0]], role, names[-1] if names else None,
tokens, len(names) == 1,
state.structure is Structure.FAMILY_COMMA)
Expand Down Expand Up @@ -1016,8 +1032,8 @@ def _resolve_deferred(state: ParseState, tokens: Sequence[WorkToken],
or not report.indices):
continue
i = report.indices[0]
for seg_idx, seg_pieces in enumerate(state.pieces):
names = [q for q in seg_pieces if tokens[q[0]].role in NAME_ROLES]
for seg_idx in range(len(state.pieces)):
names = _name_pieces(state, tokens, seg_idx)
at = next((k for k, q in enumerate(names) if i in q), None)
if at is not None:
break
Expand Down Expand Up @@ -1047,19 +1063,10 @@ def _resolve_deferred(state: ParseState, tokens: Sequence[WorkToken],
count == 1
and state.structure is not Structure.FAMILY_COMMA):
ambiguities[r] = copy_with(report, alternative=copy_with(
alt, role=Deferred.BESIDE, beside=i))
alt, role=Beside(i)))
continue
# name words stand only in the parts a structure names: the two
# of a family comma, and otherwise the main one, whose read set
# the order
if state.structure is Structure.FAMILY_COMMA:
role = (Role.FAMILY if seg_idx == 0
else Role.GIVEN if pos == 0 else Role.MIDDLE)
else:
assert order is not None
role = _name_positions(order, count)[pos]
ambiguities[r] = copy_with(report,
alternative=copy_with(alt, role=role))
ambiguities[r] = copy_with(report, alternative=copy_with(
alt, role=_slot_field(state, seg_idx, pos, count, order)))


def assign(state: ParseState) -> ParseState:
Expand Down
7 changes: 3 additions & 4 deletions nameparser/_pipeline/_post_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

from nameparser._lexicon import _normalize, _run_addresses_by_given
from nameparser._pipeline._assign import (
_attach_particle_tail, _name_alt, _name_positions,
_attach_particle_tail, _name_alt, _name_pieces, _name_positions,
)
from nameparser._pipeline._pieces import (
is_lone_never_given_particle, particle_tail, settled_by_case,
Expand Down Expand Up @@ -455,9 +455,8 @@ def _unattached(state: ParseState, tokens: list[WorkToken],
# the run ends a name read family-first, so a name word stands in
# front of it
front, first = next(
((piece, k == 0) for seg in state.pieces
for k, piece in enumerate(
[p for p in seg if tokens[p[0]].role in NAME_ROLES])
((piece, k == 0) for seg_idx in range(len(state.pieces))
for k, piece in enumerate(_name_pieces(state, tokens, seg_idx))
if start - 1 in piece), (None, False))
return _name_alt(word, role, front, tokens, first, False)

Expand Down
38 changes: 22 additions & 16 deletions nameparser/_pipeline/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,27 @@ class Deferred(Enum):
#: join the name words on either side into one, whose field assign
#: names once it has positions -- the positional read's, where the
#: join would merge two pieces, and the letter's own piece's
#: (BESIDE), where both words are already in it.
#: (`Beside`), where both words are already in it.
JOINED = auto()
#: group's particle chain: the particle it chained would stand as a
#: name word of its own in front of the piece, named by assign.
UNCHAINED = auto()
#: lands where the token at ``PendingReading.beside`` lands, named
#: from that token's final role: a word the name piece in front
#: would take in (a particle chain, which runs to the end of the
#: name words, or a bound given name), or a connective whose join
#: stays inside its own piece. Where P6 then attached the front
#: token to the family (`FOLDED_TAG`), which it would not have
#: done with a word behind it, the field the word held when the
#: report was made stands instead, given as ``unfolded``.
BESIDE = auto()


@dataclass(frozen=True, slots=True)
class Beside:
"""A PendingReading field named from another token's: the reading
lands where the token at `index` lands, named by assemble from that
token's final role -- a word the name piece in front would take in
(a particle chain, which runs to the end of the name words, or a
bound given name), or a connective whose join stays inside its own
piece. Where P6 then attached that token to the family
(`FOLDED_TAG`), which it would not have done with a word behind
it, `unfolded`, the field the word held when the report was made,
stands instead."""

index: int
unfolded: Role | None = None


@dataclass(frozen=True, slots=True)
Expand All @@ -217,18 +224,17 @@ class PendingReading:

``role`` is the field the first referent would hold under that
reading, with every other word of the name read as the parse read
it, or a `Deferred` form for a stage that runs before the field is
known. A JOINED or UNCHAINED form assign leaves unresolved -- a
referent that ends outside every name position -- surfaces as no
it, a `Deferred` form for a stage that runs before the field is
known, or `Beside` for a field another token's final role names.
A JOINED or UNCHAINED form assign leaves unresolved -- a referent
that ends outside every name position -- surfaces as no
alternative rather than a guess. Assemble also folds a MIDDLE into
the family under ``middle_as_family``, the one later rule that
moves a field whatever word stands in it."""

kind: ReadingKind
source: ReadingSource
role: Role | Deferred
beside: int | None = None
unfolded: Role | None = None
role: Role | Deferred | Beside


#: How a report names the field a word was read into, by final role.
Expand Down
Loading