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 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 *) 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; 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 = let n = Option.value ~default:1 w.interval in Buffer.add_string b (spf "*%d " (n * 7)) let add_until b rem w = 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_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_weekly rem w = 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.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)