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`
This commit is contained in:
@@ -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'
|
snippet: 'REM Mon AT 09:00 FROM 2025-09-01 UNTIL 2025-10-31 MSG Standup\nOMIT 2025-10-13'
|
||||||
priorita: Subito
|
priorita: Subito
|
||||||
|
|
||||||
- id: P11
|
- id: P11 ✅
|
||||||
pattern: Override/cancellazioni per istanza
|
pattern: Override/cancellazioni per istanza
|
||||||
ics: "RECURRENCE-ID con contenuto modificato o STATUS:CANCELLED"
|
ics: "RECURRENCE-ID con contenuto modificato o STATUS:CANCELLED"
|
||||||
remind_support: espansione
|
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
|
| Some (_, recurs) -> debug_print_of_recurrence_and_skip ev recurs
|
||||||
| None -> Ok rem
|
| None -> Ok rem
|
||||||
|
|
||||||
let warn_unhandled_recurring rem ev : (Remind.rem, error) result =
|
let is_cancelled (ev : Icalendar.event) : bool =
|
||||||
if List.length rem.Remind.recurring > 0 then
|
List.exists
|
||||||
Printf.eprintf "Warning: RECURRENCE-ID overrides present but not handled (master emitted as-is)\t\t\tUID: %s\n"
|
(function
|
||||||
(Utils.get_uid ev);
|
| `Status (_, `Cancelled) -> true
|
||||||
Ok rem
|
| _ -> 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 =
|
let all_collectors : collector list =
|
||||||
[
|
[
|
||||||
@@ -338,9 +380,9 @@ let all_collectors : collector list =
|
|||||||
collect_summary;
|
collect_summary;
|
||||||
collect_start_end_duration;
|
collect_start_end_duration;
|
||||||
collect_exdates;
|
collect_exdates;
|
||||||
|
collect_overrides;
|
||||||
yearly_simple_date;
|
yearly_simple_date;
|
||||||
simple_recurrence;
|
simple_recurrence;
|
||||||
warn_unhandled_recurring;
|
|
||||||
]
|
]
|
||||||
|
|
||||||
let remind_of_event (source : string) (ev : Icalendar.event list) : (Remind.rem, error) result =
|
let remind_of_event (source : string) (ev : Icalendar.event list) : (Remind.rem, error) result =
|
||||||
|
|||||||
@@ -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
|
(** 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 *)
|
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 *)
|
(** A complete REM command *)
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ let empty =
|
|||||||
daily = None;
|
daily = None;
|
||||||
recurring = [];
|
recurring = [];
|
||||||
exdate = [];
|
exdate = [];
|
||||||
|
overrides = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
(* ── buffer primitives ────────────────────────────────────────── *)
|
(* ── buffer primitives ────────────────────────────────────────── *)
|
||||||
@@ -221,12 +223,16 @@ let render_yearly rem month day =
|
|||||||
(* ── dispatcher ───────────────────────────────────────────────── *)
|
(* ── dispatcher ───────────────────────────────────────────────── *)
|
||||||
|
|
||||||
let string_of_rem rem =
|
let string_of_rem rem =
|
||||||
match rem.daily with
|
let main =
|
||||||
| Some d -> render_daily rem d
|
match rem.daily with
|
||||||
| None -> (
|
| Some d -> render_daily rem d
|
||||||
match rem.weekly with
|
| None -> (
|
||||||
| Some w -> render_weekly rem w
|
match rem.weekly with
|
||||||
| None -> (
|
| Some w -> render_weekly rem w
|
||||||
match rem.yearly with
|
| None -> (
|
||||||
| Some (month, day) -> render_yearly rem month day
|
match rem.yearly with
|
||||||
| None -> render_single rem))
|
| 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)
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ let separate_master_and_recurrence (events : Icalendar.event list) : Icalendar.e
|
|||||||
List.partition_map
|
List.partition_map
|
||||||
(fun (ev, recur_id_opt) ->
|
(fun (ev, recur_id_opt) ->
|
||||||
match recur_id_opt with
|
match recur_id_opt with
|
||||||
| None -> Right ev
|
| None -> Left ev (* no RECURRENCE-ID → master *)
|
||||||
| Some _ -> Left ev)
|
| Some _ -> Right ev (* has RECURRENCE-ID → override *))
|
||||||
recur_ids
|
recur_ids
|
||||||
in
|
in
|
||||||
match master_and_recurrences with
|
match master_and_recurrences with
|
||||||
|
|||||||
Reference in New Issue
Block a user