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 []