From c246ec67758aa1761bb22689253a3dce4548aba4 Mon Sep 17 00:00:00 2001 From: Paolo Donadeo Date: Sun, 17 May 2026 20:04:18 +0200 Subject: [PATCH] feat(recurring): implement RECURRENCE-ID override handling - Add `overrides` field to `rem` type to hold single-event REMs from non-cancelled overrides - Add `is_cancelled`, `build_override_rem`, and `collect_overrides` to process RECURRENCE-ID override events - Replace `warn_unhandled_recurring` with `collect_overrides` in the collector pipeline - Fix `separate_master_and_recurrence` partition logic (swapped `Left`/`Right`) - Render override REMs appended to the master REM in `string_of_rem` --- bin/eventPredicates.ml | 56 ++++++++++++++++++++++++++++++++++++------ bin/remind.ml | 24 +++++++++++------- bin/utils.ml | 4 +-- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/bin/eventPredicates.ml b/bin/eventPredicates.ml index 41d336e..889f550 100644 --- a/bin/eventPredicates.ml +++ b/bin/eventPredicates.ml @@ -90,7 +90,7 @@ open Utils snippet: 'REM Mon AT 09:00 FROM 2025-09-01 UNTIL 2025-10-31 MSG Standup\nOMIT 2025-10-13' priorita: Subito - - id: P11 + - id: P11 ✅ pattern: Override/cancellazioni per istanza ics: "RECURRENCE-ID con contenuto modificato o STATUS:CANCELLED" remind_support: espansione @@ -326,11 +326,53 @@ RRULE: (`Weekly, (Some `Until (`Utc (2026-07-01 09:00:00 +00:00))), None, []) | Some (_, recurs) -> debug_print_of_recurrence_and_skip ev recurs | None -> Ok rem -let warn_unhandled_recurring rem ev : (Remind.rem, error) result = - if List.length rem.Remind.recurring > 0 then - Printf.eprintf "Warning: RECURRENCE-ID overrides present but not handled (master emitted as-is)\t\t\tUID: %s\n" - (Utils.get_uid ev); - Ok rem +let is_cancelled (ev : Icalendar.event) : bool = + List.exists + (function + | `Status (_, `Cancelled) -> true + | _ -> false) + ev.props + +let build_override_rem (source : string) (override_ev : Icalendar.event) : (Remind.rem, error) result = + let rem = { Remind.empty with Remind.source } in + let collectors = [ collect_uuid; collect_summary; collect_start_end_duration ] in + ListLabels.fold_left ~init:(Ok rem) collectors ~f:(fun rem_or_error pred -> + match rem_or_error with + | Error e -> Error e + | Ok rem -> pred rem override_ev) + +let collect_overrides rem _ev : (Remind.rem, error) result = + (* Process each RECURRENCE-ID override event stored in rem.recurring: + - add its RECURRENCE-ID date to rem.exdate (feeds the OMIT mechanism) + - for non-cancelled overrides, build a single REM and add to rem.overrides *) + let new_exdates, new_overrides = + ListLabels.fold_left ~init:([], []) rem.Remind.recurring ~f:(fun (exdates, overrides) override_ev -> + let recur_id_opt = Utils.get_recurrence_id override_ev in + let exdates = + match recur_id_opt with + | None -> + Printf.eprintf "Warning: override event has no RECURRENCE-ID\t\t\tUID: %s\n" (Utils.get_uid override_ev); + exdates + | Some date_or_dt -> date_or_dt :: exdates + in + let overrides = + if is_cancelled override_ev then overrides + else + match build_override_rem rem.Remind.source override_ev with + | Error _ -> + Printf.eprintf "Warning: could not build override REM\t\t\tUID: %s\n" (Utils.get_uid override_ev); + overrides + | Ok override_rem -> override_rem :: overrides + in + (exdates, overrides)) + in + Ok + { + rem with + Remind.exdate = rem.Remind.exdate @ List.rev new_exdates; + Remind.overrides = List.rev new_overrides; + Remind.recurring = []; + } let all_collectors : collector list = [ @@ -338,9 +380,9 @@ let all_collectors : collector list = collect_summary; collect_start_end_duration; collect_exdates; + collect_overrides; yearly_simple_date; simple_recurrence; - warn_unhandled_recurring; ] let remind_of_event (source : string) (ev : Icalendar.event list) : (Remind.rem, error) result = diff --git a/bin/remind.ml b/bin/remind.ml index 8b78a9e..9b9a6ac 100644 --- a/bin/remind.ml +++ b/bin/remind.ml @@ -32,6 +32,7 @@ type rem = { (** List of events that are part of the same recurring series: these are only the overrides, not the master event *) exdate : Icalendar.date_or_datetime list; (** List of excluded dates for recurring events *) + overrides : rem list; (** Single-event REMs generated from non-cancelled RECURRENCE-ID overrides *) } (** A complete REM command *) @@ -49,6 +50,7 @@ let empty = daily = None; recurring = []; exdate = []; + overrides = []; } (* ── buffer primitives ────────────────────────────────────────── *) @@ -221,12 +223,16 @@ let render_yearly rem month day = (* ── dispatcher ───────────────────────────────────────────────── *) let string_of_rem rem = - match rem.daily with - | Some d -> render_daily rem d - | None -> ( - match rem.weekly with - | Some w -> render_weekly rem w - | None -> ( - match rem.yearly with - | Some (month, day) -> render_yearly rem month day - | None -> render_single rem)) + let main = + match rem.daily with + | Some d -> render_daily rem d + | None -> ( + match rem.weekly with + | Some w -> render_weekly rem w + | None -> ( + match rem.yearly with + | Some (month, day) -> render_yearly rem month day + | None -> render_single rem)) + in + let overrides = List.map render_single rem.overrides in + String.concat "" (main :: overrides) diff --git a/bin/utils.ml b/bin/utils.ml index f89cbd9..9af9901 100644 --- a/bin/utils.ml +++ b/bin/utils.ml @@ -176,8 +176,8 @@ let separate_master_and_recurrence (events : Icalendar.event list) : Icalendar.e List.partition_map (fun (ev, recur_id_opt) -> match recur_id_opt with - | None -> Right ev - | Some _ -> Left ev) + | None -> Left ev (* no RECURRENCE-ID → master *) + | Some _ -> Right ev (* has RECURRENCE-ID → override *)) recur_ids in match master_and_recurrences with