Command-line and Emacs Calendar Client
1open Icalendar
2
3type event_id = string
4
5type t = {
6 calendar_name : string;
7 file : Eio.Fs.dir_ty Eio.Path.t;
8 event : event;
9 calendar : calendar;
10}
11
12type date_error = [ `Msg of string ]
13
14let generate_uuid () =
15 let uuid = Uuidm.v4_gen (Random.State.make_self_init ()) () in
16 Uuidm.to_string uuid
17
18let default_prodid = `Prodid (Params.empty, "-//Freumh//Caledonia//EN")
19
20let create ~(fs : Eio.Fs.dir_ty Eio.Path.t) ~calendar_dir_path ~summary ~start
21 ?end_ ?location ?description ?recurrence calendar_name =
22 let uuid = generate_uuid () in
23 let uid = (Params.empty, uuid) in
24 let file_name = uuid ^ ".ics" in
25 let file =
26 Eio.Path.(
27 fs / calendar_dir_path / (match calendar_name with s -> s) / file_name)
28 in
29 let dtstart = (Params.empty, start) in
30 let dtend_or_duration = end_ in
31 let rrule = Option.map (fun r -> (Params.empty, r)) recurrence in
32 let now = Ptime_clock.now () in
33 let props = [ `Summary (Params.empty, summary) ] in
34 let props =
35 match location with
36 | Some loc -> `Location (Params.empty, loc) :: props
37 | None -> props
38 in
39 let props =
40 match description with
41 | Some desc -> `Description (Params.empty, desc) :: props
42 | None -> props
43 in
44 let event =
45 {
46 dtstamp = (Params.empty, now);
47 uid;
48 dtstart;
49 dtend_or_duration;
50 rrule;
51 props;
52 alarms = [];
53 }
54 in
55 let calendar =
56 let props = [ default_prodid ] in
57 let components = [ `Event event ] in
58 (props, components)
59 in
60 { calendar_name; file; event; calendar }
61
62let edit ?summary ?start ?end_ ?location ?description ?recurrence t =
63 let now = Ptime_clock.now () in
64 let uid = t.event.uid in
65 let dtstart =
66 match start with None -> t.event.dtstart | Some s -> (Params.empty, s)
67 in
68 let dtend_or_duration =
69 match end_ with None -> t.event.dtend_or_duration | Some _ -> end_
70 in
71 let rrule =
72 match recurrence with
73 | None -> t.event.rrule
74 | Some r -> Some (Params.empty, r)
75 in
76 let props =
77 List.filter
78 (function
79 | `Summary _ -> ( match summary with None -> true | Some _ -> false)
80 | `Location _ -> ( match location with None -> true | Some _ -> false)
81 | `Description _ -> (
82 match description with None -> true | Some _ -> false)
83 | _ -> true)
84 t.event.props
85 in
86 let props =
87 match summary with
88 | Some summary -> `Summary (Params.empty, summary) :: props
89 | None -> props
90 in
91 let props =
92 match location with
93 | Some loc -> `Location (Params.empty, loc) :: props
94 | None -> props
95 in
96 let props =
97 match description with
98 | Some desc -> `Description (Params.empty, desc) :: props
99 | None -> props
100 in
101 let alarms = t.event.alarms in
102 let event =
103 {
104 dtstamp = (Params.empty, now);
105 uid;
106 dtstart;
107 dtend_or_duration;
108 rrule;
109 props;
110 alarms;
111 }
112 in
113 let calendar_name = t.calendar_name in
114 let file = t.file in
115 let calendar = t.calendar in
116 { calendar_name; file; event; calendar }
117
118let events_of_icalendar calendar_name ~file calendar =
119 let remove_dup_ids lst =
120 let rec aux acc = function
121 | [] -> acc
122 | x :: xs ->
123 if List.exists (fun r -> r.uid = x.uid) acc then aux acc xs
124 else aux (x :: acc) xs
125 in
126 aux [] lst
127 in
128 let events =
129 List.filter_map
130 (function `Event event -> Some event | _ -> None)
131 (snd calendar)
132 in
133 let events = remove_dup_ids events in
134 List.map (function event -> { calendar_name; file; event; calendar }) events
135
136let to_ical_event t = t.event
137let to_ical_calendar t = t.calendar
138let get_id t = snd t.event.uid
139
140let get_summary t =
141 match
142 List.filter_map
143 (function `Summary (_, s) when s <> "" -> Some s | _ -> None)
144 t.event.props
145 with
146 | s :: _ -> Some s
147 | _ -> None
148
149let get_ical_start event = Date.ptime_of_ical (snd event.dtstart)
150let get_start t = get_ical_start t.event
151
152let get_ical_end event =
153 match event.dtend_or_duration with
154 | Some (`Dtend (_, d)) -> Some (Date.ptime_of_ical d)
155 | Some (`Duration (_, span)) -> (
156 let start = get_ical_start event in
157 match Ptime.add_span start span with
158 | Some t -> Some t
159 | None ->
160 failwith
161 (Printf.sprintf "Invalid duration calculation: %s + %s"
162 (Ptime.to_rfc3339 start)
163 (Printf.sprintf "%.2fs" (Ptime.Span.to_float_s span))))
164 | None -> None
165
166let get_end t = get_ical_end t.event
167
168let get_start_timezone t =
169 match t.event.dtstart with
170 | _, `Datetime (`With_tzid (_, (_, tzid))) -> Some tzid
171 | _, `Datetime (`Utc _) -> Some "UTC"
172 | _, `Datetime (`Local _) -> Some "FLOATING"
173 | _ -> None
174
175let get_end_timezone t =
176 match t.event.dtend_or_duration with
177 | Some (`Dtend (_, `Datetime (`With_tzid (_, (_, tzid))))) -> Some tzid
178 | Some (`Dtend (_, `Datetime (`Utc _))) -> Some "UTC"
179 | Some (`Dtend (_, `Datetime (`Local _))) -> Some "FLOATING"
180 | _ -> None
181
182let get_duration t =
183 match t.event.dtend_or_duration with
184 | Some (`Duration (_, span)) -> Some span
185 | Some (`Dtend (_, e)) ->
186 let span = Ptime.diff (Date.ptime_of_ical e) (get_start t) in
187 Some span
188 | None -> None
189
190let is_date t =
191 match (t.event.dtstart, t.event.dtend_or_duration) with
192 | (_, `Date _), _ -> true
193 | _, Some (`Dtend (_, `Date _)) -> true
194 | _ -> false
195
196let get_location t =
197 match
198 List.filter_map
199 (function `Location (_, s) when s <> "" -> Some s | _ -> None)
200 t.event.props
201 with
202 | s :: _ -> Some s
203 | _ -> None
204
205let get_description t =
206 match
207 List.filter_map
208 (function `Description (_, s) when s <> "" -> Some s | _ -> None)
209 t.event.props
210 with
211 | s :: _ -> Some s
212 | _ -> None
213
214let get_recurrence t = Option.map (fun r -> snd r) t.event.rrule
215let get_calendar_name t = t.calendar_name
216let get_file t = t.file
217
218type comparator = t -> t -> int
219
220let by_start e1 e2 =
221 let t1 = get_start e1 in
222 let t2 = get_start e2 in
223 Ptime.compare t1 t2
224
225let by_end e1 e2 =
226 match (get_end e1, get_end e2) with
227 | Some t1, Some t2 -> Ptime.compare t1 t2
228 | Some _, None -> 1
229 | None, Some _ -> -1
230 | None, None -> 0
231
232let by_summary e1 e2 =
233 match (get_summary e1, get_summary e2) with
234 | Some s1, Some s2 -> String.compare s1 s2
235 | Some _, None -> 1
236 | None, Some _ -> -1
237 | None, None -> 0
238
239let by_location e1 e2 =
240 match (get_location e1, get_location e2) with
241 | Some l1, Some l2 -> String.compare l1 l2
242 | Some _, None -> 1
243 | None, Some _ -> -1
244 | None, None -> 0
245
246let by_calendar_name e1 e2 =
247 match (get_calendar_name e1, get_calendar_name e2) with
248 | c1, c2 -> String.compare c1 c2
249
250let descending comp e1 e2 = -1 * comp e1 e2
251
252let chain comp1 comp2 e1 e2 =
253 let result = comp1 e1 e2 in
254 if result <> 0 then result else comp2 e1 e2
255
256let clone_with_event t event =
257 let calendar_name = t.calendar_name in
258 let file = t.file in
259 let calendar = t.calendar in
260 { calendar_name; file; event; calendar }
261
262type format = [ `Text | `Entries | `Json | `Csv | `Ics | `Sexp ]
263
264let format_date ?tz date =
265 let dt = Date.ptime_to_timedesc ?tz date in
266 let y = Timedesc.year dt in
267 let m = Timedesc.month dt in
268 let d = Timedesc.day dt in
269 let weekday =
270 match Timedesc.weekday dt with
271 | `Mon -> "Mon"
272 | `Tue -> "Tue"
273 | `Wed -> "Wed"
274 | `Thu -> "Thu"
275 | `Fri -> "Fri"
276 | `Sat -> "Sat"
277 | `Sun -> "Sun"
278 in
279 Printf.sprintf "%04d-%02d-%02d %s" y m d weekday
280
281let format_time ?tz date =
282 let dt = Date.ptime_to_timedesc ?tz date in
283 let h = Timedesc.hour dt in
284 let m = Timedesc.minute dt in
285 Printf.sprintf "%02d:%02d" h m
286
287let format_datetime ?tz date =
288 let tz_str =
289 match tz with
290 | Some tz -> Printf.sprintf "(%s)" (Timedesc.Time_zone.name tz)
291 | None -> ""
292 in
293 Printf.sprintf "%s %s%s" (format_date ?tz date) (format_time ?tz date) tz_str
294
295let day_diff day ~next =
296 let span = Ptime.diff next day in
297 let d, _ = Ptime.Span.to_d_ps span in
298 d
299
300(* exosed from icalendar *)
301
302let weekday_strings =
303 [
304 (`Monday, "MO");
305 (`Tuesday, "TU");
306 (`Wednesday, "WE");
307 (`Thursday, "TH");
308 (`Friday, "FR");
309 (`Saturday, "SA");
310 (`Sunday, "SU");
311 ]
312
313let freq_strings =
314 [
315 (`Daily, "DAILY");
316 (`Hourly, "HOURLY");
317 (`Minutely, "MINUTELY");
318 (`Monthly, "MONTHLY");
319 (`Secondly, "SECONDLY");
320 (`Weekly, "WEEKLY");
321 (`Yearly, "YEARLY");
322 ]
323
324let date_to_str (y, m, d) = Printf.sprintf "%04d%02d%02d" y m d
325
326let datetime_to_str ptime utc =
327 let date, ((hh, mm, ss), _) = Ptime.to_date_time ptime in
328 Printf.sprintf "%sT%02d%02d%02d%s" (date_to_str date) hh mm ss
329 (if utc then "Z" else "")
330
331let timestamp_to_ics ts buf =
332 Buffer.add_string buf
333 @@
334 match ts with
335 | `Utc ts -> datetime_to_str ts true
336 | `Local ts -> datetime_to_str ts false
337 | `With_tzid (ts, _str) -> (* TODO *) datetime_to_str ts false
338
339let recurs_to_ics (freq, count_or_until, interval, l) buf =
340 let write_rulepart key value =
341 Buffer.add_string buf key;
342 Buffer.add_char buf '=';
343 Buffer.add_string buf value
344 in
345 let int_list l = String.concat "," @@ List.map string_of_int l in
346 let recur_to_ics = function
347 | `Byminute byminlist -> write_rulepart "BYMINUTE" (int_list byminlist)
348 | `Byday bywdaylist ->
349 let wday (weeknumber, weekday) =
350 (if weeknumber = 0 then "" else string_of_int weeknumber)
351 ^ List.assoc weekday weekday_strings
352 in
353 write_rulepart "BYDAY" (String.concat "," @@ List.map wday bywdaylist)
354 | `Byhour byhrlist -> write_rulepart "BYHOUR" (int_list byhrlist)
355 | `Bymonth bymolist -> write_rulepart "BYMONTH" (int_list bymolist)
356 | `Bymonthday bymodaylist ->
357 write_rulepart "BYMONTHDAY" (int_list bymodaylist)
358 | `Bysecond byseclist -> write_rulepart "BYSECOND" (int_list byseclist)
359 | `Bysetposday bysplist -> write_rulepart "BYSETPOS" (int_list bysplist)
360 | `Byweek bywknolist -> write_rulepart "BYWEEKNO" (int_list bywknolist)
361 | `Byyearday byyrdaylist ->
362 write_rulepart "BYYEARDAY" (int_list byyrdaylist)
363 | `Weekday weekday ->
364 write_rulepart "WKST" (List.assoc weekday weekday_strings)
365 in
366 write_rulepart "FREQ" (List.assoc freq freq_strings);
367 (match count_or_until with
368 | None -> ()
369 | Some x -> (
370 Buffer.add_char buf ';';
371 match x with
372 | `Count c -> write_rulepart "COUNT" (string_of_int c)
373 | `Until enddate ->
374 (* TODO cleanup *)
375 Buffer.add_string buf "UNTIL=";
376 timestamp_to_ics enddate buf));
377 (match interval with
378 | None -> ()
379 | Some i ->
380 Buffer.add_char buf ';';
381 write_rulepart "INTERVAL" (string_of_int i));
382 List.iter
383 (fun recur ->
384 Buffer.add_char buf ';';
385 recur_to_ics recur)
386 l
387
388let text_event_data ?tz event =
389 let id = get_id event in
390 let start = get_start event in
391 let end_ = get_end event in
392 let start_date = format_date ?tz start in
393 let start_timezone = get_start_timezone event in
394 let end_timezone = get_end_timezone event in
395 let same_timezone =
396 match (start_timezone, end_timezone) with
397 | Some tz1, Some tz2 when tz1 = tz2 -> true
398 | _ -> false
399 in
400 let start_time =
401 match is_date event with
402 | true -> ""
403 | false ->
404 let tz_str =
405 if same_timezone then " " ^ format_time ?tz start
406 else
407 match start_timezone with
408 | Some tzid -> " " ^ format_time ?tz start ^ " (" ^ tzid ^ ")"
409 | None -> " " ^ format_time ?tz start
410 in
411 tz_str
412 in
413 let end_date, end_time =
414 match end_ with
415 | None -> ("", "")
416 | Some end_ -> (
417 match is_date event with
418 | true -> (
419 match day_diff start ~next:end_ <= 1 with
420 | true -> ("", "")
421 | false -> (" - " ^ format_date ?tz end_, ""))
422 | false -> (
423 match day_diff start ~next:end_ == 0 with
424 | true ->
425 let tz_str =
426 match end_timezone with
427 | Some tzid when same_timezone ->
428 ("", " - " ^ format_time ?tz end_ ^ " (" ^ tzid ^ ")")
429 | Some tzid ->
430 ("", " - " ^ format_time ?tz end_ ^ " (" ^ tzid ^ ")")
431 | None -> ("", " - " ^ format_time ?tz end_)
432 in
433 tz_str
434 | false ->
435 let tz_str =
436 match end_timezone with
437 | Some tzid when same_timezone ->
438 ( " - " ^ format_date ?tz end_,
439 " " ^ format_time ?tz end_ ^ " (" ^ tzid ^ ")" )
440 | Some tzid ->
441 ( " - " ^ format_date ?tz end_,
442 " " ^ format_time ?tz end_ ^ " (" ^ tzid ^ ")" )
443 | None ->
444 (" - " ^ format_date ?tz end_, " " ^ format_time ?tz end_)
445 in
446 tz_str))
447 in
448 let summary =
449 match get_summary event with
450 | Some summary when summary <> "" -> summary
451 | _ -> ""
452 in
453 let location =
454 match get_location event with
455 | Some loc when loc <> "" -> "@" ^ loc
456 | _ -> ""
457 in
458 let calendar_name = get_calendar_name event in
459 let date_time = start_date ^ start_time ^ end_date ^ end_time in
460 (id, calendar_name, date_time, summary, location)
461
462let format_event ?(format = `Text) ?tz event =
463 let start = get_start event in
464 let end_ = get_end event in
465 match format with
466 | `Text ->
467 let id, calendar_name, date_time, summary, location =
468 text_event_data ?tz event
469 in
470 Printf.sprintf "%s\t%s\t%s\t%s\t%s" id calendar_name date_time summary
471 location
472 | `Entries ->
473 let format_opt label f opt =
474 Option.map (fun x -> Printf.sprintf "%s: %s\n" label (f x)) opt
475 |> Option.value ~default:""
476 in
477 let start_timezone = get_start_timezone event in
478 let end_timezone = get_end_timezone event in
479 let same_timezone =
480 match (start_timezone, end_timezone) with
481 | Some tz1, Some tz2 when tz1 = tz2 -> true
482 | _ -> false
483 in
484 let format timezone datetime is_end =
485 match is_date event with
486 | true -> format_date ?tz datetime
487 | false ->
488 let tz_suffix =
489 if (not is_end) && same_timezone then ""
490 else match timezone with None -> "" | Some t -> " (" ^ t ^ ")"
491 in
492 format_datetime ?tz datetime ^ tz_suffix
493 in
494 let start_str =
495 format_opt "Start" (fun d -> format start_timezone d false) (Some start)
496 in
497 let end_str =
498 format_opt "End" (fun d -> format end_timezone d true) end_
499 in
500 let location_str = format_opt "Location" Fun.id (get_location event) in
501 let description_str =
502 format_opt "Description" Fun.id (get_description event)
503 in
504 let rrule_str =
505 Option.map
506 (fun r ->
507 let buf = Buffer.create 128 in
508 recurs_to_ics r buf;
509 Printf.sprintf "%s: %s\n" "Reccurence" (Buffer.contents buf))
510 (get_recurrence event)
511 |> Option.value ~default:""
512 in
513 let summary_str = format_opt "Summary" Fun.id (get_summary event) in
514 let file_str = format_opt "File" Fun.id (Some (snd (get_file event))) in
515 Printf.sprintf "%s%s%s%s%s%s%s" summary_str start_str end_str location_str
516 description_str rrule_str file_str
517 | `Json ->
518 let open Yojson.Safe in
519 let json =
520 `Assoc
521 [
522 ("id", `String (get_id event));
523 ( "summary",
524 match get_summary event with
525 | Some summary -> `String summary
526 | None -> `Null );
527 ("start", `String (format_datetime ?tz start));
528 ( "end",
529 match end_ with
530 | Some e -> `String (format_datetime ?tz e)
531 | None -> `Null );
532 ( "location",
533 match get_location event with
534 | Some loc -> `String loc
535 | None -> `Null );
536 ( "description",
537 match get_description event with
538 | Some desc -> `String desc
539 | None -> `Null );
540 ("calendar", match get_calendar_name event with cal -> `String cal);
541 ]
542 in
543 to_string json
544 | `Csv ->
545 let summary =
546 match get_summary event with Some summary -> summary | None -> ""
547 in
548 let start = format_datetime ?tz start in
549 let end_str =
550 match end_ with Some e -> format_datetime ?tz e | None -> ""
551 in
552 let location =
553 match get_location event with Some loc -> loc | None -> ""
554 in
555 let cal_id = match get_calendar_name event with cal -> cal in
556 Printf.sprintf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"" summary start end_str
557 location cal_id
558 | `Ics ->
559 let calendar = to_ical_calendar event in
560 Icalendar.to_ics ~cr:true calendar
561 | `Sexp ->
562 let summary =
563 match get_summary event with Some summary -> summary | None -> ""
564 in
565 let start_date, start_time =
566 let dt = Date.ptime_to_timedesc ?tz start in
567 let y = Timedesc.year dt in
568 let m = Timedesc.month dt in
569 let d = Timedesc.day dt in
570 let h = Timedesc.hour dt in
571 let min = Timedesc.minute dt in
572 let s = Timedesc.second dt in
573 let dow =
574 match Timedesc.weekday dt with
575 | `Mon -> "monday"
576 | `Tue -> "tuesday"
577 | `Wed -> "wednesday"
578 | `Thu -> "thursday"
579 | `Fri -> "friday"
580 | `Sat -> "saturday"
581 | `Sun -> "sunday"
582 in
583 ( Printf.sprintf "(%04d %02d %02d %s)" y m d dow,
584 Printf.sprintf "(%02d %02d %02d)" h min s )
585 in
586 let end_str =
587 match end_ with
588 | Some end_date ->
589 let dt = Date.ptime_to_timedesc ?tz end_date in
590 let y = Timedesc.year dt in
591 let m = Timedesc.month dt in
592 let d = Timedesc.day dt in
593 let h = Timedesc.hour dt in
594 let min = Timedesc.minute dt in
595 let s = Timedesc.second dt in
596 let dow =
597 match Timedesc.weekday dt with
598 | `Mon -> "monday"
599 | `Tue -> "tuesday"
600 | `Wed -> "wednesday"
601 | `Thu -> "thursday"
602 | `Fri -> "friday"
603 | `Sat -> "saturday"
604 | `Sun -> "sunday"
605 in
606 Printf.sprintf "((%04d %02d %02d %s) (%02d %02d %02d))" y m d dow h
607 min s
608 | None -> "nil"
609 in
610 let location =
611 match get_location event with
612 | Some loc -> Printf.sprintf "\"%s\"" (String.escaped loc)
613 | None -> "nil"
614 in
615 let description =
616 match get_description event with
617 | Some desc -> Printf.sprintf "\"%s\"" (String.escaped desc)
618 | None -> "nil"
619 in
620 let calendar =
621 match get_calendar_name event with
622 | cal -> Printf.sprintf "\"%s\"" (String.escaped cal)
623 in
624 let id = get_id event in
625 Printf.sprintf
626 "((:id \"%s\" :summary \"%s\" :start (%s %s) :end %s :location %s \
627 :description %s :calendar %s))"
628 (String.escaped id) (String.escaped summary) start_date start_time
629 end_str location description calendar
630
631let format_events_with_dynamic_columns ?tz events =
632 if events = [] then ""
633 else
634 let event_data = List.map (text_event_data ?tz) events in
635 (* Calculate max width for each column *)
636 let max_id_width =
637 List.fold_left
638 (fun acc (id, _, _, _, _) -> max acc (String.length id))
639 0 event_data
640 in
641 let max_cal_width =
642 List.fold_left
643 (fun acc (_, cal, _, _, _) -> max acc (String.length cal))
644 0 event_data
645 in
646 let max_date_width =
647 List.fold_left
648 (fun acc (_, _, date, _, _) -> max acc (String.length date))
649 0 event_data
650 in
651
652 (* Format each event with calculated widths *)
653 let formatted_events =
654 List.map
655 (fun (id, cal, date, summary, location) ->
656 Printf.sprintf "%-*s %-*s %-*s %s%s" max_id_width id max_cal_width
657 cal max_date_width date summary
658 (if location <> "" then " " ^ location else ""))
659 event_data
660 in
661 String.concat "\n" formatted_events
662
663let format_events ?(format = `Text) ?tz events =
664 match format with
665 | `Json ->
666 let json_events =
667 List.map
668 (fun e -> Yojson.Safe.from_string (format_event ~format:`Json ?tz e))
669 events
670 in
671 Yojson.Safe.to_string (`List json_events)
672 | `Csv ->
673 "\"Summary\",\"Start\",\"End\",\"Location\",\"Calendar\"\n"
674 ^ String.concat "\n" (List.map (format_event ~format:`Csv ?tz) events)
675 | `Sexp ->
676 "("
677 ^ String.concat "\n "
678 (List.map (fun e -> format_event ~format:`Sexp ?tz e) events)
679 ^ ")"
680 | `Text -> format_events_with_dynamic_columns ?tz events
681 | _ ->
682 String.concat "\n" (List.map (fun e -> format_event ~format ?tz e) events)
683
684let expand_recurrences ~from ~to_ event =
685 let rule = get_recurrence event in
686 match rule with
687 (* If there's no recurrence we just return the original event. *)
688 | None ->
689 (* Include the original event instance only if it falls within the query range. *)
690 let start = get_start event in
691 let end_ = match get_end event with None -> start | Some e -> e in
692 if
693 Ptime.compare start to_ < 0
694 &&
695 (* end_ > f, meaning we don't include events that end at the exact start of our range.
696 This is handy to exclude date events that end at 00:00 the next day. *)
697 match from with Some f -> Ptime.compare end_ f > 0 | None -> true
698 then [ event ]
699 else []
700 | Some _ ->
701 let rec collect generator acc =
702 match generator () with
703 | None -> List.rev acc
704 | Some recur ->
705 let start = get_ical_start recur in
706 let end_ =
707 match get_ical_end recur with None -> start | Some e -> e
708 in
709 (* if start >= to then we're outside our (exclusive) date range and we terminate *)
710 if Ptime.compare start to_ >= 0 then List.rev acc
711 (* if end > from then, *)
712 else if
713 match from with
714 | Some f -> Ptime.compare end_ f > 0
715 | None -> true
716 (* we include the event *)
717 then collect generator (clone_with_event event recur :: acc)
718 (* otherwise we iterate till the event is in range *)
719 else collect generator acc
720 in
721 let generator =
722 let ical_event = to_ical_event event in
723 (* The first event is the non recurrence-id one *)
724 let _, other_events =
725 match
726 List.partition
727 (function `Event _ -> true | _ -> false)
728 (snd event.calendar)
729 with
730 | `Event hd :: tl, _ ->
731 (hd, List.map (function `Event e -> e | _ -> assert false) tl)
732 | _ -> assert false
733 in
734 recur_events ~recurrence_ids:other_events ical_event
735 in
736 collect generator []