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