Accept date shortcuts in the Work day field

Adding work to a day you forgot to track meant typing its full date.
The Work day field now also takes 09-14 (this year), today, yesterday,
-3 (three days ago) and weekday names like mon or friday (the most
recent one). The editor shows the date it understood next to the field.

The parsing lives in time::format::parse_date, next to parse_time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-09-25 00:02:43 -04:00
1 parent 9257181a77
commit fe5e2d9a7f
4 files changed
+81 -8

No files matched your search

+62 -2
View File
@@ -6,8 +6,8 @@
use std::fmt;
use jiff::civil::{Date, Time};
use jiff::{SignedDuration, Zoned};
use jiff::civil::{Date, Time, Weekday};
use jiff::{SignedDuration, ToSpan, Zoned};
/// How clock times are written.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -284,9 +284,69 @@ fn split_am_pm(text: &str) -> (&str, Option<AmPm>) {
}
}
/// Reads a date typed by the user, with shortcuts for recent days:
///
/// `2026-09-14`, `09-14` or `9-14` (this year), `today`, `yesterday`,
/// `-3` (3 days ago), or a weekday like `mon` or `friday` (the most recent
/// one, today included).
pub fn parse_date(input: &str, today: Date) -> Result<Date, String> {
let text = input.trim().to_ascii_lowercase();
let error = || format!("\"{}\" isn't a date; try 2026-09-14, 09-14, yesterday, -3 or mon", input.trim());
match text.as_str() {
"today" => return Ok(today),
"yesterday" => return Ok(today.saturating_sub(1.day())),
_ => {}
}
if let Some(days) = text.strip_prefix('-').and_then(|days| days.parse::<i64>().ok()) {
return Ok(today.saturating_sub(days.days()));
}
// A weekday name, or its start ("mon", "thu"): the most recent one.
const WEEKDAYS: [(&str, Weekday); 7] = [
("monday", Weekday::Monday),
("tuesday", Weekday::Tuesday),
("wednesday", Weekday::Wednesday),
("thursday", Weekday::Thursday),
("friday", Weekday::Friday),
("saturday", Weekday::Saturday),
("sunday", Weekday::Sunday),
];
if text.len() >= 3
&& let Some((_, weekday)) = WEEKDAYS.iter().find(|(name, _)| name.starts_with(&text))
{
return Ok(today.saturating_sub(today.weekday().since(*weekday).days()));
}
// "09-14" or "9-14": month and day of this year.
let numbers: Vec<&str> = text.split('-').collect();
if let [month, day] = numbers.as_slice() {
let month: i8 = month.parse().map_err(|_| error())?;
let day: i8 = day.parse().map_err(|_| error())?;
return Date::new(today.year(), month, day).map_err(|_| error());
}
text.parse().map_err(|_| error())
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::civil::date;
#[test]
fn reads_dates_and_shortcuts() {
let today = date(2026, 9, 24); // a Thursday
assert_eq!(parse_date("2026-09-14", today), Ok(date(2026, 9, 14)));
assert_eq!(parse_date("09-14", today), Ok(date(2026, 9, 14)));
assert_eq!(parse_date("9-4", today), Ok(date(2026, 9, 4)));
assert_eq!(parse_date("Today", today), Ok(today));
assert_eq!(parse_date("yesterday", today), Ok(date(2026, 9, 23)));
assert_eq!(parse_date("-3", today), Ok(date(2026, 9, 21)));
assert_eq!(parse_date("mon", today), Ok(date(2026, 9, 21)));
assert_eq!(parse_date("thursday", today), Ok(today));
assert_eq!(parse_date("fri", today), Ok(date(2026, 9, 18)));
assert!(parse_date("th", today).is_err());
assert!(parse_date("13-40", today).is_err());
assert!(parse_date("someday", today).is_err());
}
fn time(hour: i8, minute: i8) -> Time {
Time::new(hour, minute, 0, 0).unwrap()