feat: support EXDATE, DATE+DURATION, and weekly day inference

- Handle DATE+DURATION all-day events by computing end_date from
  duration
- Add PUSH/POP-OMIT-CONTEXT and OMIT/SKIP support for EXDATE in
  daily/weekly rendering
- Adjust UNTIL date when local time of UNTIL timestamp precedes event
  start time
- Default weekly RRULE to event's own weekday when BYDAY list is empty
- Remove EXDATE check from complex-recurrence guard (handled via OMIT
  now)
- Add timedesc_wd_to_ical weekday conversion utility
- Remove verbose EXDATE debug logging
- Fix minor newline in file processing log message
This commit is contained in:
2026-05-17 12:58:08 +02:00
parent 0bd014c1fe
commit e9f4773312
4 changed files with 63 additions and 13 deletions

View File

@@ -82,7 +82,7 @@ open Utils
snippet: 'REM Jul 29 MSG Compleanno' snippet: 'REM Jul 29 MSG Compleanno'
priorita: Dopo priorita: Dopo
- id: P10 - id: P10
pattern: Eccezioni pattern: Eccezioni
ics: "EXDATE (una o più), RDATE aggiuntive" ics: "EXDATE (una o più), RDATE aggiuntive"
remind_support: nativo+accorgimenti remind_support: nativo+accorgimenti
@@ -192,9 +192,13 @@ let collect_start_end_duration rem ev : (Remind.rem, error) result =
Ok { rem with Remind.date = day_start; Remind.end_date = None } 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 } else Ok { rem with Remind.date = day_start; Remind.end_date = Some day_end }
end end
| Some (`Duration (_, _duration)) -> | Some (`Duration (_, duration)) ->
(* Start is a date, duration is not supported: invalid case for all-day event *) (* DATE + DURATION: compute end_date as start + duration_in_days - 1 *)
skip let days, _ = Ptime.Span.to_d_ps duration in
if days <= 1 then Ok { rem with Remind.date = day_start; Remind.end_date = None }
else
let day_end = Timedesc.Date.add ~days:(days - 1) day_start in
Ok { rem with Remind.date = day_start; Remind.end_date = Some day_end }
end) end)
| `Datetime datetime -> begin | `Datetime datetime -> begin
let start_td = Utils.timedesc_of_timestamp datetime in let start_td = Utils.timedesc_of_timestamp datetime in
@@ -291,7 +295,7 @@ RRULE: (`Weekly, (Some `Until (`Utc (2026-07-01 09:00:00 +00:00))), None, [])
| Some (_, (`Yearly, None, None, [])) -> Ok rem (* handled in yearly_simple_date *) | Some (_, (`Yearly, None, None, [])) -> Ok rem (* handled in yearly_simple_date *)
| Some (_, ((`Weekly as freq), count_or_until, interval, recurs)) | Some (_, ((`Weekly as freq), count_or_until, interval, recurs))
| Some (_, ((`Daily as freq), count_or_until, interval, recurs)) -> | Some (_, ((`Daily as freq), count_or_until, interval, recurs)) ->
begin if List.length rem.recurring > 0 || List.length rem.exdate > 0 then ( begin if List.length rem.recurring > 0 then (
Printf.eprintf "Warning: skipping complex recurrence with EXDATE/RDATE/overrides, not supported\t\t\tUID: %s\n" Printf.eprintf "Warning: skipping complex recurrence with EXDATE/RDATE/overrides, not supported\t\t\tUID: %s\n"
(Utils.get_uid ev); (Utils.get_uid ev);
debug_print_of_recurrence_and_skip ev (freq, count_or_until, interval, recurs)) debug_print_of_recurrence_and_skip ev (freq, count_or_until, interval, recurs))
@@ -311,6 +315,7 @@ RRULE: (`Weekly, (Some `Until (`Utc (2026-07-01 09:00:00 +00:00))), None, [])
match freq with match freq with
| `Daily -> Ok { rem with Remind.weekly = None; Remind.daily = Some { count_or_until; interval; week_start } } | `Daily -> Ok { rem with Remind.weekly = None; Remind.daily = Some { count_or_until; interval; week_start } }
| `Weekly -> | `Weekly ->
let days = if days = [] then [ timedesc_wd_to_ical (Timedesc.Date.weekday rem.date) ] else days in
Ok Ok
{ {
rem with rem with

View File

@@ -18,7 +18,7 @@ let ical2rem ical_files =
let good_rems = let good_rems =
ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename -> ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename ->
try try
Printf.eprintf "\nProcessing file: %s\n" filename; Printf.eprintf "Processing file: %s\n" filename;
let file_content = read_file filename in let file_content = read_file filename in
let basename = Filename.remove_extension (Filename.basename filename) in let basename = Filename.remove_extension (Filename.basename filename) in
match Icalendar.parse file_content with match Icalendar.parse file_content with

View File

@@ -67,12 +67,24 @@ let add_interval_daily b (d : simple_daily) =
let n = Option.value ~default:1 d.interval in let n = Option.value ~default:1 d.interval in
Buffer.add_string b (spf "*%d " n) Buffer.add_string b (spf "*%d " n)
(** Adjust an UNTIL date: if the event has a start time and the local time of the UNTIL timestamp is strictly before
that start time, the last valid occurrence is on the previous day. *)
let until_date_adjusted (until_ts : Timedesc.t) (event_time : Timedesc.Time.t option) : Timedesc.Date.t =
let until_date = Timedesc.date until_ts in
match event_time with
| None -> until_date
| Some evt_t ->
let until_t = Timedesc.time until_ts in
let cmp = Timedesc.Span.compare (Timedesc.Time.to_span until_t) (Timedesc.Time.to_span evt_t) in
if cmp < 0 then Timedesc.Date.add ~days:(-1) until_date else until_date
let add_until b rem (w : simple_weekly) = let add_until b rem (w : simple_weekly) =
match w.count_or_until with match w.count_or_until with
| None -> () | None -> ()
| Some (`Until d) -> | Some (`Until d) ->
let ts = timedesc_of_utc_or_timestamp_local d in let ts = timedesc_of_utc_or_timestamp_local d in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 (Timedesc.date ts))) let date = until_date_adjusted ts rem.time in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date))
| Some (`Count count) -> | Some (`Count count) ->
let wd = Timedesc.Date.weekday rem.date in let wd = Timedesc.Date.weekday rem.date in
let wd_int = Timedesc.Utils.tm_int_of_weekday wd in let wd_int = Timedesc.Utils.tm_int_of_weekday wd in
@@ -90,7 +102,8 @@ let add_until_daily b rem (d : simple_daily) =
| None -> () | None -> ()
| Some (`Until dt) -> | Some (`Until dt) ->
let ts = timedesc_of_utc_or_timestamp_local dt in let ts = timedesc_of_utc_or_timestamp_local dt in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 (Timedesc.date ts))) let date = until_date_adjusted ts rem.time in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date))
| Some (`Count count) -> | Some (`Count count) ->
let iv = Option.value ~default:1 d.interval in let iv = Option.value ~default:1 d.interval in
let until = Timedesc.Date.add ~days:((count - 1) * iv) rem.date in let until = Timedesc.Date.add ~days:((count - 1) * iv) rem.date in
@@ -110,10 +123,32 @@ let add_through b = function
let add_msg b summary = Buffer.add_string b (spf " MSG %s\n" summary) let add_msg b summary = Buffer.add_string b (spf " MSG %s\n" summary)
let date_of_date_or_datetime (d : Icalendar.date_or_datetime) : Timedesc.Date.t =
match d with
| `Date (year, month, day) -> Timedesc.Date.Ymd.make_exn ~year ~month ~day
| `Datetime ts -> Timedesc.date (timedesc_of_timestamp ts)
let add_omit b (d : Icalendar.date_or_datetime) =
let date = date_of_date_or_datetime d in
let day = Timedesc.Date.day date in
let month = string_of_month (month_of_int (Timedesc.Date.month date)) in
let year = Timedesc.Date.year date in
Buffer.add_string b (spf "OMIT %d %s %d\n" day month year)
let add_omit_context b exdates =
if exdates <> [] then begin
Buffer.add_string b "PUSH-OMIT-CONTEXT\n";
List.iter (add_omit b) exdates
end
let close_omit_context b exdates = if exdates <> [] then Buffer.add_string b "POP-OMIT-CONTEXT\n"
let add_skip b exdates = if exdates <> [] then Buffer.add_string b "SKIP "
(* ── rendering ────────────────────────────────────────────────── *) (* ── rendering ────────────────────────────────────────────────── *)
let render_daily rem (d : simple_daily) = let render_daily rem (d : simple_daily) =
let b = Buffer.create 256 in let b = Buffer.create 256 in
add_omit_context b rem.exdate;
add_rem b; add_rem b;
add_info b rem.original_uuid; add_info b rem.original_uuid;
add_source b rem.source; add_source b rem.source;
@@ -121,13 +156,16 @@ let render_daily rem (d : simple_daily) =
Buffer.add_char b ' '; Buffer.add_char b ' ';
add_interval_daily b d; add_interval_daily b d;
add_until_daily b rem d; add_until_daily b rem d;
add_skip b rem.exdate;
add_at b rem.time; add_at b rem.time;
add_duration b rem.duration; add_duration b rem.duration;
add_msg b rem.summary; add_msg b rem.summary;
close_omit_context b rem.exdate;
Buffer.contents b Buffer.contents b
let render_weekly rem (w : simple_weekly) = let render_weekly rem (w : simple_weekly) =
let b = Buffer.create 256 in let b = Buffer.create 256 in
add_omit_context b rem.exdate;
List.iter List.iter
(fun wd -> (fun wd ->
add_rem b; add_rem b;
@@ -138,10 +176,12 @@ let render_weekly rem (w : simple_weekly) =
Buffer.add_char b ' '; Buffer.add_char b ' ';
add_interval b w; add_interval b w;
add_until b rem w; add_until b rem w;
add_skip b rem.exdate;
add_at b rem.time; add_at b rem.time;
add_duration b rem.duration; add_duration b rem.duration;
add_msg b rem.summary) add_msg b rem.summary)
w.byday; w.byday;
close_omit_context b rem.exdate;
Buffer.contents b Buffer.contents b
let render_single rem = let render_single rem =

View File

@@ -2,6 +2,16 @@ open Icalendar
type months = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec type months = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec
let timedesc_wd_to_ical (wd : Timedesc.weekday) : Icalendar.weekday =
match wd with
| `Mon -> `Monday
| `Tue -> `Tuesday
| `Wed -> `Wednesday
| `Thu -> `Thursday
| `Fri -> `Friday
| `Sat -> `Saturday
| `Sun -> `Sunday
let show_error (e : Timedesc.Date.Ymd.error) : string = let show_error (e : Timedesc.Date.Ymd.error) : string =
match e with match e with
| `Does_not_exist -> "Date does not exist" | `Does_not_exist -> "Date does not exist"
@@ -103,7 +113,6 @@ let timedesc_of_utc_or_timestamp_local (ts : utc_or_timestamp_local) : Timedesc.
| `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:local_tz | `Utc t -> t |> Timedesc.Utils.timestamp_of_ptime |> Timedesc.of_timestamp_exn ~tz_of_date_time:local_tz
let get_exdates ev = let get_exdates ev =
let uid = get_uid ev in
let event_props = ev.props in let event_props = ev.props in
let dates_or_datetimes = let dates_or_datetimes =
List.filter_map List.filter_map
@@ -121,10 +130,6 @@ let get_exdates ev =
| `Datetimes ts_list -> (acc_datetimes @ ts_list, acc_dates)) | `Datetimes ts_list -> (acc_datetimes @ ts_list, acc_dates))
in in
if List.length dates > 0 then Printf.eprintf "Found EXDATE with dates: %d entries; UID: %s\n" (List.length dates) uid;
if List.length datetimes > 0 then
Printf.eprintf "Found EXDATE with datetimes: %d entries; UID: %s\n" (List.length datetimes) uid;
List.map (fun d -> `Date d) dates @ List.map (fun dt -> `Datetime dt) datetimes List.map (fun d -> `Date d) dates @ List.map (fun dt -> `Datetime dt) datetimes
let get_rdates ev = let get_rdates ev =