From 49060acb79d72ffe795f8450ae536f3f6d6c9631 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Sat, 1 Jun 2024 13:32:50 +0200 Subject: [PATCH] fix(schedule): manage past day or past month properly --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 142 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce4c02e..9b91751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,7 +296,7 @@ dependencies = [ [[package]] name = "emile" -version = "0.5.1" +version = "0.5.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 2b55023..6fbe170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "emile" -version = "0.5.1" +version = "0.5.2" authors = ["Geobert Quach "] edition = "2021" diff --git a/src/main.rs b/src/main.rs index bd3971e..56341a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ use std::{borrow::Cow, io::Write, sync::Arc}; use anyhow::{bail, Context, Error, Result}; -use chrono::{DateTime, Datelike, Days, FixedOffset, Local, NaiveTime, Timelike, Utc}; +use chrono::{ + DateTime, Datelike, Days, FixedOffset, Local, Months, NaiveDate, NaiveTime, TimeZone, Timelike, + Utc, +}; use clap::Parser; use config::SiteConfigBuilder; @@ -125,32 +128,40 @@ fn zola_build() -> Result<()> { } } -fn parse_time(time_str: &str, default_time: &NaiveTime) -> Result, Error> { - let now = Local::now(); - let time_str = fix_time(time_str, &now); - let datetime = match human_date_parser::from_human_time(&time_str)? { +fn parse_time_with_ref( + time_str: &str, + ref_date: DateTime, + default_time: &NaiveTime, +) -> Result, Error> { + let time_str = fix_time(time_str, &ref_date); + let datetime = match human_date_parser::from_human_time(&time_str) + .with_context(|| format!("Failure parsing `{time_str}`"))? + { human_date_parser::ParseResult::DateTime(d) => d.fixed_offset(), - human_date_parser::ParseResult::Date(d) => d - .and_hms_opt( - default_time.hour(), - default_time.minute(), - default_time.second(), - ) - .unwrap() - .and_local_timezone(now.timezone()) - .unwrap() - .into(), + human_date_parser::ParseResult::Date(d) => { + let datetime: DateTime = d + .and_hms_opt( + default_time.hour(), + default_time.minute(), + default_time.second(), + ) + .unwrap() + .and_local_timezone(ref_date.timezone()) + .unwrap() + .into(); + datetime + } human_date_parser::ParseResult::Time(t) => { - let now_time = now.time(); + let now_time = ref_date.time(); let date = if t < now_time { - now.checked_add_days(Days::new(1)).with_context(|| { + ref_date.checked_add_days(Days::new(1)).with_context(|| { format!( "Failed to add one day to `{}`", - format_date(&now.fixed_offset()) + format_date(&ref_date.fixed_offset()) ) })? } else { - now + ref_date }; match date.with_time(t) { chrono::offset::MappedLocalTime::Single(dt) => dt.fixed_offset(), @@ -163,17 +174,61 @@ fn parse_time(time_str: &str, default_time: &NaiveTime) -> Result Result, Error> { + let ref_date = Local::now(); + parse_time_with_ref(time_str, ref_date, default_time) +} + // We accept omitted year and month. This function construct a minimal valid input to be parsed fn fix_time<'a>(s: &'a str, now: &DateTime) -> Cow<'a, str> { + let fix_day = |day, now: &DateTime| -> DateTime { + if day < now.day() { + let d = Local + .from_local_datetime( + &NaiveDate::from_ymd_opt(now.year(), now.month(), day) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + ) + .unwrap(); + d.checked_add_months(Months::new(1)) + .expect(&format!("Add a month to `{}` blew up", now)) + } else { + let diff = day - now.day(); + now.checked_add_days(Days::new(diff as u64)) + .expect(&format!("Add `{diff}` to `{now}` blew up")) + } + }; + let day = Regex::new("^[0-3]?[0-9]$").expect("Failure compiling day regex"); if day.is_match(s) { - return Cow::Owned(format!("{}-{}-{s}", now.year(), now.month())); + let day = u32::from_str_radix(s, 10).unwrap(); + let date = fix_day(day, now); + return Cow::Owned(format!("{}-{}-{}", date.year(), date.month(), date.day())); } - let month_day = - Regex::new(r"^[0-1]?[0-9]\-[0-3]?[0-9]$").expect("Failure compiling month regex"); - if month_day.is_match(s) { - return Cow::Owned(format!("{}-{s}", now.year())); + let month_day = Regex::new(r"^(?[0-1]?[0-9])\-(?[0-3]?[0-9])$") + .expect("Failure compiling month regex"); + if let Some(caps) = month_day.captures(s) { + let day = u32::from_str_radix(&caps["day"], 10) + .expect(&format!("`{s}` is not a valid `month-day`")); + let month = u32::from_str_radix(&caps["month"], 10) + .expect(&format!("`{s}` is not a valid `month-day`")); + + let date = if month < now.month() { + let diff = now.month() - month; + + now.checked_add_months(Months::new(12 - diff)) + .expect(&format!("Adding a year to `{now}` blew up")) + } else { + let month_diff = month - now.month(); + let d = now + .checked_add_months(Months::new(month_diff)) + .expect(&format!("Adding `{month_diff}` to `{now}` blew up")); + fix_day(day, &d) + }; + + return Cow::Owned(format!("{}-{}-{}", date.year(), date.month(), date.day())); } Cow::Borrowed(s) @@ -186,3 +241,64 @@ fn format_date(date: &DateTime) -> String { fn format_utc_date(date: &DateTime) -> String { date.format("%Y-%m-%dT%H:%M:%SZ").to_string() } + +#[cfg(test)] +mod tests { + use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveTime, TimeZone}; + + use crate::parse_time_with_ref; + + fn ref_date() -> (DateTime, NaiveTime) { + let def_time = NaiveTime::from_hms_opt(12, 00, 00).unwrap(); + let now = Local + .from_local_datetime( + &NaiveDate::from_ymd_opt(2024, 06, 27) + .unwrap() + .and_time(def_time), + ) + .unwrap(); + (now, def_time) + } + + #[test] + fn test_month_in_the_past() { + let (now, def_time) = ref_date(); + let r = parse_time_with_ref("05-27", now.clone(), &def_time).unwrap(); + assert_eq!(r.year(), 2025); + assert_eq!(r.day(), 27); + assert_eq!(r.month(), 5); + let r = parse_time_with_ref("04-27", now.clone(), &def_time).unwrap(); + assert_eq!(r.year(), 2025); + assert_eq!(r.day(), 27); + assert_eq!(r.month(), 4); + } + + #[test] + fn test_month_in_the_future() { + let (now, def_time) = ref_date(); + let r = parse_time_with_ref("07-27", now.clone(), &def_time).unwrap(); + assert_eq!(r.year(), 2024); + assert_eq!(r.day(), 27); + assert_eq!(r.month(), 7); + } + + #[test] + fn test_day_in_the_past() { + let (now, def_time) = ref_date(); + let r = parse_time_with_ref("26", now.clone(), &def_time).unwrap(); + dbg!(&r); + assert_eq!(r.year(), 2024); + assert_eq!(r.month(), 7); + assert_eq!(r.day(), 26) + } + + #[test] + fn test_day_in_the_future() { + let (now, def_time) = ref_date(); + let r = parse_time_with_ref("28", now.clone(), &def_time).unwrap(); + dbg!(&r); + assert_eq!(r.year(), 2024); + assert_eq!(r.month(), 6); + assert_eq!(r.day(), 28) + } +}