Skip to content

Commit d0b3f65

Browse files
committed
Exclude core from being collected as a stray occurrence
1 parent 02e4b04 commit d0b3f65

4 files changed

Lines changed: 78 additions & 3 deletions

File tree

recurring_ical_events/adapters/component.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ def is_modification(self) -> bool:
146146
"""Whether the adapter is a modification."""
147147
return bool(self.recurrence_ids)
148148

149+
def is_core(self):
150+
"""Whether this has generation rules present."""
151+
return (
152+
"RRULE" in self._component
153+
or "RDATE" in self._component
154+
or "EXDATE" in self._component
155+
)
156+
149157
@cached_property
150158
def sequence(self) -> int:
151159
"""The sequence in the history of modification.

recurring_ical_events/series/rrule.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def __init__(self, components: Sequence[ComponentAdapter]):
322322
self.recurrence_id_to_modification: dict[
323323
RecurrenceID, ComponentAdapter
324324
] = {} # RECURRENCE-ID -> adapter
325-
self.this_and_future = []
325+
self.this_and_future = [] # sorted, earliest first
326326
self._uid = components[0].uid
327327
core: ComponentAdapter | None = None
328328
for component in components:
@@ -351,7 +351,16 @@ def __init__(self, components: Sequence[ComponentAdapter]):
351351
self.compute_span_extension()
352352

353353
def compute_span_extension(self):
354-
"""Compute how much to extend the span for the rrule to cover all events."""
354+
"""Compute how much to extend the span for the rrule to cover all events.
355+
356+
THISANDFUTURE can cause events to move.
357+
This takes care of it by extending the query to make sure we capture such
358+
occurrences.
359+
360+
This can be quite inefficient if events move a lot.
361+
So, this algorithm should be sped up e.g. by remembering
362+
how much these events moved.
363+
"""
355364
self._subtract_from_start, self._add_to_stop = (
356365
self.recurrence.extend_query_span_by
357366
)
@@ -453,11 +462,19 @@ def between(self, span_start: Time, span_stop: Time) -> Generator[Occurrence]:
453462
if occurrence.is_in_span(span_start, span_stop):
454463
yield occurrence
455464
for modification in self.modifications:
456-
# we assume that the modifications are actually included
465+
# we assume that modifications are actually included
466+
# even if they are not mentioned by the core.
467+
# However, we exclude some:
468+
# - if they were returned already
469+
# - if an EXDATE excludes them
470+
# - if they are a core
471+
# because they are expected to be included in the RRULE or RDATE
472+
# See https://github.com/niccokunzmann/python-recurring-ical-events/issues/253
457473
if (
458474
modification in returned_modifications
459475
or self.recurrence.check_exdates_datetime
460476
& set(modification.recurrence_ids)
477+
or modification.is_core()
461478
):
462479
continue
463480
if modification.is_in_span(span_start, span_stop):
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
BEGIN:VCALENDAR
2+
BEGIN:VEVENT
3+
DTSTAMP:20240707T214014Z
4+
DTSTART;VALUE=DATE:20240729
5+
DTEND;VALUE=DATE:20240804
6+
SUMMARY:test123
7+
CATEGORIES:other
8+
UID:111
9+
ORGANIZER:aaa
10+
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
11+
RECURRENCE-ID;VALUE=DATE:20240729
12+
CREATED:20240311T051101Z
13+
LAST-MODIFIED:20240311T051101Z
14+
SEQUENCE:1
15+
END:VEVENT
16+
17+
BEGIN:VEVENT
18+
DTSTAMP:20240707T214014Z
19+
DTSTART;VALUE=DATE:20240701
20+
DTEND;VALUE=DATE:20240708
21+
SUMMARY:test123
22+
CATEGORIES:other
23+
UID:111
24+
ORGANIZER:aaa
25+
RRULE:FREQ=WEEKLY;UNTIL=20240720;INTERVAL=2;BYDAY=MO
26+
CREATED:20240311T051101Z
27+
LAST-MODIFIED:20240701T063743Z
28+
SEQUENCE:2
29+
END:VEVENT
30+
END:VCALENDAR
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""A new sequence should override an older one."""
2+
3+
from datetime import date
4+
5+
6+
def test_recurrence_id_is_not_identical_to_dtstart(calendars):
7+
"""We have exactly two events in sequence 2.
8+
9+
start 2024-07-01 duration 7 days, 0:00:00
10+
start 2024-07-15 duration 7 days, 0:00:00
11+
"""
12+
events = list(calendars.issue_253_additional_recurrence_id.all())
13+
for event in events:
14+
start = event["DTSTART"].dt
15+
duration = event["DTEND"].dt - event["DTSTART"].dt
16+
print(f"start {start} duration {duration}")
17+
18+
assert len(events) == 2
19+
assert events[0].start == date(2024, 7, 1)
20+
assert events[1].start == date(2024, 7, 15)

0 commit comments

Comments
 (0)