diff --git a/rebulk/chain.py b/rebulk/chain.py index e2f7d34..0151717 100644 --- a/rebulk/chain.py +++ b/rebulk/chain.py @@ -216,7 +216,10 @@ def _fix_matches_offset(chain_part_matches: Iterable[Match], input_string: str, @staticmethod def _group_by_match_index(matches: Iterable[Match]) -> dict[int, list[Match]]: grouped_matches_dict: dict[int, list[Match]] = {} - for match_index, match in itertools.groupby(matches, lambda m: m.match_index): + # groupby only groups consecutive equal keys, so sort by match_index first + # to avoid splitting (and overwriting) groups when matches are unordered. + sorted_matches = sorted(matches, key=lambda m: m.match_index) + for match_index, match in itertools.groupby(sorted_matches, lambda m: m.match_index): grouped_matches_dict[match_index] = list(match) return grouped_matches_dict diff --git a/rebulk/test/test_chain.py b/rebulk/test/test_chain.py index 8903e97..c3f9c9c 100644 --- a/rebulk/test/test_chain.py +++ b/rebulk/test/test_chain.py @@ -8,11 +8,12 @@ from rebulk.pattern import FunctionalPattern, RePattern, StringPattern from ..chain import Chain +from ..match import Match from ..rebulk import Rebulk from ..validators import chars_surround if TYPE_CHECKING: - from ..match import Match, Matches + from ..match import Matches def test_chain_close() -> None: @@ -489,3 +490,22 @@ def chain_breaker(matches: Matches) -> bool: matches[0].value = 1 matches[1].value = 2 matches[2].value = 3 + + +def test_group_by_match_index_sorts_unordered_input() -> None: + # groupby only groups consecutive equal keys, so _group_by_match_index must + # sort by match_index first; otherwise unordered matches split (and overwrite) + # groups sharing an index. + matches: list[Match] = [] + for start, match_index in ((0, 2), (5, 0), (10, 1), (15, 2)): + match = Match(start, start + 1, name="test") + match.match_index = match_index + matches.append(match) + + grouped = Chain._group_by_match_index(matches) + + assert sorted(grouped) == [0, 1, 2] + assert [m.start for m in grouped[0]] == [5] + assert [m.start for m in grouped[1]] == [10] + # both match_index=2 matches are kept, in their original relative order + assert [m.start for m in grouped[2]] == [0, 15]