fix: relative date time handling

This commit is contained in:
yuankunzhang
2025-05-29 23:35:28 +08:00
parent c05b8217e0
commit 9b67307512
+178 -50
View File
@@ -345,11 +345,31 @@ fn last_day_of_month(year: i32, month: u32) -> u32 {
.day()
}
fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTime<FixedOffset>> {
d = d.with_hour(0).unwrap();
d = d.with_minute(0).unwrap();
d = d.with_second(0).unwrap();
d = d.with_nanosecond(0).unwrap();
fn at_date_inner(date: Vec<Item>, at: DateTime<FixedOffset>) -> Option<DateTime<FixedOffset>> {
let mut d = at
.with_hour(0)
.unwrap()
.with_minute(0)
.unwrap()
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
// This flag is used by relative items to determine which date/time to use.
// If any date/time item is set, it will use that; otherwise, it will use
// the `at` value.
let date_time_set = date.iter().any(|item| {
matches!(
item,
Item::Timestamp(_)
| Item::Date(_)
| Item::DateTime(_)
| Item::Year(_)
| Item::Time(_)
| Item::Weekday(_)
)
});
for item in date {
match item {
@@ -416,54 +436,84 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
offset,
)?;
}
Item::Weekday(weekday::Weekday {
offset: _, // TODO: use the offset
day,
}) => {
let mut beginning_of_day = d
.with_hour(0)
.unwrap()
.with_minute(0)
.unwrap()
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
Item::Weekday(weekday::Weekday { offset: x, day }) => {
let mut x = x;
let day = day.into();
while beginning_of_day.weekday() != day {
beginning_of_day += chrono::Duration::days(1);
// If the current day is not the target day, we need to adjust
// the x value to ensure we find the correct day.
//
// Consider this:
// Assuming today is Monday, next Friday is actually THIS Friday;
// but next Monday is indeed NEXT Monday.
if d.weekday() != day && x > 0 {
x -= 1;
}
d = beginning_of_day
}
Item::Relative(relative::Relative::Years(x)) => {
d = d.with_year(d.year() + x)?;
}
Item::Relative(relative::Relative::Months(x)) => {
// *NOTE* This is done in this way to conform to
// GNU behavior.
let days = last_day_of_month(d.year(), d.month());
if x >= 0 {
d += d
.date_naive()
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
.signed_duration_since(d.date_naive());
// Calculate the delta to the target day.
//
// Assuming today is Thursday, here are some examples:
//
// Example 1: last Thursday (x = -1, day = Thursday)
// delta = (3 - 3) % 7 + (-1) * 7 = -7
//
// Example 2: last Monday (x = -1, day = Monday)
// delta = (0 - 3) % 7 + (-1) * 7 = -3
//
// Example 3: next Monday (x = 1, day = Monday)
// delta = (0 - 3) % 7 + (0) * 7 = 4
// (Note that we have adjusted the x value above)
//
// Example 4: next Thursday (x = 1, day = Thursday)
// delta = (3 - 3) % 7 + (1) * 7 = 7
let delta = (day.num_days_from_monday() as i32
- d.weekday().num_days_from_monday() as i32)
.rem_euclid(7)
+ x * 7;
d = if delta < 0 {
d.checked_sub_days(chrono::Days::new((-delta) as u64))?
} else {
d += d
.date_naive()
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
.signed_duration_since(d.date_naive());
d.checked_add_days(chrono::Days::new(delta as u64))?
}
}
Item::Relative(relative::Relative::Days(x)) => d += chrono::Duration::days(x.into()),
Item::Relative(relative::Relative::Hours(x)) => d += chrono::Duration::hours(x.into()),
Item::Relative(relative::Relative::Minutes(x)) => {
d += chrono::Duration::minutes(x.into());
}
// Seconds are special because they can be given as a float
Item::Relative(relative::Relative::Seconds(x)) => {
d += chrono::Duration::seconds(x as i64);
Item::Relative(rel) => {
// If date and/or time is set, use the set value; otherwise, use
// the reference value.
if !date_time_set {
d = at;
}
match rel {
relative::Relative::Years(x) => {
d = d.with_year(d.year() + x)?;
}
relative::Relative::Months(x) => {
// *NOTE* This is done in this way to conform to
// GNU behavior.
let days = last_day_of_month(d.year(), d.month());
if x >= 0 {
d += d
.date_naive()
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
.signed_duration_since(d.date_naive());
} else {
d += d
.date_naive()
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
.signed_duration_since(d.date_naive());
}
}
relative::Relative::Days(x) => d += chrono::Duration::days(x.into()),
relative::Relative::Hours(x) => d += chrono::Duration::hours(x.into()),
relative::Relative::Minutes(x) => {
d += chrono::Duration::minutes(x.into());
}
// Seconds are special because they can be given as a float
relative::Relative::Seconds(x) => {
d += chrono::Duration::seconds(x as i64);
}
}
}
Item::TimeZone(offset) => {
d = with_timezone_restore(offset, d)?;
@@ -476,9 +526,9 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
pub(crate) fn at_date(
date: Vec<Item>,
d: DateTime<FixedOffset>,
at: DateTime<FixedOffset>,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
at_date_inner(date, d).ok_or(ParseDateTimeError::InvalidInput)
at_date_inner(date, at).ok_or(ParseDateTimeError::InvalidInput)
}
pub(crate) fn at_local(date: Vec<Item>) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
@@ -488,10 +538,12 @@ pub(crate) fn at_local(date: Vec<Item>) -> Result<DateTime<FixedOffset>, ParseDa
#[cfg(test)]
mod tests {
use super::{at_date, date::Date, parse, time::Time, Item};
use chrono::{DateTime, FixedOffset};
use chrono::{
DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
};
fn at_utc(date: Vec<Item>) -> DateTime<FixedOffset> {
at_date(date, chrono::Utc::now().fixed_offset()).unwrap()
at_date(date, Utc::now().fixed_offset()).unwrap()
}
fn test_eq_fmt(fmt: &str, input: &str) -> String {
@@ -610,4 +662,80 @@ mod tests {
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unexpected input"));
}
#[test]
fn relative_weekday() {
// Jan 1 2025 is a Wed
let now = Utc
.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
))
.fixed_offset();
assert_eq!(
at_date(parse(&mut "last wed").unwrap(), now).unwrap(),
now - chrono::Duration::days(7)
);
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now).unwrap(), now);
assert_eq!(
at_date(parse(&mut "next wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(7)
);
assert_eq!(
at_date(parse(&mut "last thu").unwrap(), now).unwrap(),
now - chrono::Duration::days(6)
);
assert_eq!(
at_date(parse(&mut "this thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
);
assert_eq!(
at_date(parse(&mut "next thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
);
assert_eq!(
at_date(parse(&mut "1 wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(7)
);
assert_eq!(
at_date(parse(&mut "1 thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
);
assert_eq!(
at_date(parse(&mut "2 wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(14)
);
assert_eq!(
at_date(parse(&mut "2 thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(8)
);
}
#[test]
fn relative_date_time() {
let now = Utc::now().fixed_offset();
let result = at_date(parse(&mut "2 days ago").unwrap(), now).unwrap();
assert_eq!(result, now - chrono::Duration::days(2));
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now).unwrap();
assert_eq!(result.hour(), 0);
assert_eq!(result.minute(), 0);
assert_eq!(result.second(), 0);
let result = at_date(parse(&mut "3 weeks").unwrap(), now).unwrap();
assert_eq!(result, now + chrono::Duration::days(21));
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now).unwrap();
assert_eq!(result.hour(), 0);
assert_eq!(result.minute(), 0);
assert_eq!(result.second(), 0);
}
}