Command-line and Emacs Calendar Client

convert filtering and sorting to functional interfaces

Ryan Gibb d97295ec 306f0284

-1
TODO.org
···
- [ ] really stress test the timezone handling -- this is full of gotcha's
- [ ] don't load all calendars into memory to show only one event
- [ ] support specifying duration
-
- [ ] make query filter a lambda
- [ ] diagnose events failing to parse
- [ ] [[https://github.com/robur-coop/icalendar/pull/13][handle RECURRENCE-ID]]
- [ ] CalDAV syncing
+9 -8
lib/date.ml
···
in
timedesc_to_ptime dt
| `Date date -> (
-
let y, m, d = date in
-
match Timedesc.Date.Ymd.make ~year:y ~month:m ~day:d with
-
| Ok new_date ->
-
let midnight = Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 () in
-
let new_dt = Timedesc.of_date_and_time_exn new_date midnight in
-
timedesc_to_ptime new_dt
-
| Error _ ->
-
failwith (Printf.sprintf "Invalid date %d-%d-%d" y m d))
+
let y, m, d = date in
+
match Timedesc.Date.Ymd.make ~year:y ~month:m ~day:d with
+
| Ok new_date ->
+
let midnight =
+
Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 ()
+
in
+
let new_dt = Timedesc.of_date_and_time_exn new_date midnight in
+
timedesc_to_ptime new_dt
+
| Error _ -> failwith (Printf.sprintf "Invalid date %d-%d-%d" y m d))
+34
lib/event.ml
···
fs / calendar_dir_path
/ (match t.collection with Col s -> s)
/ t.file_name)
+
+
type comparator = t -> t -> int
+
+
let by_start e1 e2 =
+
let t1 = get_start e1 in
+
let t2 = get_start e2 in
+
Ptime.compare t1 t2
+
+
let by_end e1 e2 =
+
match (get_end e1, get_end e2) with
+
| Some t1, Some t2 -> Ptime.compare t1 t2
+
| Some _, None -> 1
+
| None, Some _ -> -1
+
| None, None -> 0
+
+
let by_summary e1 e2 =
+
match (get_summary e1, get_summary e2) with
+
| Some s1, Some s2 -> String.compare s1 s2
+
| Some _, None -> 1
+
| None, Some _ -> -1
+
| None, None -> 0
+
+
let by_location e1 e2 =
+
match (get_location e1, get_location e2) with
+
| Some l1, Some l2 -> String.compare l1 l2
+
| Some _, None -> 1
+
| None, Some _ -> -1
+
| None, None -> 0
+
+
let by_collection e1 e2 =
+
match (get_collection e1, get_collection e2) with
+
| Collection.Col c1, Collection.Col c2 -> String.compare c1 c2
+
+
let descending comp e1 e2 = -1 * comp e1 e2
+24
lib/event.mli
···
val get_file_path :
fs:'a Eio.Path.t -> calendar_dir_path:string -> t -> 'a Eio.Path.t
+
+
type comparator = t -> t -> int
+
(** Event comparator function type *)
+
+
val by_start : comparator
+
(** Compare events by start time, earlier times come first *)
+
+
val by_end : comparator
+
(** Compare events by end time, earlier times come first. Events with end times
+
come after those without *)
+
+
val by_summary : comparator
+
(** Compare events by summary alphabetically. Events with summaries come before
+
those without *)
+
+
val by_location : comparator
+
(** Compare events by location alphabetically. Events with locations come before
+
those without *)
+
+
val by_collection : comparator
+
(** Compare events by collection name alphabetically *)
+
+
val descending : comparator -> comparator
+
(** Reverse the order of a comparator *)
+36 -120
lib/query.ml
···
-
type filter =
-
| SummaryContains of string
-
| DescriptionContains of string
-
| LocationContains of string
-
| InCollections of Collection.t list
-
| RecurringOnly
-
| NonRecurringOnly
-
| WithId of Event.event_id
-
| And of filter list
-
| Or of filter list
-
| Not of filter
+
type filter = Event.t -> bool
-
type sort_order = [ `Ascending | `Descending ]
-
type sort_by = [ `Start | `End | `Summary | `Location | `Calendar ]
+
let text_matches pattern text =
+
let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote pattern) in
+
Re.Pcre.pmatch ~rex:re text
-
let summary_contains text = SummaryContains text
-
let description_contains text = DescriptionContains text
-
let location_contains text = LocationContains text
-
let in_collections ids = InCollections ids
-
let recurring_only () = RecurringOnly
-
let non_recurring_only () = NonRecurringOnly
-
let with_id id = WithId id
-
let and_filter filters = And filters
-
let or_filter filters = Or filters
-
let not_filter filter = Not filter
+
let summary_contains text event =
+
match Event.get_summary event with
+
| Some summary -> text_matches text summary
+
| None -> false
-
let rec matches_filter event = function
-
| SummaryContains text -> (
-
match Event.get_summary event with
-
| Some summary ->
-
let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in
-
Re.Pcre.pmatch ~rex:re summary
-
| None -> false)
-
| DescriptionContains text -> (
-
match Event.get_description event with
-
| Some desc ->
-
let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in
-
Re.Pcre.pmatch ~rex:re desc
-
| None -> false)
-
| LocationContains text -> (
-
match Event.get_location event with
-
| Some loc ->
-
let re = Re.Pcre.regexp ~flags:[ `CASELESS ] (Re.Pcre.quote text) in
-
Re.Pcre.pmatch ~rex:re loc
-
| None -> false)
-
| InCollections ids ->
-
let id = Event.get_collection event in
-
List.exists (fun col -> col = id) ids
-
| RecurringOnly -> Event.get_recurrence event <> None
-
| NonRecurringOnly -> Event.get_recurrence event = None
-
| WithId id -> Event.get_id event = id
-
| And filters -> List.for_all (matches_filter event) filters
-
| Or filters -> List.exists (matches_filter event) filters
-
| Not filter -> not (matches_filter event filter)
+
let description_contains text event =
+
match Event.get_description event with
+
| Some desc -> text_matches text desc
+
| None -> false
-
let compare_events sort_by order e1 e2 =
-
let compare =
-
match sort_by with
-
| `Start ->
-
let t1 = Event.get_start e1 in
-
let t2 = Event.get_start e2 in
-
Ptime.compare t1 t2
-
| `End -> (
-
match (Event.get_end e1, Event.get_end e2) with
-
| Some t1, Some t2 -> Ptime.compare t1 t2
-
| Some _, None -> 1
-
| None, Some _ -> -1
-
| None, None -> 0)
-
| `Summary -> (
-
match (Event.get_location e1, Event.get_location e2) with
-
| Some l1, Some l2 -> String.compare l1 l2
-
| Some _, None -> 1
-
| None, Some _ -> -1
-
| None, None -> 0)
-
| `Location -> (
-
match (Event.get_location e1, Event.get_location e2) with
-
| Some l1, Some l2 -> String.compare l1 l2
-
| Some _, None -> 1
-
| None, Some _ -> -1
-
| None, None -> 0)
-
| `Calendar -> (
-
match (Event.get_collection e1, Event.get_collection e2) with
-
| Collection.Col c1, Collection.Col c2 -> String.compare c1 c2)
-
in
-
match order with `Ascending -> compare | `Descending -> -compare
+
let location_contains text event =
+
match Event.get_location event with
+
| Some loc -> text_matches text loc
+
| None -> false
+
+
let in_collections ids event =
+
let id = Event.get_collection event in
+
List.exists (fun col -> col = id) ids
+
+
let recurring_only () event = Event.get_recurrence event <> None
+
let non_recurring_only () event = Event.get_recurrence event = None
+
let with_id id event = Event.get_id event = id
+
let and_filter filters event = List.for_all (fun filter -> filter event) filters
+
let or_filter filters event = List.exists (fun filter -> filter event) filters
+
let not_filter filter event = not (filter event)
+
let matches_filter event filter = filter event
-
let query_events ~fs calendar_dir ?filter ?sort_by ?order ?limit () =
+
let query_events ~fs calendar_dir ?filter ?(comparator = Event.by_start) ?limit
+
() =
let ( let* ) = Result.bind in
let* collections = Calendar_dir.get_collections ~fs calendar_dir in
let events =
List.flatten (List.map (fun (_collection, events) -> events) collections)
in
let filtered_events =
-
match filter with
-
| Some f -> List.filter (fun event -> matches_filter event f) events
-
| None -> events
-
in
-
let sorted_events =
-
match (sort_by, order) with
-
| Some criteria, Some ord ->
-
List.sort (compare_events criteria ord) filtered_events
-
| Some criteria, None ->
-
List.sort (compare_events criteria `Ascending) filtered_events
-
| None, _ -> List.sort (compare_events `Start `Ascending) filtered_events
+
match filter with Some f -> List.filter f events | None -> events
in
+
let sorted_events = List.sort comparator filtered_events in
Ok
(match limit with
| Some n when n > 0 ->
···
take n sorted_events []
| _ -> sorted_events)
-
let query ~fs calendar_dir ?filter ~from ~to_ ?sort_by ?order ?limit () =
+
let query ~fs calendar_dir ?filter ~from ~to_
+
?(comparator = Recur.Instance.by_start) ?limit () =
Fmt.epr "Querying from %a to %a\n%!" Ptime.pp (Option.get from) Ptime.pp to_;
-
match query_events ~fs calendar_dir ?filter ?sort_by ?order () with
+
match query_events ~fs calendar_dir ?filter () with
| Ok events ->
let instances =
List.concat_map
(fun event -> Recur.expand_event event ~from ~to_)
events
in
-
let compare_instances criteria ord i1 i2 =
-
match criteria with
-
| `Start ->
-
let c = Ptime.compare i1.Recur.start i2.Recur.start in
-
if ord = `Ascending then c else -c
-
| `End -> (
-
match (i1.Recur.end_, i2.Recur.end_) with
-
| Some t1, Some t2 ->
-
let c = Ptime.compare t1 t2 in
-
if ord = `Ascending then c else -c
-
| Some _, None -> if ord = `Ascending then 1 else -1
-
| None, Some _ -> if ord = `Ascending then -1 else 1
-
| None, None -> 0)
-
| other ->
-
let c = compare_events other ord i1.Recur.event i2.Recur.event in
-
if ord = `Ascending then c else -c
-
in
-
let sorted_instances =
-
match (sort_by, order) with
-
| Some criteria, Some ord ->
-
List.sort (compare_instances criteria ord) instances
-
| Some criteria, None ->
-
List.sort (compare_instances criteria `Ascending) instances
-
| None, _ -> List.sort (compare_instances `Start `Ascending) instances
-
in
+
let sorted_instances = List.sort comparator instances in
Ok
(match limit with
| Some n when n > 0 ->
+3 -15
lib/query.mli
···
(** Filter-based searching and querying of calendar events *)
-
type filter
-
(** Type representing a query filter *)
-
-
type sort_order = [ `Ascending | `Descending ]
-
(** Type representing the sort order *)
-
-
type sort_by = [ `Start | `End | `Summary | `Location | `Calendar ]
-
(** Type representing sort criteria *)
+
type filter = Event.t -> bool
val summary_contains : string -> filter
val description_contains : string -> filter
···
fs:[> Eio.Fs.dir_ty ] Eio.Path.t ->
Calendar_dir.t ->
?filter:filter ->
-
?sort_by:[< `Calendar | `End | `Location | `Start | `Summary ] ->
-
?order:[< `Ascending | `Descending ] ->
+
?comparator:Event.comparator ->
?limit:int ->
unit ->
(Event.t list, [> `Msg of string ]) result
···
?filter:filter ->
from:Ptime.t option ->
to_:Ptime.t ->
-
?sort_by:sort_by ->
-
?order:sort_order ->
+
?comparator:Recur.Instance.comparator ->
?limit:int ->
unit ->
(Recur.instance list, [> `Msg of string ]) result
···
(* Test-only helper functions *)
val matches_filter : Event.t -> filter -> bool
(** Check if an event matches the given filter *)
-
-
val compare_events : sort_by -> sort_order -> Event.t -> Event.t -> int
-
(** Compare two events based on the sort criteria and order *)
+17
lib/recur.ml
···
type instance = { event : Event.t; start : Ptime.t; end_ : Ptime.t option }
+
module Instance = struct
+
type t = instance
+
type comparator = t -> t -> int
+
+
let by_start i1 i2 = Ptime.compare i1.start i2.start
+
+
let by_end i1 i2 =
+
match (i1.end_, i2.end_) with
+
| Some t1, Some t2 -> Ptime.compare t1 t2
+
| Some _, None -> 1
+
| None, Some _ -> -1
+
| None, None -> 0
+
+
let by_event event_comp i1 i2 = event_comp i1.event i2.event
+
let descending comp i1 i2 = -1 * comp i1 i2
+
end
+
let clone_with_time original start =
let duration = Event.get_duration original in
let end_ =
+20
lib/recur.mli
···
type instance = { event : Event.t; start : Ptime.t; end_ : Ptime.t option }
(** Instances of recurring events with adjusted start/end times *)
+
module Instance : sig
+
type t = instance
+
+
type comparator = t -> t -> int
+
(** Instance comparator function type *)
+
+
val by_start : comparator
+
(** Compare instances by start time, earlier times come first *)
+
+
val by_end : comparator
+
(** Compare instances by end time, earlier times come first.
+
Instances with end times come after those without *)
+
+
val by_event : Event.comparator -> comparator
+
(** Apply an event comparator to instances *)
+
+
val descending : comparator -> comparator
+
(** Reverse the order of a comparator *)
+
end
+
val expand_event :
Event.t -> from:Ptime.t option -> to_:Ptime.t -> instance list
(** Generates all instances of an event within a date range, including the