Files
ical2rem/bin/remind.ml
Paolo Donadeo 794c855cec feat(recurrence): add daily recurrence support
- Add `simple_daily` type and `daily` field to `rem`
- Implement `render_daily`, `add_interval_daily`, and `add_until_daily`
- Extend `simple_recurrence` collector to handle `FREQ=DAILY` alongside
  `FREQ=WEEKLY`
- Remove dead `expand_recurrence` collector
- Mark P06 pattern as implemented ()
2026-05-17 00:25:01 +02:00

174 lines
6.1 KiB
OCaml

open Utils
type week_first_day = [ `Sunday | `Monday ]
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 *)
}
(** A simple weekly REM command *)
type simple_daily = {
count_or_until : Icalendar.count_or_until option;
interval : int option; (** Optional interval for daily recurrence, default is 1 *)
week_start : week_first_day option; (** First day of the week for weekly recurrence *)
}
(** A simple daily REM command *)
type rem = {
source : string; (** Source file or identifier for the reminder *)
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 *)
daily : simple_daily option; (** Optional simple daily 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 *)
}
(** A complete REM command *)
let empty =
{
source = "";
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;
daily = None;
recurring = [];
exdate = [];
}
(* ── buffer primitives ────────────────────────────────────────── *)
let add_rem b = Buffer.add_string b "REM "
let add_info b uuid = Buffer.add_string b (spf "INFO \"UID: %s\" " uuid)
let add_date b date = Buffer.add_string b (Timedesc.Date.to_rfc3339 date)
let add_weekday b wd = Buffer.add_string b (spf "%s " (string_of_weekday wd))
let add_interval b (w : simple_weekly) =
let n = Option.value ~default:1 w.interval in
Buffer.add_string b (spf "*%d " (n * 7))
let add_interval_daily b (d : simple_daily) =
let n = Option.value ~default:1 d.interval in
Buffer.add_string b (spf "*%d " n)
let add_until b rem (w : simple_weekly) =
match w.count_or_until with
| None -> ()
| Some (`Until d) ->
let ts = timedesc_of_utc_or_timestamp_local d in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 (Timedesc.date ts)))
| Some (`Count count) ->
let wd = Timedesc.Date.weekday rem.date in
let wd_int = Timedesc.Utils.tm_int_of_weekday wd in
let sub =
match w.week_start with
| Some `Monday -> wd_int - 1
| _ -> wd_int
in
let iv = Option.value ~default:1 w.interval in
let until = Timedesc.Date.add ~days:((count * 7 * iv) - sub) rem.date in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 until))
let add_until_daily b rem (d : simple_daily) =
match d.count_or_until with
| None -> ()
| Some (`Until dt) ->
let ts = timedesc_of_utc_or_timestamp_local dt in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 (Timedesc.date ts)))
| Some (`Count count) ->
let iv = Option.value ~default:1 d.interval in
let until = Timedesc.Date.add ~days:((count - 1) * iv) rem.date in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 until))
let add_at b = function
| Some t -> Buffer.add_string b (spf " AT %s" (string_of_time t))
| None -> ()
let add_duration b = function
| Some d -> Buffer.add_string b (spf " DURATION %s" (string_of_span d))
| None -> ()
let add_through b = function
| Some d -> Buffer.add_string b (spf " THROUGH %s" (Timedesc.Date.to_rfc3339 d))
| None -> ()
let add_msg b summary = Buffer.add_string b (spf " MSG %s\n" summary)
(* ── rendering ────────────────────────────────────────────────── *)
let render_daily rem (d : simple_daily) =
let b = Buffer.create 256 in
add_rem b;
add_info b rem.original_uuid;
add_date b rem.date;
Buffer.add_char b ' ';
add_interval_daily b d;
add_until_daily b rem d;
add_at b rem.time;
add_duration b rem.duration;
add_msg b rem.summary;
Buffer.contents b
let render_weekly rem (w : simple_weekly) =
let b = Buffer.create 256 in
List.iter
(fun wd ->
add_rem b;
add_info b rem.original_uuid;
add_weekday b wd;
add_date b rem.date;
Buffer.add_char b ' ';
add_interval b w;
add_until b rem w;
add_at b rem.time;
add_duration b rem.duration;
add_msg b rem.summary)
w.byday;
Buffer.contents b
let render_single rem =
let b = Buffer.create 256 in
add_rem b;
add_info b rem.original_uuid;
add_date b rem.date;
add_at b rem.time;
add_duration b rem.duration;
add_through b rem.end_date;
add_msg b rem.summary;
Buffer.contents b
let render_yearly month day summary =
let b = Buffer.create 64 in
add_rem b;
Buffer.add_string b (spf "%s %d" (month_of_int month |> string_of_month) day);
add_msg b summary;
Buffer.contents b
(* ── 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 month day rem.summary
| None -> render_single rem))