feat(monthly): add support for MONTHLY recurrence (P07, P08)

- Add `monthly_pattern`, `simple_monthly` types and `monthly` field to
  `rem`
- Implement `render_monthly` and `add_until_monthly` in `remind.ml`
- Handle `BYMONTHDAY` (P07) and nth-weekday `BYDAY` (P08) patterns in
  `eventPredicates.ml`
- Add `add_months` utility for date arithmetic
- Mark P07 and P08 as implemented in documentation
This commit is contained in:
2026-05-17 23:51:54 +02:00
parent 106aff01bf
commit eda3be195a
3 changed files with 108 additions and 5 deletions

View File

@@ -17,6 +17,17 @@ type simple_daily = {
}
(** A simple daily REM command *)
type monthly_pattern =
| By_month_day of int (** P07: BYMONTHDAY=n or implicit day from DTSTART *)
| By_nth_weekday of int * Icalendar.weekday (** P08: BYDAY=nWD, n≠0, can be negative *)
type simple_monthly = {
count_or_until : Icalendar.count_or_until option;
interval : int option;
pattern : monthly_pattern;
}
(** A simple monthly REM command *)
type rem = {
source : string; (** Source file or identifier for the reminder *)
original_uuid : string; (** Original UID from the iCalendar event *)
@@ -26,6 +37,7 @@ type rem = {
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) *)
monthly : simple_monthly option; (** Optional simple monthly recurrence *)
weekly : simple_weekly option; (** Optional simple weekly recurrence *)
daily : simple_daily option; (** Optional simple daily recurrence *)
recurring : Icalendar.event list;
@@ -46,6 +58,7 @@ let empty =
time = None;
duration = None;
yearly = None;
monthly = None;
weekly = None;
daily = None;
recurring = [];
@@ -111,6 +124,22 @@ let add_until_daily b rem (d : simple_daily) =
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_until_monthly b rem (m : simple_monthly) =
match m.count_or_until with
| None -> ()
| Some (`Until dt) ->
let ts = timedesc_of_utc_or_timestamp_local dt in
let date = until_date_adjusted ts rem.time in
Buffer.add_string b (spf "UNTIL %s " (Timedesc.Date.to_rfc3339 date))
| Some (`Count count) ->
let base = Utils.add_months rem.date (count - 1) in
let until =
match m.pattern with
| By_month_day _ -> base
| By_nth_weekday _ -> Timedesc.Date.add ~days:6 base (* weekday can shift up to 6 days *)
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 -> ()
@@ -199,6 +228,31 @@ let render_weekly rem (w : simple_weekly) =
close_omit_context b rem.exdate;
Buffer.contents b
let render_monthly rem (m : simple_monthly) =
let b = Buffer.create 256 in
add_omit_context b rem.exdate;
add_rem b;
add_info b rem.original_uuid;
add_source b rem.source;
(match m.pattern with
| By_month_day day -> Buffer.add_string b (spf "%d " day)
| By_nth_weekday (n, wd) when n > 0 ->
let day = ((n - 1) * 7) + 1 in
add_weekday b wd;
Buffer.add_string b (spf "%d " day)
| By_nth_weekday (n, wd) (* n < 0 *) ->
let back = -n * 7 in
add_weekday b wd;
Buffer.add_string b (spf "1 --%d " back));
Buffer.add_string b (spf "FROM %s " (Timedesc.Date.to_rfc3339 rem.date));
add_until_monthly b rem m;
add_skip b rem.exdate;
add_at b rem.time;
add_duration b rem.duration;
add_msg b rem.summary;
close_omit_context b rem.exdate;
Buffer.contents b
let render_single rem =
let b = Buffer.create 256 in
add_rem b;
@@ -230,9 +284,12 @@ 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 rem month day
| None -> render_single rem))
match rem.monthly with
| Some m -> render_monthly rem m
| None -> (
match rem.yearly with
| Some (month, day) -> render_yearly rem month day
| None -> render_single rem)))
in
let overrides = List.map render_single rem.overrides in
String.concat "" (main :: overrides)