use chrono::{DateTime, Utc};
pub const DUE_PRESETS: &[&str] = &[
"",
"today",
"tomorrow",
"+2d",
"+3d",
"+1w",
"+2w",
"next monday",
"next friday",
];
pub fn parse_due(s: &str, dialect_str: &str) -> Option<DateTime<Utc>> {
let s = s.trim();
if s.is_empty() {
return None;
}
let dialect = match dialect_str {
"us" => interim::Dialect::Us,
_ => interim::Dialect::Uk,
};
if let Ok(date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
let dt = date.and_hms_opt(23, 59, 59)?;
return Some(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc));
}
if s.eq_ignore_ascii_case("today") {
return Some(Utc::now());
}
if let Some(rest) = s.strip_prefix('+') {
if let Some(days_str) = rest.strip_suffix('d')
&& let Ok(days) = days_str.trim().parse::<i64>()
{
return Utc::now().checked_add_signed(chrono::Duration::try_days(days)?);
}
if let Some(weeks_str) = rest.strip_suffix('w')
&& let Ok(weeks) = weeks_str.trim().parse::<i64>()
{
return Utc::now().checked_add_signed(chrono::Duration::try_weeks(weeks)?);
}
}
if let Ok(dt) = interim::parse_date_string(s, chrono::Local::now(), dialect) {
return Some(dt.with_timezone(&Utc));
}
None
}
pub fn parse_duration_mins(s: &str) -> Option<i64> {
let s = s.trim();
if s.is_empty() {
return None;
}
let s_lower = s.to_lowercase();
let rest = s_lower.as_str();
let non_negative = |t: &str| t.trim().parse::<i64>().ok().filter(|n| *n >= 0);
if let Some(h_pos) = rest.find('h')
&& let Some(h) = non_negative(&rest[..h_pos])
{
let mut total = h.checked_mul(60)?;
let after_h = rest[h_pos + 1..].trim().trim_end_matches('m').trim();
if let Ok(m) = after_h.parse::<i64>() {
if m < 0 {
return None;
}
total = total.checked_add(m)?;
}
return Some(total);
}
non_negative(rest.trim_end_matches('m'))
}
pub fn is_valid_due(s: &str) -> bool {
let s = s.trim();
s.is_empty() || parse_due(s, "uk").is_some()
}
#[cfg(test)]
#[path = "../../../tests/unit/infrastructure/dates.rs"]
mod tests;