Command-line and Emacs Calendar Client

better date formats and help pages

Ryan Gibb 794a1d15 e81dfdf7

+26 -59
bin/add_cmd.ml
···
`P
"Specify the event summary (title) as the first argument, and use \
options to set other details.";
+
`S Manpage.s_examples;
+
`I
+
( "Add a event for today:",
+
"caled add \"Meeting\" --date today --time 14:00" );
+
`I
+
( "Add an event with a specific date and time:",
+
"caled add \"Dentist Appointment\" --date 2025-04-15 --time 10:30" );
+
`I
+
( "Add an event with an end time:",
+
"caled add \"Conference\" --date 2025-05-20 --time 09:00 --end-date \
+
2025-05-22 --end-time 17:00" );
+
`I
+
( "Add an event with location and description:",
+
"caled add \"Lunch with Bob\" --date 2025-04-02 --time 12:30 \
+
--location \"Pasta Restaurant\" --description \"Discuss project \
+
plans\"" );
+
`I
+
( "Add an event to a specific calendar:",
+
"caled add \"Work Meeting\" --date 2025-04-03 --time 15:00 \
+
--calendar work" );
`S Manpage.s_options;
]
-
@ date_format_manpage_entries
-
@ [
-
`S Manpage.s_examples;
-
`I
-
( "Add a event for today:",
-
"caled add \"Meeting\" --date today --time 14:00" );
-
`I
-
( "Add an event with a specific date and time:",
-
"caled add \"Dentist Appointment\" --date 2025-04-15 --time 10:30"
-
);
-
`I
-
( "Add an event with an end time:",
-
"caled add \"Conference\" --date 2025-05-20 --time 09:00 \
-
--end-date 2025-05-22 --end-time 17:00" );
-
`I
-
( "Add an event with location and description:",
-
"caled add \"Lunch with Bob\" --date 2025-04-02 --time 12:30 \
-
--location \"Pasta Restaurant\" --description \"Discuss project \
-
plans\"" );
-
`I
-
( "Add an event to a specific calendar:",
-
"caled add \"Work Meeting\" --date 2025-04-03 --time 15:00 \
-
--calendar work" );
-
`S "RECURRENCE";
-
`P
-
"Recurrence rule in iCalendar RFC5545 format. The FREQ part is \
-
required.";
-
`I ("FREQ=<frequency>", "DAILY, WEEKLY, MONTHLY, or YEARLY (required)");
-
`I
-
( "COUNT=<number>",
-
"Limit to this many occurrences (optional, cannot be used with \
-
UNTIL)" );
-
`I
-
( "UNTIL=<date>",
-
"Repeat until this date (optional, cannot be used with COUNT)" );
-
`I
-
( "INTERVAL=<number>",
-
"Interval between occurrences, e.g., 2 for every other (optional)"
-
);
-
`I
-
( "BYDAY=<dayspec>",
-
"Specific days, e.g., MO,WE,FR or 1MO (first Monday) (optional)" );
-
`I
-
( "BYMONTHDAY=<daynum>",
-
"Day of month, e.g., 1,15 or -1 (last day) (optional)" );
-
`I
-
( "BYMONTH=<monthnum>",
-
"Month number, e.g., 1,6,12 for Jan,Jun,Dec (optional)" );
-
`P "Examples:";
-
`I ("FREQ=DAILY;COUNT=5", "Daily for 5 occurrences");
-
`I ("FREQ=WEEKLY;INTERVAL=2", "Every other week indefinitely");
-
`I ("FREQ=WEEKLY;BYDAY=MO,WE,FR", "Every Monday, Wednesday, Friday");
-
`I ("FREQ=MONTHLY;BYDAY=1MO", "First Monday of every month");
-
`I
-
( "FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1",
-
"Every January 1st (New Year's Day)" );
-
`I ("FREQ=MONTHLY;BYMONTHDAY=-1", "Last day of every month");
-
]
+
@ date_format_manpage_entries @ recurrence_format_manpage_entries
+
@ [ `S Manpage.s_see_also ]
+
in
+
let exit_info =
+
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
in
-
let info = Cmd.info "add" ~doc ~man in
+
let info = Cmd.info "add" ~doc ~man ~exits:exit_info in
Cmd.v info term
+6 -1
bin/delete_cmd.ml
···
`S Manpage.s_examples;
`P "Delete an event:";
`P " caled delete 12345678-1234-5678-1234-567812345678";
+
`S Manpage.s_options;
]
+
@ [ `S Manpage.s_see_also ]
in
-
let info = Cmd.info "delete" ~doc ~man in
+
let exit_info =
+
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
+
in
+
let info = Cmd.info "delete" ~doc ~man ~exits:exit_info in
Cmd.v info term
+23 -21
bin/edit_cmd.ml
···
`P
"Specify the event ID as the first argument, and use options to change \
event details.";
+
`S Manpage.s_examples;
+
`I
+
( "Change the summary of an event:",
+
"caled edit 12345678-1234-5678-1234-567812345678 --summary \"New \
+
Title\"" );
+
`I
+
( "Change the date and time:",
+
"caled edit 12345678-1234-5678-1234-567812345678 --date 2025-05-01 \
+
--time 15:30" );
+
`I
+
( "Update the location:",
+
"caled edit 12345678-1234-5678-1234-567812345678 --location \
+
\"Conference Room B\"" );
+
`I
+
( "Change the description:",
+
"caled edit 12345678-1234-5678-1234-567812345678 --description \
+
\"Updated agenda for the meeting\"" );
`S Manpage.s_options;
]
-
@ date_format_manpage_entries
-
@ [
-
`S Manpage.s_examples;
-
`I
-
( "Change the summary of an event:",
-
"caled edit 12345678-1234-5678-1234-567812345678 --summary \"New \
-
Title\"" );
-
`I
-
( "Change the date and time:",
-
"caled edit 12345678-1234-5678-1234-567812345678 --date 2025-05-01 \
-
--time 15:30" );
-
`I
-
( "Update the location:",
-
"caled edit 12345678-1234-5678-1234-567812345678 --location \
-
\"Conference Room B\"" );
-
`I
-
( "Change the description:",
-
"caled edit 12345678-1234-5678-1234-567812345678 --description \
-
\"Updated agenda for the meeting\"" );
-
]
+
@ date_format_manpage_entries @ recurrence_format_manpage_entries
+
@ [ `S Manpage.s_see_also ]
+
in
+
let exit_info =
+
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
in
-
let info = Cmd.info "edit" ~doc ~man in
+
let info = Cmd.info "edit" ~doc ~man ~exits:exit_info in
Cmd.v info term
+47 -5
bin/event_args.ml
···
let date_format_manpage_entries =
[
`S "DATE FORMATS";
-
`P "Relative date formats for --date / -d and --end-date / -e:";
+
`P
+
"The following are the possible date formats for the --date and \
+
--end-date command line parameters. Note the value is dependent on \
+
--date / --end-date, so --date 2025-03 --end-date 2025-03 will span the \
+
month of March.";
+
`I ("YYYY-MM-DD", "Specific date (e.g., 2025-3-27, zero-padding optional)");
+
`I ("YYYY-MM", "Start/end of specific month (e.g., 2025-3 for March 2025)");
+
`I ("YYYY", "Start/end of specific year (e.g., 2025)");
`I ("today", "Current day");
`I ("tomorrow", "Next day");
`I ("yesterday", "Previous day");
-
`I ("this-week", "Start of current week");
-
`I ("next-week", "Start of next week");
-
`I ("this-month", "Start of current month");
-
`I ("next-month", "Start of next month");
+
`I ("this-week", "Start/end of current week");
+
`I ("next-week", "Start/end of next week");
+
`I ("this-month", "Start/end of current month");
+
`I ("next-month", "Start/end of next month");
`I ("+Nd", "N days from today (e.g., +7d for a week from today)");
`I ("-Nd", "N days before today (e.g., -7d for a week ago)");
`I ("+Nw", "N weeks from today (e.g., +4w for 4 weeks from today)");
···
let recurrence = (f, limit, !interval, !by_parts) in
Ok recurrence
| None -> Error (`Msg "FREQ is required in recurrence rule")
+
+
let recurrence_format_manpage_entries =
+
[
+
`S "RECURRENCE";
+
`P "Recurrence rule in iCalendar RFC5545 format. The FREQ part is required.";
+
`I ("FREQ=<frequency>", "DAILY, WEEKLY, MONTHLY, or YEARLY (required)");
+
`I
+
( "COUNT=<number>",
+
"Limit to this many occurrences (optional, cannot be used with UNTIL)"
+
);
+
`I
+
( "UNTIL=<date>",
+
"Repeat until this date (optional, cannot be used with COUNT)" );
+
`I
+
( "INTERVAL=<number>",
+
"Interval between occurrences, e.g., 2 for every other (optional)" );
+
`I
+
( "BYDAY=<dayspec>",
+
"Specific days, e.g., MO,WE,FR or 1MO (first Monday) (optional)" );
+
`I
+
( "BYMONTHDAY=<daynum>",
+
"Day of month, e.g., 1,15 or -1 (last day) (optional)" );
+
`I
+
( "BYMONTH=<monthnum>",
+
"Month number, e.g., 1,6,12 for Jan,Jun,Dec (optional)" );
+
`P "Examples:";
+
`I ("FREQ=DAILY;COUNT=5", "Daily for 5 occurrences");
+
`I ("FREQ=WEEKLY;INTERVAL=2", "Every other week indefinitely");
+
`I ("FREQ=WEEKLY;BYDAY=MO,WE,FR", "Every Monday, Wednesday, Friday");
+
`I ("FREQ=MONTHLY;BYDAY=1MO", "First Monday of every month");
+
`I
+
( "FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1",
+
"Every January 1st (New Year's Day)" );
+
`I ("FREQ=MONTHLY;BYMONTHDAY=-1", "Last day of every month");
+
]
+31 -33
bin/list_cmd.ml
···
open Caledonia_lib
open Query_args
-
let run ?from_str ?to_str ?calendar ?count ~format ~today ~tomorrow ~week ~month
-
?timezone ~sort ~fs calendar_dir =
+
let run ?from_str ?to_str ~calendar:calendars ?count ~format ~today ~tomorrow
+
~week ~month ?timezone ~sort ~fs calendar_dir =
let ( let* ) = Result.bind in
let tz = Query_args.parse_timezone ~timezone in
let* from, to_ =
···
Ok (Some today_date, one_month_later))
in
let filter =
-
match calendar with
-
| Some calendar_name -> Some (Query.in_calendar_names [ calendar_name ])
-
| None -> None
+
match calendars with
+
| [] -> None
+
| calendar -> Some (Query.in_calendars calendar)
in
let comparator = Query_args.create_event_comparator sort in
let* results =
···
Ok ()
let cmd ~fs calendar_dir =
-
let run from_str to_str calendar count format today tomorrow week month
+
let run from_str to_str calendars count format today tomorrow week month
timezone sort =
match
-
run ?from_str ?to_str ?calendar ?count ~format ~today ~tomorrow ~week
-
~month ?timezone ~sort ~fs calendar_dir
+
run ?from_str ?to_str ~calendar:calendars ?count ~format ~today ~tomorrow
+
~week ~month ?timezone ~sort ~fs calendar_dir
with
| Error (`Msg msg) ->
Printf.eprintf "Error: %s\n%!" msg;
···
let man =
[
`S Manpage.s_description;
-
`P "List calendar events within a specified date range.";
-
`P "By default, events from today to one month from today are shown.";
-
`P "You can use date flags to show events for a specific time period.";
-
`P "You can also filter events by calendar using the --calendar flag.";
-
`P "Use the --sort option to control the sorting of results.";
+
`P
+
"List calendar events within a specified date range. By default, \
+
events from today to one month from today are shown. You can use date \
+
flags to show events for a specific time period, and filter events \
+
with the --sort option.";
+
`S Manpage.s_examples;
+
`I ("List all events for today:", "caled list --today");
+
`I ("List all events for tomorrow:", "caled list --tomorrow");
+
`I ("List all events for the current week:", "caled list --week");
+
`I ("List all events for the current month:", "caled list --month");
+
`I
+
( "List events within a specific date range:",
+
"caled list --from 2025-03-27 --to 2025-04-01" );
+
`I ("List events from a specific calendar:", "caled list --calendar work");
+
`I ("List events in JSON format:", "caled list --format json");
+
`I ("Limit the number of events shown:", "caled list --count 5");
+
`I
+
( "Sort by multiple fields (start time and summary):",
+
"caled list --sort start --sort summary" );
+
`I
+
( "Sort by calendar name in descending order:",
+
"caled list --sort calendar:desc" );
`S Manpage.s_options;
]
@ date_format_manpage_entries
-
@ [
-
`S Manpage.s_examples;
-
`I ("List all events for today:", "caled list --today");
-
`I ("List all events for tomorrow:", "caled list --tomorrow");
-
`I ("List all events for the current week:", "caled list --week");
-
`I ("List all events for the current month:", "caled list --month");
-
`I
-
( "List events within a specific date range:",
-
"caled list --from 2025-03-27 --to 2025-04-01" );
-
`I
-
("List events from a specific calendar:", "caled list --calendar work");
-
`I ("List events in JSON format:", "caled list --format json");
-
`I ("Limit the number of events shown:", "caled list --count 5");
-
`I
-
( "Sort by multiple fields (start time and summary):",
-
"caled list --sort start --sort summary" );
-
`I
-
( "Sort by calendar name in descending order:",
-
"caled list --sort calendar:desc" );
-
]
+
@ [ `S Manpage.s_see_also ]
in
let exit_info =
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
+27 -31
bin/query_args.ml
···
let from_arg =
let doc =
-
"Start date in YYYY-MM-DD format or a relative expression (today, \
-
tomorrow, this-week, next-week, this-month, next-month, +Nd, -Nd, +Nw, \
-
+Nm)"
+
"Start date in YYYY-MM-DD format, partial date format (YYYY-MM or YYYY), \
+
or a relative expression (today, tomorrow, this-week, next-week, \
+
this-month, next-month, +Nd, -Nd, +Nw, +Nm). See DATE FORMATS for more."
in
let i = Arg.info [ "from"; "f" ] ~docv:"DATE" ~doc in
Arg.(value @@ opt (some string) None i)
let to_arg =
let doc =
-
"End date in YYYY-MM-DD format or a relative expression (today, tomorrow, \
-
this-week, next-week, this-month, next-month, +Nd, -Nd, +Nw, +Nm)"
+
"End date in YYYY-MM-DD format, partial date format (YYYY-MM or YYYY), or \
+
a relative expression (today, tomorrow, this-week, next-week, this-month, \
+
next-month, +Nd, -Nd, +Nw, +Nm). See DATE FORMATS for more."
in
let i = Arg.info [ "to"; "t" ] ~docv:"DATE" ~doc in
Arg.(value @@ opt (some string) None i)
let calendar_arg =
-
let doc = "Calendar to filter by" in
+
let doc = "Filter by calendar" in
Arg.(
-
value
-
& opt (some string) None
-
& info [ "calendar"; "c" ] ~docv:"CALENDAR" ~doc)
+
value & opt_all string [] & info [ "calendar"; "c" ] ~docv:"CALENDAR" ~doc)
let format_enum =
[
···
in
if spec.descending then Event.descending comp else comp)
+
let parse_timezone ~timezone =
+
match timezone with
+
| Some tzid -> (
+
match Timedesc.Time_zone.make tzid with
+
| Some tz -> tz
+
| None -> failwith ("Invalid timezone: " ^ tzid))
+
| None -> !Date.default_timezone ()
+
let date_format_manpage_entries =
[
`S "DATE FORMATS";
-
`P "Date format flags:";
-
`I ("--today, -d", "Show events for today only");
-
`I ("--tomorrow", "Show events for tomorrow only");
-
`I ("--week, -w", "Show events for the current week");
-
`I ("--month, -m", "Show events for the current month");
-
`I
-
( "--timezone, -z",
-
"Timezone to use for date calculations (e.g., 'America/New_York', \
-
'UTC')" );
-
`P "Relative date formats for --from and --to:";
+
`P
+
"The following are the possible date formats for the --from and --to \
+
command line parameters. Note the value is dependent on --from / --to, \
+
so --from 2025 --to 2025 will include all the events in the year 2025";
+
`I ("YYYY-MM-DD", "Specific date (e.g., 2025-3-27, zero-padding optional)");
+
`I ("YYYY-MM", "Start/end of specific month (e.g., 2025-3 for March 2025)");
+
`I ("YYYY", "Start/end of specific year (e.g., 2025)");
`I ("today", "Current day");
`I ("tomorrow", "Next day");
`I ("yesterday", "Previous day");
-
`I ("this-week", "Start of current week");
-
`I ("next-week", "Start of next week");
-
`I ("this-month", "Start of current month");
-
`I ("next-month", "Start of next month");
+
`I ("this-week", "Start/end of current week");
+
`I ("next-week", "Start/end of next week");
+
`I ("this-month", "Start/end of current month");
+
`I ("next-month", "Start/end of next month");
`I ("+Nd", "N days from today (e.g., +7d for a week from today)");
`I ("-Nd", "N days before today (e.g., -7d for a week ago)");
`I ("+Nw", "N weeks from today (e.g., +4w for 4 weeks from today)");
`I ("+Nm", "N months from today (e.g., +2m for 2 months from today)");
]
-
-
let parse_timezone ~timezone =
-
match timezone with
-
| Some tzid -> (
-
match Timedesc.Time_zone.make tzid with
-
| Some tz -> tz
-
| None -> failwith ("Invalid timezone: " ^ tzid))
-
| None -> !Date.default_timezone ()
+51 -63
bin/search_cmd.ml
···
open Caledonia_lib
open Query_args
-
let run ?from_str ?to_str ?calendar ?count ?query_text ~summary ~description
+
let run ?from_str ?to_str ~calendar ?count ?query_text ~summary ~description
~location ~id ~format ~today ~tomorrow ~week ~month ~recurring
~non_recurring ?timezone ~sort ~fs calendar_dir =
let ( let* ) = Result.bind in
···
| None, None -> Ok (None, Date.to_end_of_day max_date))
in
(match calendar with
-
| Some calendar_name ->
-
filters := Query.in_calendar_names [ calendar_name ] :: !filters
-
| None -> ());
+
| [] -> ()
+
| calendars -> filters := Query.in_calendars calendars :: !filters);
(match query_text with
| Some text ->
if summary then filters := Query.summary_contains text :: !filters;
···
Ok ()
let query_text_arg =
-
let doc = "Text to search for in events (summary, description, location)" in
+
let doc =
+
"Text to search for in the fields summary, description, and/or location."
+
in
Arg.(value & pos 0 (some string) None & info [] ~docv:"TEXT" ~doc)
let summary_arg =
···
Arg.(value & opt (some string) None & info [ "id"; "i" ] ~docv:"ID" ~doc)
let cmd ~fs calendar_dir =
-
let run query_text from_str to_str calendar count format summary description
+
let run query_text from_str to_str calendars count format summary description
location id today tomorrow week month recurring non_recurring timezone
sort =
match
-
run ?from_str ?to_str ?calendar ?count ?query_text ~summary ~description
-
~location ~id ~format ~today ~tomorrow ~week ~month ~recurring
-
~non_recurring ?timezone ~sort ~fs calendar_dir
+
run ?from_str ?to_str ~calendar:calendars ?count ?query_text ~summary
+
~description ~location ~id ~format ~today ~tomorrow ~week ~month
+
~recurring ~non_recurring ?timezone ~sort ~fs calendar_dir
with
| Error (`Msg msg) ->
Printf.eprintf "Error: %s\n%!" msg;
···
`S Manpage.s_description;
`P
"Search calendar events for text in summary, description, or location \
-
fields.";
-
`P
-
"By default, the search looks across all text fields in all events \
-
regardless of date.";
-
`P
-
"You can narrow the search to a specific date range with date flags or \
-
--from and --to.";
-
`P
-
"You can specify specific fields to search in using the --summary, \
-
--description, or --location flags.";
-
`P
-
"You can limit results to only recurring or non-recurring events using \
-
the --recurring or --non-recurring flags.";
-
`P "Use the --sort option to control the sorting of results.";
-
`P
-
"The search text is optional if you're using other filters. For \
-
example, you can find all recurring events without specifying any \
-
search text.";
+
fields. By default, the search looks across all text fields in all \
+
events regardless of date. You can specify specific fields to search \
+
in using the --summary, --description, or --location flags. You can \
+
use date flags to show events for a specific time period, and filter \
+
events with the --sort option.\n\
+
\ The search text is optional, so you search events according \
+
to other query criteria.";
+
`S Manpage.s_examples;
+
`I ("Search for 'meeting' in all events:", "caled search meeting");
+
`I
+
( "Search for 'interview' in event summaries only:",
+
"caled search --summary interview" );
+
`I
+
( "Search for 'conference' in a specific calendar:",
+
"caled search --calendar work conference" );
+
`I
+
( "Search for 'workshop' in event descriptions for today only:",
+
"caled search --description --today workshop" );
+
`I
+
( "Search for 'project' in events this month:",
+
"caled search --month project" );
+
`I
+
( "Search for 'workshop' in event descriptions within a date range:",
+
"caled search --description --from 2025-03-27 --to 2025-04-01 \
+
workshop" );
+
`I
+
("Search for recurring events only:", "caled search --recurring meeting");
+
`I
+
( "Search for non-recurring events only:",
+
"caled search --non-recurring appointment" );
+
`I ("Find all recurring events:", "caled search --recurring");
+
`I
+
( "Find all events in a specific calendar:",
+
"caled search --calendar work" );
+
`I
+
( "Sort results by location and then summary:",
+
"caled search --sort location --sort summary" );
+
`I
+
( "Sort results by end time in descending order:",
+
"caled search --sort end:desc" );
`S Manpage.s_options;
]
@ date_format_manpage_entries
-
@ [
-
`S Manpage.s_examples;
-
`I ("Search for 'meeting' in all events:", "caled search meeting");
-
`I
-
( "Search for 'interview' in event summaries only:",
-
"caled search --summary interview" );
-
`I
-
( "Search for 'conference' in a specific calendar:",
-
"caled search --calendar work conference" );
-
`I
-
( "Search for 'workshop' in event descriptions for today only:",
-
"caled search --description --today workshop" );
-
`I
-
( "Search for 'project' in events this month:",
-
"caled search --month project" );
-
`I
-
( "Search for 'workshop' in event descriptions within a date range:",
-
"caled search --description --from 2025-03-27 --to 2025-04-01 \
-
workshop" );
-
`I
-
( "Search for recurring events only:",
-
"caled search --recurring meeting" );
-
`I
-
( "Search for non-recurring events only:",
-
"caled search --non-recurring appointment" );
-
`I ("Find all recurring events:", "caled search --recurring");
-
`I
-
( "Find all events in a specific calendar:",
-
"caled search --calendar work" );
-
`I
-
( "Sort results by location and then summary:",
-
"caled search --sort location --sort summary" );
-
`I
-
( "Sort results by end time in descending order:",
-
"caled search --sort end:desc" );
-
]
+
@ [ `S Manpage.s_see_also ]
in
let exit_info =
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
+6 -5
bin/show_cmd.ml
···
[
`S Manpage.s_description;
`P "Show detailed information about a specific event by its ID.";
-
`P
-
"You can find event IDs by using the `list` or `search` commands with \
-
the `id` output using `-o id`.";
-
`S Manpage.s_options;
+
`P "You can find event IDs by using the `list` or `search` commands.";
`S Manpage.s_examples;
`P "Show event details:";
`P " caled show 12345678-1234-5678-1234-567812345678";
`P "Show event details in JSON format:";
`P " caled show 12345678-1234-5678-1234-567812345678 --format json";
+
`S Manpage.s_options;
]
in
-
let info = Cmd.info "show" ~doc ~man in
+
let exit_info =
+
[ Cmd.Exit.info ~doc:"on success." 0; Cmd.Exit.info ~doc:"on error." 1 ]
+
in
+
let info = Cmd.info "show" ~doc ~man ~exits:exit_info in
Cmd.v info term
+110 -55
lib/date.ml
···
let ( let* ) = Result.bind
-
(* Parse a date string that could be ISO format or a relative expression *)
+
let parse_full_iso_datet ~tz expr =
+
let regex = Re.Pcre.regexp "^(\\d{4})-(\\d{1,2})-(\\d{1,2})$" in
+
if Re.Pcre.pmatch ~rex:regex expr then
+
let match_result = Re.Pcre.exec ~rex:regex expr in
+
let year = int_of_string (Re.Pcre.get_substring match_result 1) in
+
let month = int_of_string (Re.Pcre.get_substring match_result 2) in
+
let day = int_of_string (Re.Pcre.get_substring match_result 3) in
+
match Timedesc.Date.Ymd.make ~year ~month ~day with
+
| Ok date ->
+
let midnight = Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 () in
+
let dt = Timedesc.of_date_and_time_exn ~tz date midnight in
+
Some (Ok (timedesc_to_ptime dt))
+
| Error _ -> Some (Error (`Msg (Printf.sprintf "Invalid date: %s" expr)))
+
else None
+
+
let parse_year_only ~tz expr parameter =
+
let regex = Re.Pcre.regexp "^(\\d{4})$" in
+
if Re.Pcre.pmatch ~rex:regex expr then
+
let match_result = Re.Pcre.exec ~rex:regex expr in
+
let year = int_of_string (Re.Pcre.get_substring match_result 1) in
+
match parameter with
+
| `From -> (
+
match Timedesc.Date.Ymd.make ~year ~month:1 ~day:1 with
+
| Ok date ->
+
let time = Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 () in
+
let dt = Timedesc.of_date_and_time_exn ~tz date time in
+
Some (Ok (timedesc_to_ptime dt))
+
| Error _ ->
+
Some (Error (`Msg (Printf.sprintf "Invalid year: %s" expr))))
+
| `To -> (
+
match Timedesc.Date.Ymd.make ~year ~month:12 ~day:31 with
+
| Ok date ->
+
let time =
+
Timedesc.Time.make_exn ~hour:23 ~minute:59 ~second:59 ()
+
in
+
let dt = Timedesc.of_date_and_time_exn ~tz date time in
+
Some (Ok (timedesc_to_ptime dt))
+
| Error _ ->
+
Some (Error (`Msg (Printf.sprintf "Invalid year: %s" expr))))
+
else None
+
+
let parse_year_month ~tz expr parameter =
+
let regex = Re.Pcre.regexp "^(\\d{4})-(\\d{1,2})$" in
+
if Re.Pcre.pmatch ~rex:regex expr then
+
let match_result = Re.Pcre.exec ~rex:regex expr in
+
let year = int_of_string (Re.Pcre.get_substring match_result 1) in
+
let month = int_of_string (Re.Pcre.get_substring match_result 2) in
+
match parameter with
+
| `From -> (
+
match Timedesc.Date.Ymd.make ~year ~month ~day:1 with
+
| Ok date ->
+
let time = Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 () in
+
let dt = Timedesc.of_date_and_time_exn ~tz date time in
+
Some (Ok (timedesc_to_ptime dt))
+
| Error _ ->
+
Some (Error (`Msg (Printf.sprintf "Invalid year-month: %s" expr))))
+
| `To -> (
+
let next_month = if month = 12 then 1 else month + 1 in
+
let next_month_year = if month = 12 then year + 1 else year in
+
match
+
Timedesc.Date.Ymd.make ~year:next_month_year ~month:next_month ~day:1
+
with
+
| Ok next_month_date ->
+
let last_day_of_month = Timedesc.Date.sub ~days:1 next_month_date in
+
let end_of_day =
+
Timedesc.Time.make_exn ~hour:23 ~minute:59 ~second:59 ()
+
in
+
let dt =
+
Timedesc.of_date_and_time_exn ~tz last_day_of_month end_of_day
+
in
+
Some (Ok (timedesc_to_ptime dt))
+
| Error _ ->
+
Some (Error (`Msg (Printf.sprintf "Invalid year-month: %s" expr))))
+
else None
+
+
let parse_relative ~tz expr parameter =
+
let regex = Re.Pcre.regexp "^([+-])(\\d+)([dwm])$" in
+
if Re.Pcre.pmatch ~rex:regex expr then
+
let match_result = Re.Pcre.exec ~rex:regex expr in
+
let sign = Re.Pcre.get_substring match_result 1 in
+
let num = int_of_string (Re.Pcre.get_substring match_result 2) in
+
let unit = Re.Pcre.get_substring match_result 3 in
+
let multiplier = if sign = "+" then 1 else -1 in
+
let value = num * multiplier in
+
let today = !get_today ~tz () in
+
match unit with
+
| "d" -> Some (Ok (add_days today value))
+
| "w" -> (
+
let date = add_weeks today value in
+
match parameter with
+
| `From -> Some (Ok (get_start_of_week date))
+
| `To -> Some (Ok (get_end_of_week date)))
+
| "m" -> (
+
let date = add_months today value in
+
match parameter with
+
| `From -> Some (Ok (get_start_of_month date))
+
| `To -> Some (Ok (get_end_of_month date)))
+
| _ -> Some (Error (`Msg (Printf.sprintf "Invalid date unit: %s" unit)))
+
else None
+
let parse_date ?(tz = !default_timezone ()) expr parameter =
-
let iso_date_regex = Re.Pcre.regexp "^(\\d{4})-(\\d{2})-(\\d{2})$" in
-
let relative_regex = Re.Pcre.regexp "^([+-])(\\d+)([dwm])$" in
match expr with
| "today" -> Ok (!get_today ~tz ())
| "tomorrow" -> Ok (add_days (!get_today ~tz ()) 1)
···
match parameter with
| `From -> Ok (get_start_of_next_month ~tz ())
| `To -> Ok (get_end_of_next_month ~tz ()))
-
| _ ->
-
(* Try to parse as ISO date *)
-
if Re.Pcre.pmatch ~rex:iso_date_regex expr then
-
let year =
-
int_of_string
-
(Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 1)
-
in
-
let month =
-
int_of_string
-
(Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 2)
-
in
-
let day =
-
int_of_string
-
(Re.Pcre.get_substring (Re.Pcre.exec ~rex:iso_date_regex expr) 3)
-
in
-
match Timedesc.Date.Ymd.make ~year ~month ~day with
-
| Ok date ->
-
let midnight =
-
Timedesc.Time.make_exn ~hour:0 ~minute:0 ~second:0 ()
-
in
-
let dt = Timedesc.of_date_and_time_exn ~tz date midnight in
-
Ok (timedesc_to_ptime dt)
-
| Error _ -> Error (`Msg (Printf.sprintf "Invalid date: %s" expr))
-
(* Try to parse as relative expression +Nd, -Nd, etc. *)
-
else if Re.Pcre.pmatch ~rex:relative_regex expr then
-
let sign =
-
Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 1
-
in
-
let num =
-
int_of_string
-
(Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 2)
-
in
-
let unit =
-
Re.Pcre.get_substring (Re.Pcre.exec ~rex:relative_regex expr) 3
-
in
-
let multiplier = if sign = "+" then 1 else -1 in
-
let value = num * multiplier in
-
let today = !get_today ~tz () in
-
match unit with
-
| "d" -> Ok (add_days today value)
-
| "w" -> (
-
let date = add_weeks today value in
-
match parameter with
-
| `From -> Ok (get_start_of_week date)
-
| `To -> Ok (get_end_of_week date))
-
| "m" -> (
-
let date = add_months today value in
-
match parameter with
-
| `From -> Ok (get_start_of_month date)
-
| `To -> Ok (get_end_of_month date))
-
| _ -> Error (`Msg (Printf.sprintf "Invalid date unit: %s" unit))
-
else Error (`Msg (Printf.sprintf "Invalid date format: %s" expr))
+
| _ -> (
+
(* Option alternative operator *)
+
let ( |>? ) opt f = match opt with None -> f () | Some x -> Some x in
+
( ( ( parse_full_iso_datet ~tz expr |>? fun () ->
+
parse_year_only ~tz expr parameter )
+
|>? fun () -> parse_year_month ~tz expr parameter )
+
|>? fun () -> parse_relative ~tz expr parameter )
+
|> function
+
| Some result -> result
+
| None -> Error (`Msg (Printf.sprintf "Invalid date format: %s" expr)))
let parse_time str =
try
+8 -1
lib/date.mli
···
default_timezone.
Supported formats:
-
- ISO format: "YYYY-MM-DD"
+
- ISO format:
+
- "YYYY-MM-DD" (full date)
+
- "YYYY-MM" (partial date)
+
- For --from: defaults to first day of month
+
- For --to: defaults to last day of month
+
- "YYYY" (partial date)
+
- For --from: defaults to January 1st of year
+
- For --to: defaults to December 31st of year
- Relative expressions:
- "today" - Current day
- "tomorrow" - Next day
+1 -1
lib/query.ml
···
| Some loc -> text_matches text loc
| None -> false
-
let in_calendar_names ids event =
+
let in_calendars ids event =
let id = Event.get_calendar_name event in
List.exists (fun col -> col = id) ids
+1 -1
lib/query.mli
···
val summary_contains : string -> filter
val description_contains : string -> filter
val location_contains : string -> filter
-
val in_calendar_names : string list -> filter
+
val in_calendars : string list -> filter
val recurring_only : unit -> filter
val non_recurring_only : unit -> filter
val with_id : Event.event_id -> filter
+6
test/test_date.ml
···
test_expr "+1m" `To "2025-04-30";
test_expr "2025-01-01" `From "2025-01-01";
test_expr "2025-01-01" `To "2025-01-01";
+
test_expr "2025-01" `From "2025-01-01";
+
test_expr "2025-01" `To "2025-01-01";
+
test_expr "2025" `From "2025-01-01";
+
test_expr "2025" `To "2025-01-01";
+
test_expr "2025-3-1" `From "2025-03-01";
+
test_expr "2025-3-1" `To "2025-03-01";
(try
let _ = Query.parse_date "invalid-format" `From in
Alcotest.fail "Should have raised an exception for invalid format"
+3 -3
test/test_query.ml
···
in
let to_ = Option.get @@ Ptime.of_date_time ((2026, 01, 01), ((0, 0, 0), 0)) in
let calendar_name = "example" in
-
let filter = Query.in_calendar_names [ calendar_name ] in
+
let filter = Query.in_calendars [ calendar_name ] in
(match Query.query ~fs calendar_dir ~from ~to_ ~filter () with
| Ok events ->
let all_match_calendar =
···
Alcotest.(check int) "Should find events" 2 (List.length events)
| Error _ -> Alcotest.fail "Error querying events");
let calendar_names = [ "example"; "recurrence" ] in
-
let filter = Query.in_calendar_names calendar_names in
+
let filter = Query.in_calendars calendar_names in
(match Query.query ~fs calendar_dir ~from ~to_ ~filter () with
| Ok events ->
Alcotest.(check int) "Should find events" 791 (List.length events)
| Error _ -> Alcotest.fail "Error querying events");
-
let filter = Query.in_calendar_names [ "non-existent-calendar" ] in
+
let filter = Query.in_calendars [ "non-existent-calendar" ] in
(match Query.query ~fs calendar_dir ~from ~to_ ~filter () with
| Ok events ->
Alcotest.(check int)