Skip to content

Commit

Permalink
fix(schedule): manage past day or past month properly
Browse files Browse the repository at this point in the history
  • Loading branch information
Geobert committed Jun 1, 2024
1 parent 77b1b4a commit 49060ac
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "emile"
version = "0.5.1"
version = "0.5.2"
authors = ["Geobert Quach <geobert@proton.me>"]
edition = "2021"

Expand Down
164 changes: 140 additions & 24 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -125,32 +128,40 @@ fn zola_build() -> Result<()> {
}
}

fn parse_time(time_str: &str, default_time: &NaiveTime) -> Result<DateTime<FixedOffset>, 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<Local>,
default_time: &NaiveTime,
) -> Result<DateTime<FixedOffset>, 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<FixedOffset> = 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(),
Expand All @@ -163,17 +174,61 @@ fn parse_time(time_str: &str, default_time: &NaiveTime) -> Result<DateTime<Fixed
Ok(datetime)
}

fn parse_time(time_str: &str, default_time: &NaiveTime) -> Result<DateTime<FixedOffset>, 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<Local>) -> Cow<'a, str> {
let fix_day = |day, now: &DateTime<Local>| -> DateTime<Local> {
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"^(?<month>[0-1]?[0-9])\-(?<day>[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)
Expand All @@ -186,3 +241,64 @@ fn format_date(date: &DateTime<FixedOffset>) -> String {
fn format_utc_date(date: &DateTime<Utc>) -> 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<Local>, 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)
}
}

0 comments on commit 49060ac

Please sign in to comment.