feat: implement simple weekly recurrence rendering
- Add `simple_weekly` type and `weekly` field to `rem` record
- Add `exdate` field to `rem` for excluded dates
- Add `collect_exdates` collector to pipeline
- implement weekly `RRULE` handling with `BYDAY`, `INTERVAL`,
`COUNT`/`UNTIL`
- Add `render_weekly` to emit one `REM` per weekday with `UNTIL`/`*N`
- Replace `timedesc_of_date_or_datetime` with
`timedesc_of_utc_or_timestamp_local` in utils
- Refactor `get_exdates`/`get_rdates` to separate dates, datetimes and
periods; add debug logging per UID
- Wrap reminder output in try/catch in main; drop trailing newline
duplication
- Mark implemented predicates (P00–P05, P09, P12, P14) with ✅;
remove P18–P20 (ignored/deferred)
This commit is contained in:
@@ -3,7 +3,7 @@ open Icalendar
|
||||
open Utils
|
||||
|
||||
(* CASE ANALYSIS PREDICATES
|
||||
- id: P00
|
||||
- id: P00 ✅
|
||||
pattern: Ha un SUMMARY?
|
||||
ics: "SUMMARY:…"
|
||||
remind_support: nativo
|
||||
@@ -11,7 +11,7 @@ open Utils
|
||||
snippet: 'REM 2025-12-25 MSG Natale'
|
||||
priorita: Subito
|
||||
|
||||
- id: P01
|
||||
- id: P01 ✅
|
||||
pattern: All-day singolo
|
||||
ics: "DTSTART;VALUE=DATE, opzionale DTEND=giorno+1"
|
||||
remind_support: nativo
|
||||
@@ -19,7 +19,7 @@ open Utils
|
||||
snippet: 'REM 2025-12-25 MSG Natale'
|
||||
priorita: Subito
|
||||
|
||||
- id: P02
|
||||
- id: P02 ✅
|
||||
pattern: All-day multi-giorno
|
||||
ics: "DTSTART;VALUE=DATE + DTEND;VALUE=DATE esclusivo"
|
||||
remind_support: nativo+accorgimenti
|
||||
@@ -27,7 +27,7 @@ open Utils
|
||||
snippet: 'REM 2025-08-10 THROUGH 2025-08-15 MSG Ferie # oppure espansione per-giorno'
|
||||
priorita: Subito
|
||||
|
||||
- id: P03
|
||||
- id: P03 ✅
|
||||
pattern: Evento a orario locale
|
||||
ics: "DTSTART;TZID=… + DTEND oppure DURATION"
|
||||
remind_support: nativo
|
||||
@@ -35,7 +35,7 @@ open Utils
|
||||
snippet: 'REM 2025-10-05 AT 09:00 DURATION 1:00 MSG Riunione'
|
||||
priorita: Subito
|
||||
|
||||
- id: P04
|
||||
- id: P04 ✅
|
||||
pattern: Evento a orario in UTC
|
||||
ics: "DTSTART/DTEND con suffisso Z"
|
||||
remind_support: nativo+accorgimenti
|
||||
@@ -43,7 +43,7 @@ open Utils
|
||||
snippet: 'REM 2025-09-03 AT 06:45 MSG Treno'
|
||||
priorita: Subito
|
||||
|
||||
- id: P05
|
||||
- id: P05 ✅
|
||||
pattern: Ricorrenza settimanale semplice
|
||||
ics: "RRULE:FREQ=WEEKLY;BYDAY=…;[UNTIL|COUNT]"
|
||||
remind_support: nativo
|
||||
@@ -75,7 +75,7 @@ open Utils
|
||||
snippet: '# genera REM per ciascuna data calcolata'
|
||||
priorita: Dopo
|
||||
|
||||
- id: P09
|
||||
- id: P09 ✅
|
||||
pattern: Ricorrenza annuale semplice
|
||||
ics: "RRULE:FREQ=YEARLY;[BYMONTH][BYMONTHDAY]"
|
||||
remind_support: nativo
|
||||
@@ -99,7 +99,7 @@ open Utils
|
||||
snippet: '# serie + REM specifico per l’istanza'
|
||||
priorita: Subito
|
||||
|
||||
- id: P12
|
||||
- id: P12 ✅
|
||||
pattern: DURATION al posto di DTEND
|
||||
ics: "DURATION:PT…"
|
||||
remind_support: nativo
|
||||
@@ -115,7 +115,7 @@ open Utils
|
||||
snippet: 'REM 2025-10-05 AT 09:00 WARN 15 MSG Riunione'
|
||||
priorita: Dopo
|
||||
|
||||
- id: P14
|
||||
- id: P14 ✅
|
||||
pattern: Fusi orari dichiarati (VTIMEZONE, TZID diversi)
|
||||
ics: "VTIMEZONE + DTSTART;TZID=…"
|
||||
remind_support: nativo+accorgimenti
|
||||
@@ -146,41 +146,8 @@ open Utils
|
||||
strategia: "estrai solo URL di join nel MSG"
|
||||
snippet: '# riduci al link'
|
||||
priorita: Quando serve
|
||||
|
||||
- id: P18
|
||||
pattern: Visibilità/trasparenza
|
||||
ics: "CLASS, TRANSP"
|
||||
remind_support: non previsto
|
||||
strategia: "ignora o aggiungi prefisso [FREE]/[BUSY] nel MSG"
|
||||
snippet: '# opzionale'
|
||||
priorita: Ignora
|
||||
|
||||
- id: P19
|
||||
pattern: Stato/versioning
|
||||
ics: "STATUS, SEQUENCE, CREATED, LAST-MODIFIED"
|
||||
remind_support: non previsto
|
||||
strategia: "ignora; usa solo STATUS:CANCELLED per soppressioni"
|
||||
snippet: '# già coperto in P11'
|
||||
priorita: Ignora
|
||||
|
||||
- id: P20
|
||||
pattern: Categorie/etichette
|
||||
ics: "CATEGORIES:…"
|
||||
remind_support: parziale
|
||||
strategia: "prefisso nel MSG o uso TAG se ti serve filtrare"
|
||||
snippet: 'REM 2025-10-05 AT 09:00 TAG Work MSG [Work] Riunione'
|
||||
priorita: Dopo
|
||||
*)
|
||||
|
||||
type event_description =
|
||||
[ `Collect_uuid
|
||||
| `Has_summary
|
||||
| `All_day_event
|
||||
| `Expand_recurrence
|
||||
| `Yearly_simple_date
|
||||
| `Simple_weekly_recurrence ]
|
||||
[@@deriving show]
|
||||
|
||||
type error = Invalid_date of string | Skip [@@deriving show]
|
||||
|
||||
let invalid_date s e =
|
||||
@@ -215,17 +182,22 @@ let collect_start_end_duration rem ev : (Remind.rem, error) result =
|
||||
| Ok day_start ->
|
||||
begin match ev.dtend_or_duration with
|
||||
| None -> { rem with Remind.date = day_start } |> Result.ok
|
||||
| Some (`Dtend (_, `Datetime _)) -> skip
|
||||
| Some (`Dtend (_, `Datetime _)) ->
|
||||
skip (* Start is a date, end is a datetime: invalid case for all-day event *)
|
||||
| Some (`Dtend (_, `Date (year, month, day))) ->
|
||||
begin match Timedesc.Date.Ymd.make ~year ~month ~day with
|
||||
| Error e -> invalid_date "DTEND" e
|
||||
| Ok day_end ->
|
||||
let day_end = Timedesc.Date.add ~days:(-1) day_end in
|
||||
let day_end =
|
||||
if Timedesc.Date.lt day_start day_end then Timedesc.Date.add ~days:(-1) day_end else day_end
|
||||
in
|
||||
if Timedesc.Date.diff_days day_end day_start = 0 then
|
||||
Ok { rem with Remind.date = day_start; Remind.end_date = None }
|
||||
else Ok { rem with Remind.date = day_start; Remind.end_date = Some day_end }
|
||||
end
|
||||
| Some (`Duration (_, _duration)) -> skip
|
||||
| Some (`Duration (_, _duration)) ->
|
||||
(* Start is a date, duration is not supported: invalid case for all-day event *)
|
||||
skip
|
||||
end)
|
||||
| `Datetime datetime -> begin
|
||||
let start_td = Utils.timedesc_of_timestamp datetime in
|
||||
@@ -243,7 +215,9 @@ let collect_start_end_duration rem ev : (Remind.rem, error) result =
|
||||
let rem = { rem with Remind.duration = Some duration } in
|
||||
Ok rem
|
||||
end
|
||||
| `Date (_year, _month, _day) -> skip
|
||||
| `Date (_year, _month, _day) ->
|
||||
(* Start is a datetime, end is a date: invalid case for timed event *)
|
||||
skip
|
||||
end
|
||||
| Some (`Duration (_, duration)) ->
|
||||
let span = Timedesc.Utils.span_of_ptime_span duration in
|
||||
@@ -251,18 +225,29 @@ let collect_start_end_duration rem ev : (Remind.rem, error) result =
|
||||
Ok rem
|
||||
end
|
||||
|
||||
let expand_recurrence rem ev : (Remind.rem, error) result =
|
||||
let collect_exdates rem ev : (Remind.rem, error) result =
|
||||
let exdates = Utils.get_exdates ev in
|
||||
Ok { rem with Remind.exdate = exdates }
|
||||
|
||||
let expand_recurrence rem _ev : (Remind.rem, error) result =
|
||||
if List.length rem.Remind.recurring > 0 then skip else Ok rem
|
||||
|
||||
let yearly_simple_date rem ev : (Remind.rem, error) result =
|
||||
match ev.rrule with
|
||||
| Some (_, (`Yearly, None, None, [])) ->
|
||||
let month, day = (Timedesc.Date.month rem.Remind.date, Timedesc.Date.day rem.Remind.date) in
|
||||
Ok { rem with Remind.simple_yearly = Some (month, day) }
|
||||
Ok { rem with Remind.yearly = Some (month, day) }
|
||||
| Some _ -> Ok rem
|
||||
| None -> Ok rem
|
||||
|
||||
let simple_weekly_recurrence rem ev : (Remind.rem, error) result =
|
||||
let debug_print_of_recurrence_and_skip ev recurs =
|
||||
let uid = Utils.get_uid ev in
|
||||
Printf.eprintf "RRULE: %s\t\t\tUID: %s\n" (Icalendar.show_recurrence recurs) uid;
|
||||
skip
|
||||
|
||||
let simple_recurrence rem ev : (Remind.rem, error) result =
|
||||
(* Here we want to handle simple recurrences, both weekly and daily, but without RDATE or EXDATE or overrides *)
|
||||
|
||||
(*
|
||||
type recur =
|
||||
[ `Byminute of int list
|
||||
@@ -309,23 +294,39 @@ RRULE: (`Weekly, (Some `Until (`Utc (2026-03-25 00:00:00 +00:00))), None, [`Week
|
||||
RRULE: (`Weekly, (Some `Until (`Utc (2026-07-01 09:00:00 +00:00))), None, []) UID: 4479f7fd-7be9-470f-bc10-5ed61636547b
|
||||
*)
|
||||
match ev.rrule with
|
||||
| Some (_, (`Yearly, None, None, [])) -> Ok rem
|
||||
| Some (_, (freq, count_or_until, interval, recurs)) ->
|
||||
let _recur = (freq, count_or_until, interval, recurs) in
|
||||
let uid = Utils.get_uid ev in
|
||||
Printf.eprintf "RRULE: %s\t\t\tUID: %s\n" (Icalendar.show_recurrence _recur) uid;
|
||||
skip
|
||||
(* TODO: implementare *)
|
||||
| Some (_, (`Yearly, None, None, [])) -> Ok rem (* handled in yearly_simple_date *)
|
||||
| Some (_, (`Weekly, count_or_until, interval, recurs)) ->
|
||||
begin if List.length rem.recurring > 0 || List.length rem.exdate > 0 then (
|
||||
Printf.eprintf "Warning: skipping complex recurrence with EXDATE/RDATE/overrides, not supported\t\t\tUID: %s\n"
|
||||
(Utils.get_uid ev);
|
||||
debug_print_of_recurrence_and_skip ev (`Weekly, count_or_until, interval, recurs))
|
||||
else
|
||||
let days =
|
||||
ListLabels.filter_map recurs ~f:(function
|
||||
| `Byday days -> begin List.map (fun (_n, weekday) -> weekday) days |> Option.some end
|
||||
| _ -> None)
|
||||
|> List.flatten
|
||||
in
|
||||
let week_start =
|
||||
ListLabels.find_map recurs ~f:(function
|
||||
| `Weekday `Sunday -> Some `Sunday
|
||||
| `Weekday `Monday -> Some `Monday
|
||||
| _ -> None)
|
||||
in
|
||||
Ok { rem with Remind.weekly = Some { count_or_until; interval; byday = days; week_start } }
|
||||
end
|
||||
| Some (_, recurs) -> debug_print_of_recurrence_and_skip ev recurs
|
||||
| None -> Ok rem
|
||||
|
||||
let all_collectors : (collector * event_description) list =
|
||||
let all_collectors : collector list =
|
||||
[
|
||||
(collect_uuid, `Collect_uuid);
|
||||
(collect_summary, `Has_summary);
|
||||
(collect_start_end_duration, `All_day_event);
|
||||
(expand_recurrence, `Expand_recurrence);
|
||||
(yearly_simple_date, `Yearly_simple_date);
|
||||
(simple_weekly_recurrence, `Simple_weekly_recurrence);
|
||||
collect_uuid;
|
||||
collect_summary;
|
||||
collect_start_end_duration;
|
||||
collect_exdates;
|
||||
expand_recurrence;
|
||||
yearly_simple_date;
|
||||
simple_recurrence;
|
||||
]
|
||||
|
||||
let remind_of_event (ev : Icalendar.event list) : (Remind.rem, error) result =
|
||||
@@ -343,7 +344,7 @@ let remind_of_event (ev : Icalendar.event list) : (Remind.rem, error) result =
|
||||
|
||||
let rem = { Remind.empty with Remind.recurring = recurrence } in
|
||||
|
||||
ListLabels.fold_left ~init:(Ok rem) all_collectors ~f:(fun rem_or_error (pred, _desc) ->
|
||||
ListLabels.fold_left ~init:(Ok rem) all_collectors ~f:(fun rem_or_error pred ->
|
||||
match rem_or_error with
|
||||
| Error e -> Error e
|
||||
| Ok rem -> pred rem master)
|
||||
|
||||
Reference in New Issue
Block a user