Files
ical2rem/bin/remind.ml
Paolo Donadeo cd03124c07 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)
2026-06-20 00:10:25 +02:00

141 lines
5.1 KiB
OCaml

open Remind_sync
open Utils
type week_first_day = [ `Sunday | `Monday ] [@@deriving show]
type simple_weekly = {
count_or_until : Icalendar.count_or_until option;
interval : int option; (** Optional interval for weekly recurrence, default is 1 *)
byday : Icalendar.weekday list;
week_start : week_first_day option; (** First day of the week for weekly recurrence *)
}
[@@deriving show]
(** A simple weekly REM command *)
type rem = {
original_uuid : string; (** Original UID from the iCalendar event *)
summary : string; (** Summary or title of the reminder *)
date : Timedesc.Date.t; (** Date specification (day, month, year) *)
end_date : Timedesc.Date.t option; (** Optional end date for a date range *)
time : Timedesc.Time.t option; (** Optional time specification (hour, minute) *)
duration : Timedesc.Span.t option; (** Optional duration for timed events *)
yearly : (int * int) option; (** Optional simple yearly recurrence (month, day) *)
weekly : simple_weekly option; (** Optional simple weekly recurrence *)
recurring : Icalendar.event list;
(** 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 *)
}
[@@deriving show]
(** A complete REM command *)
let empty =
{
original_uuid = "";
summary = "";
date = Timedesc.Date.Ymd.make_exn ~year:1970 ~month:1 ~day:1;
end_date = None;
time = None;
duration = None;
yearly = None;
weekly = None;
recurring = [];
exdate = [];
}
let render_yearly month day summary =
let month_str = month_of_int month |> string_of_month in
spf "REM %s %d MSG %s\n" month_str day summary
let render_weekly rem weekly =
let b = Buffer.create 256 in
List.iter
begin fun weekday ->
Buffer.add_string b "REM ";
Buffer.add_string b (spf "INFO \"UID: %s\" " rem.original_uuid);
Buffer.add_string b (spf "%s " (string_of_weekday weekday));
Buffer.add_string b (Timedesc.Date.to_rfc3339 rem.date);
Buffer.add_string b " ";
(match weekly.interval with
| Some interval -> Buffer.add_string b (spf "*%d " (interval * 7))
| None -> Buffer.add_string b "*7 ");
(match weekly.count_or_until with
| Some (`Count count) -> begin
(* We must compute the until date based on the count and the interval *)
let wd = Timedesc.Date.weekday rem.date in
let wd_int = Timedesc.Utils.tm_int_of_weekday wd in
let day_to_subtract =
match weekly.week_start with
| Some `Sunday -> wd_int
| Some `Monday -> wd_int - 1
| None -> wd_int (* Default to Sunday if not specified *)
in
let interval = Option.value ~default:1 weekly.interval in
let until_date = Timedesc.Date.add ~days:((count * 7 * interval) - day_to_subtract) rem.date in
Buffer.add_string b "UNTIL ";
Buffer.add_string b (Timedesc.Date.to_rfc3339 until_date);
Buffer.add_string b " "
end
| Some (`Until until_date) -> begin
Buffer.add_string b "UNTIL ";
let ts = timedesc_of_utc_or_timestamp_local until_date in
Buffer.add_string b (Timedesc.Date.to_rfc3339 (Timedesc.date ts));
Buffer.add_string b " "
end
| None -> ());
(match rem.time with
| Some time ->
Buffer.add_string b " AT ";
Buffer.add_string b (string_of_time time)
| None -> ());
(match rem.duration with
| Some duration ->
Buffer.add_string b " DURATION ";
Buffer.add_string b (string_of_span duration);
Buffer.add_string b ""
| None -> ());
Buffer.add_string b " MSG ";
Buffer.add_string b rem.summary;
Buffer.add_string b "\n"
end
weekly.byday;
Buffer.contents b
let string_of_rem rem =
match rem.weekly with
| Some weekly -> render_weekly rem weekly
| None ->
begin match rem.yearly with
| Some (month, day) -> render_yearly month day rem.summary
| None -> begin
let b = Buffer.create 256 in
Buffer.add_string b "REM ";
Buffer.add_string b (spf "INFO \"UID: %s\" " rem.original_uuid);
Buffer.add_string b (Timedesc.Date.to_rfc3339 rem.date);
(match rem.time with
| Some time ->
Buffer.add_string b " AT ";
Buffer.add_string b (string_of_time time)
| None -> ());
(match rem.duration with
| Some duration ->
Buffer.add_string b " DURATION ";
Buffer.add_string b (string_of_span duration);
Buffer.add_string b ""
| None -> ());
(match rem.end_date with
| Some end_date ->
Buffer.add_string b " THROUGH ";
Buffer.add_string b (Timedesc.Date.to_rfc3339 end_date)
| None -> ());
Buffer.add_string b " MSG ";
Buffer.add_string b rem.summary;
Buffer.add_string b "\n";
Buffer.contents b
end
end