@@ -345,29 +345,55 @@ fn last_day_of_month(year: i32, month: u32) -> u32 {
345345 . day ( )
346346}
347347
348- fn at_date_inner ( date : Vec < Item > , mut d : DateTime < FixedOffset > ) -> Option < DateTime < FixedOffset > > {
349- d = d. with_hour ( 0 ) . unwrap ( ) ;
350- d = d. with_minute ( 0 ) . unwrap ( ) ;
351- d = d. with_second ( 0 ) . unwrap ( ) ;
352- d = d. with_nanosecond ( 0 ) . unwrap ( ) ;
348+ fn at_date_inner ( date : Vec < Item > , at : DateTime < FixedOffset > ) -> Option < DateTime < FixedOffset > > {
349+ let mut dt = at
350+ . with_hour ( 0 )
351+ . unwrap ( )
352+ . with_minute ( 0 )
353+ . unwrap ( )
354+ . with_second ( 0 )
355+ . unwrap ( )
356+ . with_nanosecond ( 0 )
357+ . unwrap ( ) ;
358+
359+ // This flag is used by relative items to determine which date/time to use.
360+ // If any date/time item is set, it will use that; otherwise, it will use
361+ // the `at` value.
362+ //
363+ // TODO: find cleaner way to do this
364+ let mut date_time_set = false ;
365+ for item in & date {
366+ match item {
367+ Item :: Timestamp ( _)
368+ | Item :: Date ( _)
369+ | Item :: DateTime ( _)
370+ | Item :: Year ( _)
371+ | Item :: Time ( _)
372+ | Item :: Weekday ( _) => {
373+ date_time_set = true ;
374+ break ;
375+ }
376+ _ => { }
377+ }
378+ }
353379
354380 for item in date {
355381 match item {
356382 Item :: Timestamp ( ts) => {
357- d = chrono:: Utc
383+ dt = chrono:: Utc
358384 . timestamp_opt ( ts. into ( ) , 0 )
359385 . unwrap ( )
360- . with_timezone ( & d . timezone ( ) )
386+ . with_timezone ( & dt . timezone ( ) )
361387 }
362388 Item :: Date ( date:: Date { day, month, year } ) => {
363- d = new_date (
364- year. map ( |x| x as i32 ) . unwrap_or ( d . year ( ) ) ,
389+ dt = new_date (
390+ year. map ( |x| x as i32 ) . unwrap_or ( dt . year ( ) ) ,
365391 month,
366392 day,
367- d . hour ( ) ,
368- d . minute ( ) ,
369- d . second ( ) ,
370- * d . offset ( ) ,
393+ dt . hour ( ) ,
394+ dt . minute ( ) ,
395+ dt . second ( ) ,
396+ * dt . offset ( ) ,
371397 ) ?;
372398 }
373399 Item :: DateTime ( combined:: DateTime {
@@ -383,10 +409,10 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
383409 } ) => {
384410 let offset = offset
385411 . and_then ( |o| chrono:: FixedOffset :: try_from ( o) . ok ( ) )
386- . unwrap_or ( * d . offset ( ) ) ;
412+ . unwrap_or ( * dt . offset ( ) ) ;
387413
388- d = new_date (
389- year. map ( |x| x as i32 ) . unwrap_or ( d . year ( ) ) ,
414+ dt = new_date (
415+ year. map ( |x| x as i32 ) . unwrap_or ( dt . year ( ) ) ,
390416 month,
391417 day,
392418 hour,
@@ -395,7 +421,7 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
395421 offset,
396422 ) ?;
397423 }
398- Item :: Year ( year) => d = d . with_year ( year as i32 ) . unwrap_or ( d ) ,
424+ Item :: Year ( year) => dt = dt . with_year ( year as i32 ) . unwrap_or ( dt ) ,
399425 Item :: Time ( time:: Time {
400426 hour,
401427 minute,
@@ -404,81 +430,139 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
404430 } ) => {
405431 let offset = offset
406432 . and_then ( |o| chrono:: FixedOffset :: try_from ( o) . ok ( ) )
407- . unwrap_or ( * d . offset ( ) ) ;
433+ . unwrap_or ( * dt . offset ( ) ) ;
408434
409- d = new_date (
410- d . year ( ) ,
411- d . month ( ) ,
412- d . day ( ) ,
435+ dt = new_date (
436+ dt . year ( ) ,
437+ dt . month ( ) ,
438+ dt . day ( ) ,
413439 hour,
414440 minute,
415441 second as u32 ,
416442 offset,
417443 ) ?;
418444 }
419- Item :: Weekday ( weekday:: Weekday {
420- offset : _, // TODO: use the offset
421- day,
422- } ) => {
423- let mut beginning_of_day = d
424- . with_hour ( 0 )
425- . unwrap ( )
426- . with_minute ( 0 )
427- . unwrap ( )
428- . with_second ( 0 )
429- . unwrap ( )
430- . with_nanosecond ( 0 )
431- . unwrap ( ) ;
445+ Item :: Weekday ( weekday:: Weekday { offset : x, day } ) => {
446+ let mut x = x;
432447 let day = day. into ( ) ;
433448
434- while beginning_of_day. weekday ( ) != day {
435- beginning_of_day += chrono:: Duration :: days ( 1 ) ;
449+ // If the current day is not the target day, we need to adjust
450+ // the x value to ensure we find the correct day.
451+ //
452+ // Consider this:
453+ // Assuming today is Monday, next Friday is actually THIS Friday;
454+ // but next Monday is indeed NEXT Monday.
455+ if dt. weekday ( ) != day && x > 0 {
456+ x -= 1 ;
436457 }
437458
438- d = beginning_of_day
439- }
440- Item :: Relative ( relative:: Relative :: Years ( x) ) => {
441- d = d. with_year ( d. year ( ) + x) ?;
442- }
443- Item :: Relative ( relative:: Relative :: Months ( x) ) => {
444- // *NOTE* This is done in this way to conform to
445- // GNU behavior.
446- let days = last_day_of_month ( d. year ( ) , d. month ( ) ) ;
447- if x >= 0 {
448- d += d
449- . date_naive ( )
450- . checked_add_days ( chrono:: Days :: new ( ( days * x as u32 ) as u64 ) ) ?
451- . signed_duration_since ( d. date_naive ( ) ) ;
459+ // Calculate the delta to the target day.
460+ //
461+ // Assuming today is Thursday, here are some examples:
462+ //
463+ // Example 1: last Thursday (x = -1, day = Thursday)
464+ // delta = (3 - 3) % 7 + (-1) * 7 = -7
465+ //
466+ // Example 2: last Monday (x = -1, day = Monday)
467+ // delta = (0 - 3) % 7 + (-1) * 7 = -3
468+ //
469+ // Example 3: next Monday (x = 1, day = Monday)
470+ // delta = (0 - 3) % 7 + (0) * 7 = 4
471+ // (Note that we have adjusted the x value above)
472+ //
473+ // Example 4: next Thursday (x = 1, day = Thursday)
474+ // delta = (3 - 3) % 7 + (1) * 7 = 7
475+ let delta = ( day. num_days_from_monday ( ) as i32
476+ - dt. weekday ( ) . num_days_from_monday ( ) as i32 )
477+ . rem_euclid ( 7 )
478+ + x * 7 ;
479+
480+ dt = if delta < 0 {
481+ dt. checked_sub_days ( chrono:: Days :: new ( ( -delta) as u64 ) ) ?
452482 } else {
453- d += d
454- . date_naive ( )
455- . checked_sub_days ( chrono:: Days :: new ( ( days * -x as u32 ) as u64 ) ) ?
456- . signed_duration_since ( d. date_naive ( ) ) ;
483+ dt. checked_add_days ( chrono:: Days :: new ( delta as u64 ) ) ?
457484 }
458485 }
459- Item :: Relative ( relative:: Relative :: Days ( x) ) => d += chrono:: Duration :: days ( x. into ( ) ) ,
460- Item :: Relative ( relative:: Relative :: Hours ( x) ) => d += chrono:: Duration :: hours ( x. into ( ) ) ,
461- Item :: Relative ( relative:: Relative :: Minutes ( x) ) => {
462- d += chrono:: Duration :: minutes ( x. into ( ) ) ;
463- }
464- // Seconds are special because they can be given as a float
465- Item :: Relative ( relative:: Relative :: Seconds ( x) ) => {
466- d += chrono:: Duration :: seconds ( x as i64 ) ;
486+ Item :: Relative ( rel) => {
487+ // If date and/or time is set, use the set value; otherwise, use
488+ // the reference value.
489+ if !date_time_set {
490+ dt = at;
491+ }
492+
493+ match rel {
494+ relative:: Relative :: Years ( x) => {
495+ dt = dt. with_year ( dt. year ( ) + x) ?;
496+ }
497+ relative:: Relative :: Months ( x) => {
498+ // *NOTE* This is done in this way to conform to
499+ // GNU behavior.
500+ let days = last_day_of_month ( dt. year ( ) , dt. month ( ) ) ;
501+ if x >= 0 {
502+ dt += dt
503+ . date_naive ( )
504+ . checked_add_days ( chrono:: Days :: new ( ( days * x as u32 ) as u64 ) ) ?
505+ . signed_duration_since ( dt. date_naive ( ) ) ;
506+ } else {
507+ dt += dt
508+ . date_naive ( )
509+ . checked_sub_days ( chrono:: Days :: new ( ( days * -x as u32 ) as u64 ) ) ?
510+ . signed_duration_since ( dt. date_naive ( ) ) ;
511+ }
512+ }
513+ relative:: Relative :: Days ( x) => dt += chrono:: Duration :: days ( x. into ( ) ) ,
514+ relative:: Relative :: Hours ( x) => dt += chrono:: Duration :: hours ( x. into ( ) ) ,
515+ relative:: Relative :: Minutes ( x) => {
516+ dt += chrono:: Duration :: minutes ( x. into ( ) ) ;
517+ }
518+ // Seconds are special because they can be given as a float
519+ relative:: Relative :: Seconds ( x) => {
520+ dt += chrono:: Duration :: seconds ( x as i64 ) ;
521+ }
522+ }
467523 }
524+ // Item::Relative(relative::Relative::Years(x)) => {
525+ // dt = dt.with_year(dt.year() + x)?;
526+ // }
527+ // Item::Relative(relative::Relative::Months(x)) => {
528+ // // *NOTE* This is done in this way to conform to
529+ // // GNU behavior.
530+ // let days = last_day_of_month(dt.year(), dt.month());
531+ // if x >= 0 {
532+ // dt += dt
533+ // .date_naive()
534+ // .checked_add_days(chrono::Days::new((days * x as u32) as u64))?
535+ // .signed_duration_since(dt.date_naive());
536+ // } else {
537+ // dt += dt
538+ // .date_naive()
539+ // .checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
540+ // .signed_duration_since(dt.date_naive());
541+ // }
542+ // }
543+ // Item::Relative(relative::Relative::Days(x)) => dt += chrono::Duration::days(x.into()),
544+ // Item::Relative(relative::Relative::Hours(x)) => dt += chrono::Duration::hours(x.into()),
545+ // Item::Relative(relative::Relative::Minutes(x)) => {
546+ // dt += chrono::Duration::minutes(x.into());
547+ // }
548+ // // Seconds are special because they can be given as a float
549+ // Item::Relative(relative::Relative::Seconds(x)) => {
550+ // dt += chrono::Duration::seconds(x as i64);
551+ // }
468552 Item :: TimeZone ( offset) => {
469- d = with_timezone_restore ( offset, d ) ?;
553+ dt = with_timezone_restore ( offset, dt ) ?;
470554 }
471555 }
472556 }
473557
474- Some ( d )
558+ Some ( dt )
475559}
476560
477561pub ( crate ) fn at_date (
478562 date : Vec < Item > ,
479- d : DateTime < FixedOffset > ,
563+ at : DateTime < FixedOffset > ,
480564) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
481- at_date_inner ( date, d ) . ok_or ( ParseDateTimeError :: InvalidInput )
565+ at_date_inner ( date, at ) . ok_or ( ParseDateTimeError :: InvalidInput )
482566}
483567
484568pub ( crate ) fn at_local ( date : Vec < Item > ) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
@@ -488,10 +572,12 @@ pub(crate) fn at_local(date: Vec<Item>) -> Result<DateTime<FixedOffset>, ParseDa
488572#[ cfg( test) ]
489573mod tests {
490574 use super :: { at_date, date:: Date , parse, time:: Time , Item } ;
491- use chrono:: { DateTime , FixedOffset } ;
575+ use chrono:: {
576+ DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Utc ,
577+ } ;
492578
493579 fn at_utc ( date : Vec < Item > ) -> DateTime < FixedOffset > {
494- at_date ( date, chrono :: Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
580+ at_date ( date, Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
495581 }
496582
497583 fn test_eq_fmt ( fmt : & str , input : & str ) -> String {
@@ -561,6 +647,80 @@ mod tests {
561647 test_eq_fmt( "%Y-%m-%d %H:%M:%S %:z" , "Jul 17 06:14:49 2024 BRT" ) ,
562648 ) ;
563649 }
650+ #[ test]
651+ fn relative_weekday ( ) {
652+ // Jan 1 2025 is a Wed
653+ let now = Utc
654+ . from_utc_datetime ( & NaiveDateTime :: new (
655+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
656+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
657+ ) )
658+ . fixed_offset ( ) ;
659+
660+ assert_eq ! (
661+ at_date( parse( & mut "last wed" ) . unwrap( ) , now) . unwrap( ) ,
662+ now - chrono:: Duration :: days( 7 )
663+ ) ;
664+ assert_eq ! ( at_date( parse( & mut "this wed" ) . unwrap( ) , now) . unwrap( ) , now) ;
665+ assert_eq ! (
666+ at_date( parse( & mut "next wed" ) . unwrap( ) , now) . unwrap( ) ,
667+ now + chrono:: Duration :: days( 7 )
668+ ) ;
669+ assert_eq ! (
670+ at_date( parse( & mut "last thu" ) . unwrap( ) , now) . unwrap( ) ,
671+ now - chrono:: Duration :: days( 6 )
672+ ) ;
673+ assert_eq ! (
674+ at_date( parse( & mut "this thu" ) . unwrap( ) , now) . unwrap( ) ,
675+ now + chrono:: Duration :: days( 1 )
676+ ) ;
677+ assert_eq ! (
678+ at_date( parse( & mut "next thu" ) . unwrap( ) , now) . unwrap( ) ,
679+ now + chrono:: Duration :: days( 1 )
680+ ) ;
681+ assert_eq ! (
682+ at_date( parse( & mut "1 wed" ) . unwrap( ) , now) . unwrap( ) ,
683+ now + chrono:: Duration :: days( 7 )
684+ ) ;
685+ assert_eq ! (
686+ at_date( parse( & mut "1 thu" ) . unwrap( ) , now) . unwrap( ) ,
687+ now + chrono:: Duration :: days( 1 )
688+ ) ;
689+ assert_eq ! (
690+ at_date( parse( & mut "2 wed" ) . unwrap( ) , now) . unwrap( ) ,
691+ now + chrono:: Duration :: days( 14 )
692+ ) ;
693+ assert_eq ! (
694+ at_date( parse( & mut "2 thu" ) . unwrap( ) , now) . unwrap( ) ,
695+ now + chrono:: Duration :: days( 8 )
696+ ) ;
697+ }
698+ #[ test]
699+ fn relative_date_time ( ) {
700+ let now = Utc :: now ( ) . fixed_offset ( ) ;
701+
702+ let result = at_date ( parse ( & mut "2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
703+ assert_eq ! ( result, now - chrono:: Duration :: days( 2 ) ) ;
704+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
705+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
706+ assert_eq ! ( result. second( ) , now. second( ) ) ;
707+
708+ let result = at_date ( parse ( & mut "2025-01-01 2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
709+ assert_eq ! ( result. hour( ) , 0 ) ;
710+ assert_eq ! ( result. minute( ) , 0 ) ;
711+ assert_eq ! ( result. second( ) , 0 ) ;
712+
713+ let result = at_date ( parse ( & mut "3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
714+ assert_eq ! ( result, now + chrono:: Duration :: days( 21 ) ) ;
715+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
716+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
717+ assert_eq ! ( result. second( ) , now. second( ) ) ;
718+
719+ let result = at_date ( parse ( & mut "2025-01-01 3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
720+ assert_eq ! ( result. hour( ) , 0 ) ;
721+ assert_eq ! ( result. minute( ) , 0 ) ;
722+ assert_eq ! ( result. second( ) , 0 ) ;
723+ }
564724
565725 #[ test]
566726 fn invalid ( ) {
0 commit comments