Command-line and Emacs Calendar Client

better date parsing

Ryan Gibb 692888ae 2ccf4bc1

+3
TODO.org
···
- [ ] don't load all calendars into memory to show only one event
- [ ] support specifying duration
- [ ] better add/edit event datetime parsing
+
- day of the week
+
- month names
+
- allow editting date or time without touching the other
- [x] diagnose events failing to parse [[https://github.com/robur-coop/icalendar/issues/14]]
- [x] [[https://github.com/robur-coop/icalendar/pull/13][handle RECURRENCE-ID]]
- [x] [[https://github.com/robur-coop/icalendar/issues/15][RRULE with local datetime]]
+12 -3
bin/add_cmd.ml
···
in
let* end_ =
let end_date =
-
match end_date with None -> start_date | Some e -> Some e
+
(* if we have an endtime and no end date default to start date *)
+
match (end_date, end_time) with
+
| None, Some _ -> start_date
+
| _ -> end_date
+
in
+
let end_timezone =
+
(* if we specify and end date and time without a end timezone, default to the start timezone *)
+
match (end_date, end_time, end_timezone) with
+
| Some _, Some _, None -> timezone
+
| _ -> end_timezone
in
-
parse_end ~end_date ~end_time ~timezone ~end_timezone
+
parse_end ~end_date ~end_time ~end_timezone
in
let* recurrence =
match recur with
···
| None -> Ok None
in
let calendar_name = calendar_name in
-
let event =
+
let* event =
Event.create ~fs
~calendar_dir_path:(Calendar_dir.get_path calendar_dir)
~summary ~start ?end_ ?location ?description ?recurrence calendar_name
+12 -3
bin/edit_cmd.ml
···
let* start = parse_start ~start_date ~start_time ~timezone in
let* end_ =
let end_date =
-
match end_date with None -> start_date | Some e -> Some e
+
(* if we have an endtime and no end date default to start date *)
+
match (end_date, end_time) with
+
| None, Some _ -> start_date
+
| _ -> end_date
+
in
+
let end_timezone =
+
(* if we specify and end date and time without a end timezone, default to the start timezone *)
+
match (end_date, end_time, end_timezone) with
+
| Some _, Some _, None -> timezone
+
| _ -> end_timezone
in
-
parse_end ~end_date ~end_time ~timezone ~end_timezone
+
parse_end ~end_date ~end_time ~end_timezone
in
let* recurrence =
match recur with
···
Ok (Some p)
| None -> Ok None
in
-
let modifed_event =
+
let* modifed_event =
Event.edit ?summary ?start ?end_ ?location ?description ?recurrence event
in
let* _ = Calendar_dir.edit_event ~fs calendar_dir modifed_event in
+23 -25
bin/event_args.ml
···
match start_time with
| None -> Ok ()
| Some _ ->
-
Error (`Msg "Can't specify an start time without an end date")
+
Error (`Msg "Can't specify an start time without a start date")
in
let* _ =
match timezone with
···
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:start_date
~time:start_time `From
in
-
Ok (Some (Icalendar.Params.empty, `Datetime (`With_tzid (datetime, (false, tzid)))))
+
Ok
+
(Some
+
( Icalendar.Params.empty,
+
`Datetime (`With_tzid (datetime, (false, tzid))) ))
| Some "FLOATING" ->
let* datetime =
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:start_date
···
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:start_date
~time:start_time `From
in
-
Ok (Some (Icalendar.Params.empty, `Datetime (`With_tzid (datetime, (false, tzid)))))))
+
Ok
+
(Some
+
( Icalendar.Params.empty,
+
`Datetime (`With_tzid (datetime, (false, tzid))) ))))
-
let parse_end ~end_date ~end_time ~timezone ~end_timezone =
+
let parse_end ~end_date ~end_time ~end_timezone =
let ( let* ) = Result.bind in
-
let* _ =
-
match timezone with
-
| None -> Ok ()
-
| Some tzid -> (
-
match Timedesc.Time_zone.make tzid with
-
| Some _ -> Ok ()
-
| None ->
-
Error (`Msg (Printf.sprintf "Warning: Unknown timezone %s" tzid)))
-
in
let* _ =
match end_timezone with
| None -> Ok ()
···
let* _ =
match end_timezone with
| None -> Ok ()
-
| Some _ -> Error (`Msg "Can't specify a timezone without a end time")
+
| Some _ ->
+
Error (`Msg "Can't specify an end timezone without an end date")
in
Ok None
| Some end_date -> (
match end_time with
| None ->
let* _ =
-
match (timezone, end_timezone) with
-
| None, None -> Ok ()
-
| Some _, None ->
-
Error (`Msg "Can't specify a timezone without a end time")
-
| _ ->
+
match end_timezone with
+
| Some _ ->
Error (`Msg "Can't specify an end timezone without a end time")
+
| _ -> Ok ()
in
let* ptime =
Date.parse_date end_date ~tz:Timedesc.Time_zone.utc `From
···
(* DTEND;VALUE=DATE the event ends at the start of the specified date *)
let ptime = Date.add_days ptime 1 in
let date = Ptime.to_date ptime in
-
Ok (Some (`Dtend (Icalendar.Params.singleton Valuetype `Date, `Date date)))
+
Ok
+
(Some
+
(`Dtend (Icalendar.Params.singleton Valuetype `Date, `Date date)))
| Some end_time -> (
-
match (timezone, end_timezone) with
-
| None, None ->
+
match end_timezone with
+
| None ->
let* tzid =
match Timedesc.Time_zone.local () with
| Some tz -> Ok (Timedesc.Time_zone.name tz)
···
(`Dtend
( Icalendar.Params.empty,
`Datetime (`With_tzid (datetime, (false, tzid))) )))
-
| _, Some "FLOATING" | Some "FLOATING", None ->
+
| Some "FLOATING" ->
let* datetime =
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:end_date
~time:end_time `From
···
Ok
(Some
(`Dtend (Icalendar.Params.empty, `Datetime (`Local datetime))))
-
| _, Some "UTC" | Some "UTC", None ->
+
| Some "UTC" ->
let* datetime =
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:end_date
~time:end_time `From
···
Ok
(Some
(`Dtend (Icalendar.Params.empty, `Datetime (`Utc datetime))))
-
| _, Some tzid | Some tzid, _ ->
+
| Some tzid ->
let* datetime =
Date.parse_date_time ~tz:Timedesc.Time_zone.utc ~date:end_date
~time:end_time `From
+22 -5
lib/event.ml
···
Uuidm.to_string uuid
let default_prodid = `Prodid (Params.empty, "-//Freumh//Caledonia//EN")
+
let ( let* ) = Result.bind
let create ~(fs : Eio.Fs.dir_ty Eio.Path.t) ~calendar_dir_path ~summary ~start
?end_ ?location ?description ?recurrence calendar_name =
···
in
let dtstart = start in
let dtend_or_duration = end_ in
+
let* _ =
+
match (dtstart, dtend_or_duration) with
+
| (_, `Date _), Some (`Dtend (_, `Datetime _)) ->
+
Error (`Msg "If the start is a date the end must also be a date.")
+
| (_, `Datetime _), Some (`Dtend (_, `Date _)) ->
+
Error
+
(`Msg "If the start is a datetime the end must also be a datetime.")
+
| _ -> Ok ()
+
in
let rrule = Option.map (fun r -> (Params.empty, r)) recurrence in
let now = Ptime_clock.now () in
let props = [ `Summary (Params.empty, summary) ] in
···
let components = [ `Event event ] in
(props, components)
in
-
{ calendar_name; file; event; calendar }
+
Ok { calendar_name; file; event; calendar }
let edit ?summary ?start ?end_ ?location ?description ?recurrence t =
let now = Ptime_clock.now () in
let uid = t.event.uid in
-
let dtstart =
-
match start with None -> t.event.dtstart | Some s -> s
-
in
+
let dtstart = match start with None -> t.event.dtstart | Some s -> s in
let dtend_or_duration =
match end_ with None -> t.event.dtend_or_duration | Some _ -> end_
+
in
+
let* _ =
+
match (dtstart, dtend_or_duration) with
+
| (_, `Date _), Some (`Dtend (_, `Datetime _)) ->
+
Error (`Msg "If the start is a date the end must also be a date.")
+
| (_, `Datetime _), Some (`Dtend (_, `Date _)) ->
+
Error
+
(`Msg "If the start is a datetime the end must also be a datetime.")
+
| _ -> Ok ()
in
let rrule =
match recurrence with
···
let calendar_name = t.calendar_name in
let file = t.file in
let calendar = t.calendar in
-
{ calendar_name; file; event; calendar }
+
Ok { calendar_name; file; event; calendar }
let events_of_icalendar calendar_name ~file calendar =
let remove_dup_ids lst =
+2 -2
lib/event.mli
···
?description:string ->
?recurrence:Icalendar.recurrence ->
string ->
-
t
+
(t, [> `Msg of string ]) result
(** Create a new event with required properties.
The start and end times can be specified as Icalendar.timestamp values,
···
?description:string ->
?recurrence:Icalendar.recurrence ->
t ->
-
t
+
(t, [> `Msg of string ]) result
(** Edit an existing event. *)
val events_of_icalendar :
+22 -17
test/test_query.ml
···
in
[
(* Event with text in all fields *)
-
create_test_event ~calendar_name:"search_test" ~summary:"Project Meeting"
-
~description:"Weekly project status meeting with team"
-
~location:"Conference Room A"
-
~start:(`Datetime (`Utc fixed_date));
+
Result.get_ok
+
@@ create_test_event ~calendar_name:"search_test" ~summary:"Project Meeting"
+
~description:"Weekly project status meeting with team"
+
~location:"Conference Room A"
+
~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
(* Event with mixed case to test case insensitivity *)
-
create_test_event ~calendar_name:"search_test" ~summary:"IMPORTANT Meeting"
-
~description:"Critical project review with stakeholders"
-
~location:"Executive Suite"
-
~start:(`Datetime (`Utc fixed_date));
+
Result.get_ok
+
@@ create_test_event ~calendar_name:"search_test"
+
~summary:"IMPORTANT Meeting"
+
~description:"Critical project review with stakeholders"
+
~location:"Executive Suite"
+
~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
(* Event with word fragments *)
-
create_test_event ~calendar_name:"search_test" ~summary:"Conference Call"
-
~description:"International conference preparation"
-
~location:"Remote Meeting Room"
-
~start:(`Datetime (`Utc fixed_date));
+
Result.get_ok
+
@@ create_test_event ~calendar_name:"search_test" ~summary:"Conference Call"
+
~description:"International conference preparation"
+
~location:"Remote Meeting Room"
+
~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
(* Event with unique text in each field *)
-
create_test_event ~calendar_name:"search_test"
-
~summary:"Workshop on Testing"
-
~description:"Quality Assurance techniques and practices"
-
~location:"Training Center"
-
~start:(`Datetime (`Utc fixed_date));
+
Result.get_ok
+
@@ create_test_event ~calendar_name:"search_test"
+
~summary:"Workshop on Testing"
+
~description:"Quality Assurance techniques and practices"
+
~location:"Training Center"
+
~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
]
(* Test helper to verify if a list of events contains an event with a given summary *)