Skip to content

Commit 1f0a6e0

Browse files
authored
Merge pull request #209 from derek73/fix/join-on-conjunctions-restructure
Fix repeated-prefix-chain data loss in join_on_conjunctions
2 parents 2951abf + 947b1aa commit 1f0a6e0

2 files changed

Lines changed: 56 additions & 67 deletions

File tree

nameparser/parser.py

Lines changed: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,18 +1251,14 @@ def join_on_conjunctions(self, pieces: list[str], additional_parts_count: int =
12511251

12521252
contiguous_conj_i = group_contiguous_integers(conj_index)
12531253

1254-
delete_i: list[int] = []
1255-
for cont_i in contiguous_conj_i:
1254+
# process ranges in reverse so deleting one range doesn't shift the
1255+
# indices of ranges still to be processed
1256+
for cont_i in reversed(contiguous_conj_i):
12561257
new_piece = " ".join(pieces[cont_i[0]: cont_i[1]+1])
1257-
delete_i += list(range(cont_i[0]+1, cont_i[1]+1))
1258-
pieces[cont_i[0]] = new_piece
1258+
pieces[cont_i[0]:cont_i[1]+1] = [new_piece]
12591259
# add newly joined conjunctions to constants to be found later
12601260
self.C.conjunctions.add(new_piece)
12611261

1262-
for i in reversed(delete_i):
1263-
# delete pieces in reverse order or the index changes on each delete
1264-
del pieces[i]
1265-
12661262
if len(pieces) == 1:
12671263
# if there's only one piece left, nothing left to do
12681264
return pieces
@@ -1295,70 +1291,36 @@ def shift_conj_index(past: int, by: int) -> None:
12951291
# http://code.google.com/p/python-nameparser/issues/detail?id=11
12961292
continue
12971293

1298-
if i == 0:
1299-
new_piece = " ".join(pieces[i:i+2])
1300-
register_joined_piece(new_piece, pieces[i+1])
1301-
pieces[i] = new_piece
1302-
pieces.pop(i+1)
1303-
shift_conj_index(past=i, by=1)
1304-
1305-
else:
1306-
new_piece = " ".join(pieces[i-1:i+2])
1307-
register_joined_piece(new_piece, pieces[i-1])
1308-
pieces[i-1] = new_piece
1309-
# len(pieces) - i is always >= 1 here: pieces[i-1:i+2] above
1310-
# already accessed index i, so i is guaranteed in range.
1311-
rm_count = min(2, len(pieces) - i)
1312-
assert rm_count > 0, f"unexpected empty deletion at i={i}, pieces={pieces}"
1313-
del pieces[i:i+rm_count]
1314-
shift_conj_index(past=i, by=rm_count)
1294+
start = max(0, i - 1)
1295+
end = min(len(pieces), i + 2)
1296+
new_piece = " ".join(pieces[start:end])
1297+
neighbor = pieces[start] if start < i else pieces[end - 1]
1298+
register_joined_piece(new_piece, neighbor)
1299+
pieces[start:end] = [new_piece]
1300+
shift_conj_index(past=i, by=end - start - 1)
13151301

13161302
# join prefixes to following lastnames: ['de la Vega'], ['van Buren']
1317-
prefixes = list(filter(self.is_prefix, pieces))
1318-
if prefixes:
1319-
for prefix in prefixes:
1320-
try:
1321-
i = pieces.index(prefix)
1322-
except ValueError:
1323-
# If the prefix is no longer in pieces, it's because it has been
1324-
# combined with the prefix that appears right before (or before that when
1325-
# chained together) in the last loop, so the index of that newly created
1326-
# piece is the same as in the last loop, i==i still, and we want to join
1327-
# it to the next piece.
1328-
pass
1303+
i = 0
1304+
while i < len(pieces):
1305+
# total_length >= 1 covers essentially all real input, so this
1306+
# treats any leading piece as a first name rather than a prefix.
1307+
leading_first_name = i == 0 and total_length >= 1
1308+
if not self.is_prefix(pieces[i]) or leading_first_name:
1309+
i += 1
1310+
continue
13291311

1330-
new_piece = ''
1312+
# absorb any immediately-adjacent prefixes into one contiguous run
1313+
# e.g. "von und zu der" ==> chain them all before looking further
1314+
j = i + 1
1315+
while j < len(pieces) and self.is_prefix(pieces[j]):
1316+
j += 1
13311317

1332-
# join everything after the prefix until the next prefix or suffix
1318+
# then join everything after the run until the next prefix or suffix
1319+
while j < len(pieces) and not self.is_prefix(pieces[j]) and not self.is_suffix(pieces[j]):
1320+
j += 1
13331321

1334-
try:
1335-
if i == 0 and total_length >= 1:
1336-
# If it's the first piece and there are more than 1 rootnames, assume it's a first name
1337-
continue
1338-
next_prefix = next(iter(filter(self.is_prefix, pieces[i + 1:])))
1339-
j = pieces.index(next_prefix, i + 1)
1340-
if j == i + 1:
1341-
# if there are two prefixes in sequence, join to the following piece
1342-
j += 1
1343-
new_piece = ' '.join(pieces[i:j])
1344-
pieces[i:j] = [new_piece]
1345-
except StopIteration:
1346-
try:
1347-
# if there are no more prefixes, look for a suffix to stop at
1348-
stop_at = next(iter(filter(self.is_suffix, pieces[i + 1:])))
1349-
# search from i + 1: filter() finds the value of stop_at
1350-
# in pieces[i+1:] but pieces.index() without a start
1351-
# argument searches from 0, so an earlier occurrence of
1352-
# the same token (e.g. a suffix token that also appears
1353-
# before the prefix) would be matched instead.
1354-
j = pieces.index(stop_at, i + 1)
1355-
new_piece = ' '.join(pieces[i:j])
1356-
pieces[i:j] = [new_piece]
1357-
except StopIteration:
1358-
# if there were no suffixes, nothing to stop at so join all
1359-
# remaining pieces
1360-
new_piece = ' '.join(pieces[i:])
1361-
pieces[i:] = [new_piece]
1322+
pieces[i:j] = [' '.join(pieces[i:j])]
1323+
i += 1
13621324

13631325
log.debug("pieces: %s", pieces)
13641326
return pieces

tests/test_prefixes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,33 @@ def test_non_first_name_prefix_with_custom_title(self) -> None:
307307
self.m(hn.first, "", hn)
308308
self.m(hn.last, "de Mesnil", hn)
309309

310+
def test_repeated_prefix_chain_de_la(self) -> None:
311+
hn = HumanName("Juan de la de la Vega")
312+
self.m(hn.first, "Juan", hn)
313+
self.m(hn.last, "de la de la Vega", hn)
314+
315+
def test_repeated_prefix_chain_van_der(self) -> None:
316+
hn = HumanName("Charles van der van der Berg")
317+
self.m(hn.first, "Charles", hn)
318+
self.m(hn.last, "van der van der Berg", hn)
319+
320+
def test_triple_repeated_prefix_chain(self) -> None:
321+
# a stronger regression guard than the 2-repeat cases above: the
322+
# contiguous-prefix absorption loop should chain any number of
323+
# repeats, not just handle exactly two
324+
hn = HumanName("Juan de la de la de la Vega")
325+
self.m(hn.first, "Juan", hn)
326+
self.m(hn.last, "de la de la de la Vega", hn)
327+
328+
def test_repeated_prefix_chain_followed_by_suffix(self) -> None:
329+
# the prefix-run absorption loop and the suffix-boundary loop share
330+
# the same index variable, so a repeated chain immediately
331+
# followed by a suffix is worth pinning down explicitly
332+
hn = HumanName("Juan de la de la Vega Jr.")
333+
self.m(hn.first, "Juan", hn)
334+
self.m(hn.last, "de la de la Vega", hn)
335+
self.m(hn.suffix, "Jr.", hn)
336+
310337
# --- safety: excluded / ambiguous particles are unchanged ---
311338

312339
def test_leading_von_is_unchanged(self) -> None:

0 commit comments

Comments
 (0)