Skip to content

Commit e83b17f

Browse files
fix(transform): prefer latest overlapping subwatcher event
1 parent 5823d15 commit e83b17f

2 files changed

Lines changed: 46 additions & 7 deletions

File tree

aw_transform/merge_subwatcher_fields.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ def merge_subwatcher_fields(
6161
Note on overlap:
6262
Base events are split at the clipped subwatcher boundaries so each
6363
output segment only carries the subwatcher fields that were actually
64-
present during that slice of time.
64+
present during that slice of time. If multiple subwatcher events cover
65+
the same slice, the most recent one wins so a later transition does not
66+
get smeared backward by an older, longer pulse.
6567
"""
6668
if conflict not in ("base_wins", "sub_wins"):
6769
raise ValueError(
@@ -102,17 +104,25 @@ def merge_subwatcher_fields(
102104
for start, end in zip(boundary_points, boundary_points[1:]):
103105
segment_period = Timeslot(start, end)
104106
best_sub: Optional[Event] = None
105-
best_overlap_secs: float = 0.0
107+
best_sub_period: Optional[Timeslot] = None
106108

107109
for sub, sub_period in overlapping:
108-
ip = segment_period.intersection(sub_period)
109-
if not ip:
110+
if not segment_period.intersection(sub_period):
110111
continue
111112

112-
overlap_secs = ip.duration.total_seconds()
113-
if overlap_secs > best_overlap_secs:
114-
best_overlap_secs = overlap_secs
113+
# Later subwatcher events should supersede older overlapping
114+
# pulses on the shared slice instead of letting stale data linger.
115+
if (
116+
best_sub is None
117+
or sub.timestamp > best_sub.timestamp
118+
or (
119+
sub.timestamp == best_sub.timestamp
120+
and best_sub_period is not None
121+
and sub_period.end > best_sub_period.end
122+
)
123+
):
115124
best_sub = sub
125+
best_sub_period = sub_period
116126

117127
enriched = deepcopy(base)
118128
enriched.timestamp = start

tests/test_transforms.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,35 @@ def test_merge_subwatcher_fields_partial_overlap_splits_base():
537537
assert sum_durations(result) == td1h
538538

539539

540+
def test_merge_subwatcher_fields_overlapping_subwatchers_prefer_latest_start():
541+
"""Newer overlapping subwatcher events take over from their own start time."""
542+
now = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
543+
td20m = timedelta(minutes=20)
544+
td40m = timedelta(minutes=40)
545+
td1h = timedelta(hours=1)
546+
547+
base = [Event(timestamp=now, duration=td1h, data={"app": "vim"})]
548+
sub = [
549+
Event(timestamp=now, duration=td40m, data={"project": "alpha"}),
550+
Event(timestamp=now + td20m, duration=td40m, data={"project": "beta"}),
551+
]
552+
553+
result = merge_subwatcher_fields(base, sub, ["project"])
554+
555+
assert len(result) == 2
556+
assert [event.timestamp for event in result] == [now, now + td20m]
557+
assert [event.duration for event in result] == [td20m, td40m]
558+
assert [event.data.get("project") for event in result] == ["alpha", "beta"]
559+
560+
by_project = {
561+
event.data.get("project"): event.duration
562+
for event in merge_events_by_keys(result, ["project"])
563+
}
564+
assert by_project["alpha"] == td20m
565+
assert by_project["beta"] == td40m
566+
assert sum_durations(result) == td1h
567+
568+
540569
def test_merge_subwatcher_fields_no_overlap():
541570
"""Base events with no overlapping subwatcher event are returned unchanged."""
542571
now = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)

0 commit comments

Comments
 (0)