@@ -345,11 +345,31 @@ fn last_day_of_month(year: i32, month: u32) -> u32 {
345
345
. day ( )
346
346
}
347
347
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
+ } ) ;
353
373
354
374
for item in date {
355
375
match item {
@@ -416,54 +436,84 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
416
436
offset,
417
437
) ?;
418
438
}
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;
432
441
let day = day. into ( ) ;
433
442
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 ;
436
451
}
437
452
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 ) ) ?
452
476
} 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 ) ) ?
457
478
}
458
479
}
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
+ }
467
517
}
468
518
Item :: TimeZone ( offset) => {
469
519
d = with_timezone_restore ( offset, d) ?;
@@ -476,9 +526,9 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
476
526
477
527
pub ( crate ) fn at_date (
478
528
date : Vec < Item > ,
479
- d : DateTime < FixedOffset > ,
529
+ at : DateTime < FixedOffset > ,
480
530
) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
481
- at_date_inner ( date, d ) . ok_or ( ParseDateTimeError :: InvalidInput )
531
+ at_date_inner ( date, at ) . ok_or ( ParseDateTimeError :: InvalidInput )
482
532
}
483
533
484
534
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
488
538
#[ cfg( test) ]
489
539
mod tests {
490
540
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
+ } ;
492
544
493
545
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 ( )
495
547
}
496
548
497
549
fn test_eq_fmt ( fmt : & str , input : & str ) -> String {
@@ -610,4 +662,80 @@ mod tests {
610
662
assert ! ( result. is_err( ) ) ;
611
663
assert ! ( result. unwrap_err( ) . to_string( ) . contains( "unexpected input" ) ) ;
612
664
}
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
+ }
613
741
}
0 commit comments