@@ -345,11 +345,31 @@ 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 d = 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+ let date_time_set = date. iter ( ) . any ( |item| {
363+ matches ! (
364+ item,
365+ Item :: Timestamp ( _)
366+ | Item :: Date ( _)
367+ | Item :: DateTime ( _)
368+ | Item :: Year ( _)
369+ | Item :: Time ( _)
370+ | Item :: Weekday ( _)
371+ )
372+ } ) ;
353373
354374 for item in date {
355375 match item {
@@ -416,54 +436,84 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
416436 offset,
417437 ) ?;
418438 }
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 ( ) ;
439+ Item :: Weekday ( weekday:: Weekday { offset : x, day } ) => {
440+ let mut x = x;
432441 let day = day. into ( ) ;
433442
434- while beginning_of_day. weekday ( ) != day {
435- beginning_of_day += chrono:: Duration :: days ( 1 ) ;
443+ // If the current day is not the target day, we need to adjust
444+ // the x value to ensure we find the correct day.
445+ //
446+ // Consider this:
447+ // Assuming today is Monday, next Friday is actually THIS Friday;
448+ // but next Monday is indeed NEXT Monday.
449+ if d. weekday ( ) != day && x > 0 {
450+ x -= 1 ;
436451 }
437452
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 ( ) ) ;
453+ // Calculate the delta to the target day.
454+ //
455+ // Assuming today is Thursday, here are some examples:
456+ //
457+ // Example 1: last Thursday (x = -1, day = Thursday)
458+ // delta = (3 - 3) % 7 + (-1) * 7 = -7
459+ //
460+ // Example 2: last Monday (x = -1, day = Monday)
461+ // delta = (0 - 3) % 7 + (-1) * 7 = -3
462+ //
463+ // Example 3: next Monday (x = 1, day = Monday)
464+ // delta = (0 - 3) % 7 + (0) * 7 = 4
465+ // (Note that we have adjusted the x value above)
466+ //
467+ // Example 4: next Thursday (x = 1, day = Thursday)
468+ // delta = (3 - 3) % 7 + (1) * 7 = 7
469+ let delta = ( day. num_days_from_monday ( ) as i32
470+ - d. weekday ( ) . num_days_from_monday ( ) as i32 )
471+ . rem_euclid ( 7 )
472+ + x * 7 ;
473+
474+ d = if delta < 0 {
475+ d. checked_sub_days ( chrono:: Days :: new ( ( -delta) as u64 ) ) ?
452476 } 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 ( ) ) ;
477+ d. checked_add_days ( chrono:: Days :: new ( delta as u64 ) ) ?
457478 }
458479 }
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 ) ;
480+ Item :: Relative ( rel) => {
481+ // If date and/or time is set, use the set value; otherwise, use
482+ // the reference value.
483+ if !date_time_set {
484+ d = at;
485+ }
486+
487+ match rel {
488+ relative:: Relative :: Years ( x) => {
489+ d = d. with_year ( d. year ( ) + x) ?;
490+ }
491+ relative:: Relative :: Months ( x) => {
492+ // *NOTE* This is done in this way to conform to
493+ // GNU behavior.
494+ let days = last_day_of_month ( d. year ( ) , d. month ( ) ) ;
495+ if x >= 0 {
496+ d += d
497+ . date_naive ( )
498+ . checked_add_days ( chrono:: Days :: new ( ( days * x as u32 ) as u64 ) ) ?
499+ . signed_duration_since ( d. date_naive ( ) ) ;
500+ } else {
501+ d += d
502+ . date_naive ( )
503+ . checked_sub_days ( chrono:: Days :: new ( ( days * -x as u32 ) as u64 ) ) ?
504+ . signed_duration_since ( d. date_naive ( ) ) ;
505+ }
506+ }
507+ relative:: Relative :: Days ( x) => d += chrono:: Duration :: days ( x. into ( ) ) ,
508+ relative:: Relative :: Hours ( x) => d += chrono:: Duration :: hours ( x. into ( ) ) ,
509+ relative:: Relative :: Minutes ( x) => {
510+ d += chrono:: Duration :: minutes ( x. into ( ) ) ;
511+ }
512+ // Seconds are special because they can be given as a float
513+ relative:: Relative :: Seconds ( x) => {
514+ d += chrono:: Duration :: seconds ( x as i64 ) ;
515+ }
516+ }
467517 }
468518 Item :: TimeZone ( offset) => {
469519 d = with_timezone_restore ( offset, d) ?;
@@ -476,9 +526,9 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
476526
477527pub ( crate ) fn at_date (
478528 date : Vec < Item > ,
479- d : DateTime < FixedOffset > ,
529+ at : DateTime < FixedOffset > ,
480530) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
481- at_date_inner ( date, d ) . ok_or ( ParseDateTimeError :: InvalidInput )
531+ at_date_inner ( date, at ) . ok_or ( ParseDateTimeError :: InvalidInput )
482532}
483533
484534pub ( 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
488538#[ cfg( test) ]
489539mod tests {
490540 use super :: { at_date, date:: Date , parse, time:: Time , Item } ;
491- use chrono:: { DateTime , FixedOffset } ;
541+ use chrono:: {
542+ DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Utc ,
543+ } ;
492544
493545 fn at_utc ( date : Vec < Item > ) -> DateTime < FixedOffset > {
494- at_date ( date, chrono :: Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
546+ at_date ( date, Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
495547 }
496548
497549 fn test_eq_fmt ( fmt : & str , input : & str ) -> String {
@@ -610,4 +662,80 @@ mod tests {
610662 assert ! ( result. is_err( ) ) ;
611663 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "unexpected input" ) ) ;
612664 }
665+
666+ #[ test]
667+ fn relative_weekday ( ) {
668+ // Jan 1 2025 is a Wed
669+ let now = Utc
670+ . from_utc_datetime ( & NaiveDateTime :: new (
671+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
672+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
673+ ) )
674+ . fixed_offset ( ) ;
675+
676+ assert_eq ! (
677+ at_date( parse( & mut "last wed" ) . unwrap( ) , now) . unwrap( ) ,
678+ now - chrono:: Duration :: days( 7 )
679+ ) ;
680+ assert_eq ! ( at_date( parse( & mut "this wed" ) . unwrap( ) , now) . unwrap( ) , now) ;
681+ assert_eq ! (
682+ at_date( parse( & mut "next wed" ) . unwrap( ) , now) . unwrap( ) ,
683+ now + chrono:: Duration :: days( 7 )
684+ ) ;
685+ assert_eq ! (
686+ at_date( parse( & mut "last thu" ) . unwrap( ) , now) . unwrap( ) ,
687+ now - chrono:: Duration :: days( 6 )
688+ ) ;
689+ assert_eq ! (
690+ at_date( parse( & mut "this thu" ) . unwrap( ) , now) . unwrap( ) ,
691+ now + chrono:: Duration :: days( 1 )
692+ ) ;
693+ assert_eq ! (
694+ at_date( parse( & mut "next thu" ) . unwrap( ) , now) . unwrap( ) ,
695+ now + chrono:: Duration :: days( 1 )
696+ ) ;
697+ assert_eq ! (
698+ at_date( parse( & mut "1 wed" ) . unwrap( ) , now) . unwrap( ) ,
699+ now + chrono:: Duration :: days( 7 )
700+ ) ;
701+ assert_eq ! (
702+ at_date( parse( & mut "1 thu" ) . unwrap( ) , now) . unwrap( ) ,
703+ now + chrono:: Duration :: days( 1 )
704+ ) ;
705+ assert_eq ! (
706+ at_date( parse( & mut "2 wed" ) . unwrap( ) , now) . unwrap( ) ,
707+ now + chrono:: Duration :: days( 14 )
708+ ) ;
709+ assert_eq ! (
710+ at_date( parse( & mut "2 thu" ) . unwrap( ) , now) . unwrap( ) ,
711+ now + chrono:: Duration :: days( 8 )
712+ ) ;
713+ }
714+
715+ #[ test]
716+ fn relative_date_time ( ) {
717+ let now = Utc :: now ( ) . fixed_offset ( ) ;
718+
719+ let result = at_date ( parse ( & mut "2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
720+ assert_eq ! ( result, now - chrono:: Duration :: days( 2 ) ) ;
721+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
722+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
723+ assert_eq ! ( result. second( ) , now. second( ) ) ;
724+
725+ let result = at_date ( parse ( & mut "2025-01-01 2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
726+ assert_eq ! ( result. hour( ) , 0 ) ;
727+ assert_eq ! ( result. minute( ) , 0 ) ;
728+ assert_eq ! ( result. second( ) , 0 ) ;
729+
730+ let result = at_date ( parse ( & mut "3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
731+ assert_eq ! ( result, now + chrono:: Duration :: days( 21 ) ) ;
732+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
733+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
734+ assert_eq ! ( result. second( ) , now. second( ) ) ;
735+
736+ let result = at_date ( parse ( & mut "2025-01-01 3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
737+ assert_eq ! ( result. hour( ) , 0 ) ;
738+ assert_eq ! ( result. minute( ) , 0 ) ;
739+ assert_eq ! ( result. second( ) , 0 ) ;
740+ }
613741}
0 commit comments