preferences added

This commit is contained in:
Hermes Agent
2026-08-16 20:08:27 +00:00
parent b099157a17
commit f53ed36f31
11 changed files with 1425 additions and 173 deletions

View File

@@ -4,14 +4,46 @@ use chrono::{
};
use regex::Regex;
/// Extracts the first recognizable date/time phrase and returns local RFC 3339.
/// Deliberately favors useful, predictable planner phrases over pretending to be
/// a complete natural-language parser.
pub fn extract_when(text: &str) -> Option<String> {
extract_when_from(text, Local::now().naive_local())
#[cfg(test)]
pub fn extract_when_from(text: &str, now: NaiveDateTime) -> Option<String> {
extract_when_from_configured(
text, now, "ymd", "monday", "09:00", "09:00", "13:00", "18:00",
)
}
pub fn extract_when_from(text: &str, now: NaiveDateTime) -> Option<String> {
#[allow(clippy::too_many_arguments)]
pub fn extract_when_configured(
text: &str,
date_order: &str,
week_start: &str,
default_time: &str,
morning_time: &str,
afternoon_time: &str,
evening_time: &str,
) -> Option<String> {
extract_when_from_configured(
text,
Local::now().naive_local(),
date_order,
week_start,
default_time,
morning_time,
afternoon_time,
evening_time,
)
}
#[allow(clippy::too_many_arguments)]
fn extract_when_from_configured(
text: &str,
now: NaiveDateTime,
date_order: &str,
week_start: &str,
default_time: &str,
morning_time: &str,
afternoon_time: &str,
evening_time: &str,
) -> Option<String> {
let lower = text.to_lowercase();
let mut date = None;
@@ -20,6 +52,24 @@ pub fn extract_when_from(text: &str, now: NaiveDateTime) -> Option<String> {
date = NaiveDate::from_ymd_opt(c[1].parse().ok()?, c[2].parse().ok()?, c[3].parse().ok()?);
}
if date.is_none() {
let numeric = Regex::new(r"\b(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})\b").unwrap();
if let Some(c) = numeric.captures(&lower) {
let values = [
c[1].parse::<i32>().ok()?,
c[2].parse::<i32>().ok()?,
c[3].parse::<i32>().ok()?,
];
let (year, month, day) = match date_order {
"mdy" => (values[2], values[0], values[1]),
"dmy" => (values[2], values[1], values[0]),
_ => (values[0], values[1], values[2]),
};
let year = if year < 100 { year + 2000 } else { year };
date = NaiveDate::from_ymd_opt(year, month as u32, day as u32);
}
}
if date.is_none() {
let in_days = Regex::new(r"\bin\s+(\d+)\s+days?\b").unwrap();
if let Some(c) = in_days.captures(&lower) {
@@ -33,6 +83,20 @@ pub fn extract_when_from(text: &str, now: NaiveDateTime) -> Option<String> {
}
}
if date.is_none() && (lower.contains("this week") || lower.contains("next week")) {
let day_from_start = if week_start == "sunday" {
now.weekday().num_days_from_sunday() as i64
} else {
now.weekday().num_days_from_monday() as i64
};
let current_start = now.date() - Duration::days(day_from_start);
date = Some(if lower.contains("next week") {
current_start + Duration::weeks(1)
} else {
current_start
});
}
if date.is_none() {
let weekdays = [
("monday", Weekday::Mon),
@@ -76,20 +140,43 @@ pub fn extract_when_from(text: &str, now: NaiveDateTime) -> Option<String> {
NaiveTime::from_hms_opt(hour, minute, 0)?
} else if let Some(c) = time24_re.captures(&lower) {
NaiveTime::from_hms_opt(c[1].parse().ok()?, c[2].parse().ok()?, 0)?
} else if lower.contains("morning") {
parse_clock(morning_time)?
} else if lower.contains("afternoon") {
parse_clock(afternoon_time)?
} else if lower.contains("evening") || lower.contains("tonight") {
parse_clock(evening_time)?
} else {
NaiveTime::from_hms_opt(9, 0, 0)?
parse_clock(default_time)?
};
let local = Local.from_local_datetime(&date.and_time(time)).single()?;
Some(local.to_rfc3339())
}
pub fn pretty_when(value: Option<&str>) -> String {
fn parse_clock(value: &str) -> Option<NaiveTime> {
NaiveTime::parse_from_str(value, "%H:%M").ok()
}
pub fn format_when(value: Option<&str>, date_format: &str, clock_24h: bool) -> String {
let Some(value) = value else {
return String::new();
};
chrono::DateTime::parse_from_rfc3339(value)
.map(|d| d.format("%Y-%m-%d %H:%M").to_string())
.map(|d| {
let date = match date_format {
"us" => d.format("%m/%d/%Y").to_string(),
"european" => d.format("%d/%m/%Y").to_string(),
"long" => d.format("%b %-d, %Y").to_string(),
_ => d.format("%Y-%m-%d").to_string(),
};
let time = if clock_24h {
d.format("%H:%M").to_string()
} else {
d.format("%-I:%M%P").to_string()
};
format!("{date} {time}")
})
.unwrap_or_else(|_| value.to_owned())
}
@@ -176,4 +263,36 @@ mod tests {
.contains("2026-08-30T09:00:00")
);
}
#[test]
fn honors_document_date_and_named_time_settings() {
let got = extract_when_from_configured(
"Planning on 18/08/2026 in the evening",
base(),
"dmy",
"monday",
"08:30",
"08:00",
"14:00",
"19:15",
)
.unwrap();
assert!(got.contains("2026-08-18T19:15:00"));
assert_eq!(
format_when(Some(&got), "european", false),
"18/08/2026 7:15pm"
);
let next_week = extract_when_from_configured(
"Review next week",
base(),
"ymd",
"sunday",
"09:00",
"09:00",
"13:00",
"18:00",
)
.unwrap();
assert!(next_week.contains("2026-08-23T09:00:00"));
}
}