Skip to content

Commit 7b24386

Browse files
authored
fix(transform): don't double-count overlapping filter events in filter_period_intersect (#166)
* fix(transform): don't double-count overlapping filter events in filter_period_intersect When filter events overlap, an event spanning both was intersected with each of them in full, so the shared time was returned twice: an event [0, 10] filtered by [0, 6] and [4, 8] gave [0, 6] and [4, 8]. Only intersect the part of the event that has not been returned yet, giving [0, 6] and [6, 8]. Results for non-overlapping filter events, the common case (not-afk events), are unchanged. Found in review of ActivityWatch/aw-server-rust#749, which ports this function to Rust. * test(transform): pin filter_period_intersect with overlapping filters sharing an end
1 parent 3501963 commit 7b24386

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

‎aw_transform/filter_period_intersect.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ def _intersecting_eventpairs(
2929
events2.sort(key=lambda e: e.timestamp)
3030
e1_i = 0
3131
e2_i = 0
32+
# Start of the part of events1[e1_i] that has not been yielded yet, so that
33+
# overlapping events in events2 don't yield the same time twice.
34+
yielded_until = None
3235
while e1_i < len(events1) and e2_i < len(events2):
3336
e1 = events1[e1_i]
3437
e2 = events2[e2_i]
3538
e1_p = _get_event_period(e1)
39+
if yielded_until is not None and yielded_until > e1_p.start:
40+
e1_p = Timeslot(yielded_until, e1_p.end)
3641
e2_p = _get_event_period(e2)
3742

3843
ip = e1_p.intersection(e2_p)
@@ -41,13 +46,16 @@ def _intersecting_eventpairs(
4146
yield (e1, e2, ip)
4247
if e1_p.end <= e2_p.end:
4348
e1_i += 1
49+
yielded_until = None
4450
else:
4551
e2_i += 1
52+
yielded_until = ip.end
4653
else:
4754
# No intersection, check if event is before/after filterevent
4855
if e1_p.end <= e2_p.start:
4956
# Event ended before filter event started
5057
e1_i += 1
58+
yielded_until = None
5159
elif e2_p.end <= e1_p.start:
5260
# Event started after filter event ended
5361
e2_i += 1

‎tests/test_transforms.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,47 @@ def test_filter_period_intersect():
165165
assert filtered_events[1].duration == timedelta(minutes=15)
166166

167167

168+
def test_filter_period_intersect_overlapping_filters():
169+
"""Time covered by several overlapping filter events is only kept once."""
170+
now = datetime(2026, 1, 1, 10, tzinfo=timezone.utc)
171+
td1s = timedelta(seconds=1)
172+
to_filter = [Event(timestamp=now, duration=10 * td1s)]
173+
174+
filter_with = [
175+
Event(timestamp=now, duration=6 * td1s),
176+
Event(timestamp=now + 4 * td1s, duration=4 * td1s),
177+
]
178+
filtered_events = filter_period_intersect(to_filter, filter_with)
179+
assert [(e.timestamp - now, e.duration) for e in filtered_events] == [
180+
(0 * td1s, 6 * td1s),
181+
(6 * td1s, 2 * td1s),
182+
]
183+
184+
# A filter event contained in an earlier one adds nothing, also when they
185+
# end at the same time
186+
for start, duration in [(2, 2), (2, 4)]:
187+
filter_with = [
188+
Event(timestamp=now, duration=6 * td1s),
189+
Event(timestamp=now + start * td1s, duration=duration * td1s),
190+
]
191+
filtered_events = filter_period_intersect(to_filter, filter_with)
192+
assert [(e.timestamp - now, e.duration) for e in filtered_events] == [
193+
(0 * td1s, 6 * td1s),
194+
]
195+
196+
197+
def test_filter_period_intersect_zero_duration():
198+
"""Zero-duration events within a filter event are kept, boundaries included."""
199+
now = datetime(2026, 1, 1, 10, tzinfo=timezone.utc)
200+
td1s = timedelta(seconds=1)
201+
filter_with = [Event(timestamp=now, duration=10 * td1s)]
202+
for offset, kept in [(-1, False), (0, True), (5, True), (10, True), (11, False)]:
203+
to_filter = [Event(timestamp=now + offset * td1s, duration=0)]
204+
assert filter_period_intersect(to_filter, filter_with) == (
205+
to_filter if kept else []
206+
)
207+
208+
168209
def test_period_union():
169210
now = datetime.now(timezone.utc)
170211

0 commit comments

Comments
 (0)