Commit4c320bfcRecorded17 Mar 2026Repositorysigil-caldav
Suppress RRULE occurrences replaced by RECURRENCE-ID exceptions
Message
When a recurring event has modified instances (VEVENT entries with RECURRENCE-ID), the original RRULE-generated occurrence for that date/time should be suppressed. Collect all RECURRENCE-ID timestamps and filter them from expanded results so only the exception instance appears, matching how calendar UIs like Fastmail handle this.
Changed
src/sigil/caldav/event.sgl | 50 ++++++++++++++++++++++++++++++++------------------
1 file changed, 32 insertions(+), 18 deletions(-)Diff
src/sigil/caldav/event.sglmodified
@@ -91,25 +91,39 @@
91
(loop (cdr remaining) events)))))) 92
93
;; Expand recurrence for all events that have RRULE.−94
;; Non-recurring events (including exception instances with RECURRENCE-ID)−95
;; are filtered by the date range to avoid returning out-of-range entries.+94
;; Exception instances (RECURRENCE-ID) replace the RRULE-generated+95
;; occurrence for that date — the original is suppressed.+96
;; Non-recurring events are filtered by the date range. 97
(define (expand-all-events events start end)−97
(let loop ((remaining events) (result '()))−98
(if (null? remaining)−99
(reverse result)−100
(let ((ev (car remaining)))−101
(if (ical-event-rrule ev)−102
(loop (cdr remaining)−103
(append (reverse (ical-expand-recurrence ev−104
start: start end: end))−105
result))−106
;; Non-recurring: include only if within range−107
(let ((ev-start (ical-event-dtstart ev)))−108
(if (and ev-start−109
(or (not start) (>= ev-start start))−110
(or (not end) (< ev-start end)))−111
(loop (cdr remaining) (cons ev result))−112
(loop (cdr remaining) result))))))))+98
;; Collect RECURRENCE-ID timestamps — these suppress RRULE occurrences+99
(let ((exception-times+100
(filter-map (lambda (ev)+101
(ical-event-recurrence-id ev))+102
events)))+103
(let loop ((remaining events) (result '()))+104
(if (null? remaining)+105
(reverse result)+106
(let ((ev (car remaining)))+107
(if (ical-event-rrule ev)+108
;; Expand, then remove occurrences replaced by exceptions+109
(let ((expanded (ical-expand-recurrence ev+110
start: start end: end)))+111
(let ((filtered+112
(if (null? exception-times)+113
expanded+114
(filter (lambda (occ)+115
(not (member (ical-event-dtstart occ)+116
exception-times)))+117
expanded))))+118
(loop (cdr remaining)+119
(append (reverse filtered) result))))+120
;; Non-recurring: include only if within range+121
(let ((ev-start (ical-event-dtstart ev)))+122
(if (and ev-start+123
(or (not start) (>= ev-start start))+124
(or (not end) (< ev-start end)))+125
(loop (cdr remaining) (cons ev result))+126
(loop (cdr remaining) result))))))))) 127
128
129
;; ============================================================