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
;; ============================================================