@@ -345,11 +345,37 @@ 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
+ //
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
+ }
353
379
354
380
for item in date {
355
381
match item {
@@ -416,54 +442,84 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
416
442
offset,
417
443
) ?;
418
444
}
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;
432
447
let day = day. into ( ) ;
433
448
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 d. weekday ( ) != day && x > 0 {
456
+ x -= 1 ;
436
457
}
437
458
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
+ - d. weekday ( ) . num_days_from_monday ( ) as i32 )
477
+ . rem_euclid ( 7 )
478
+ + x * 7 ;
479
+
480
+ d = if delta < 0 {
481
+ d. checked_sub_days ( chrono:: Days :: new ( ( -delta) as u64 ) ) ?
452
482
} 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
+ d. checked_add_days ( chrono:: Days :: new ( delta as u64 ) ) ?
457
484
}
458
485
}
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
+ d = at;
491
+ }
492
+
493
+ match rel {
494
+ relative:: Relative :: Years ( x) => {
495
+ d = d. with_year ( d. 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 ( d. year ( ) , d. month ( ) ) ;
501
+ if x >= 0 {
502
+ d += d
503
+ . date_naive ( )
504
+ . checked_add_days ( chrono:: Days :: new ( ( days * x as u32 ) as u64 ) ) ?
505
+ . signed_duration_since ( d. date_naive ( ) ) ;
506
+ } else {
507
+ d += d
508
+ . date_naive ( )
509
+ . checked_sub_days ( chrono:: Days :: new ( ( days * -x as u32 ) as u64 ) ) ?
510
+ . signed_duration_since ( d. date_naive ( ) ) ;
511
+ }
512
+ }
513
+ relative:: Relative :: Days ( x) => d += chrono:: Duration :: days ( x. into ( ) ) ,
514
+ relative:: Relative :: Hours ( x) => d += chrono:: Duration :: hours ( x. into ( ) ) ,
515
+ relative:: Relative :: Minutes ( x) => {
516
+ d += chrono:: Duration :: minutes ( x. into ( ) ) ;
517
+ }
518
+ // Seconds are special because they can be given as a float
519
+ relative:: Relative :: Seconds ( x) => {
520
+ d += chrono:: Duration :: seconds ( x as i64 ) ;
521
+ }
522
+ }
467
523
}
468
524
Item :: TimeZone ( offset) => {
469
525
d = with_timezone_restore ( offset, d) ?;
@@ -476,9 +532,9 @@ fn at_date_inner(date: Vec<Item>, mut d: DateTime<FixedOffset>) -> Option<DateTi
476
532
477
533
pub ( crate ) fn at_date (
478
534
date : Vec < Item > ,
479
- d : DateTime < FixedOffset > ,
535
+ at : DateTime < FixedOffset > ,
480
536
) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
481
- at_date_inner ( date, d ) . ok_or ( ParseDateTimeError :: InvalidInput )
537
+ at_date_inner ( date, at ) . ok_or ( ParseDateTimeError :: InvalidInput )
482
538
}
483
539
484
540
pub ( crate ) fn at_local ( date : Vec < Item > ) -> Result < DateTime < FixedOffset > , ParseDateTimeError > {
@@ -488,10 +544,12 @@ pub(crate) fn at_local(date: Vec<Item>) -> Result<DateTime<FixedOffset>, ParseDa
488
544
#[ cfg( test) ]
489
545
mod tests {
490
546
use super :: { at_date, date:: Date , parse, time:: Time , Item } ;
491
- use chrono:: { DateTime , FixedOffset } ;
547
+ use chrono:: {
548
+ DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Utc ,
549
+ } ;
492
550
493
551
fn at_utc ( date : Vec < Item > ) -> DateTime < FixedOffset > {
494
- at_date ( date, chrono :: Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
552
+ at_date ( date, Utc :: now ( ) . fixed_offset ( ) ) . unwrap ( )
495
553
}
496
554
497
555
fn test_eq_fmt ( fmt : & str , input : & str ) -> String {
@@ -610,4 +668,80 @@ mod tests {
610
668
assert ! ( result. is_err( ) ) ;
611
669
assert ! ( result. unwrap_err( ) . to_string( ) . contains( "unexpected input" ) ) ;
612
670
}
671
+
672
+ #[ test]
673
+ fn relative_weekday ( ) {
674
+ // Jan 1 2025 is a Wed
675
+ let now = Utc
676
+ . from_utc_datetime ( & NaiveDateTime :: new (
677
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
678
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
679
+ ) )
680
+ . fixed_offset ( ) ;
681
+
682
+ assert_eq ! (
683
+ at_date( parse( & mut "last wed" ) . unwrap( ) , now) . unwrap( ) ,
684
+ now - chrono:: Duration :: days( 7 )
685
+ ) ;
686
+ assert_eq ! ( at_date( parse( & mut "this wed" ) . unwrap( ) , now) . unwrap( ) , now) ;
687
+ assert_eq ! (
688
+ at_date( parse( & mut "next wed" ) . unwrap( ) , now) . unwrap( ) ,
689
+ now + chrono:: Duration :: days( 7 )
690
+ ) ;
691
+ assert_eq ! (
692
+ at_date( parse( & mut "last thu" ) . unwrap( ) , now) . unwrap( ) ,
693
+ now - chrono:: Duration :: days( 6 )
694
+ ) ;
695
+ assert_eq ! (
696
+ at_date( parse( & mut "this thu" ) . unwrap( ) , now) . unwrap( ) ,
697
+ now + chrono:: Duration :: days( 1 )
698
+ ) ;
699
+ assert_eq ! (
700
+ at_date( parse( & mut "next thu" ) . unwrap( ) , now) . unwrap( ) ,
701
+ now + chrono:: Duration :: days( 1 )
702
+ ) ;
703
+ assert_eq ! (
704
+ at_date( parse( & mut "1 wed" ) . unwrap( ) , now) . unwrap( ) ,
705
+ now + chrono:: Duration :: days( 7 )
706
+ ) ;
707
+ assert_eq ! (
708
+ at_date( parse( & mut "1 thu" ) . unwrap( ) , now) . unwrap( ) ,
709
+ now + chrono:: Duration :: days( 1 )
710
+ ) ;
711
+ assert_eq ! (
712
+ at_date( parse( & mut "2 wed" ) . unwrap( ) , now) . unwrap( ) ,
713
+ now + chrono:: Duration :: days( 14 )
714
+ ) ;
715
+ assert_eq ! (
716
+ at_date( parse( & mut "2 thu" ) . unwrap( ) , now) . unwrap( ) ,
717
+ now + chrono:: Duration :: days( 8 )
718
+ ) ;
719
+ }
720
+
721
+ #[ test]
722
+ fn relative_date_time ( ) {
723
+ let now = Utc :: now ( ) . fixed_offset ( ) ;
724
+
725
+ let result = at_date ( parse ( & mut "2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
726
+ assert_eq ! ( result, now - chrono:: Duration :: days( 2 ) ) ;
727
+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
728
+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
729
+ assert_eq ! ( result. second( ) , now. second( ) ) ;
730
+
731
+ let result = at_date ( parse ( & mut "2025-01-01 2 days ago" ) . unwrap ( ) , now) . unwrap ( ) ;
732
+ assert_eq ! ( result. hour( ) , 0 ) ;
733
+ assert_eq ! ( result. minute( ) , 0 ) ;
734
+ assert_eq ! ( result. second( ) , 0 ) ;
735
+
736
+ let result = at_date ( parse ( & mut "3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
737
+ assert_eq ! ( result, now + chrono:: Duration :: days( 21 ) ) ;
738
+ assert_eq ! ( result. hour( ) , now. hour( ) ) ;
739
+ assert_eq ! ( result. minute( ) , now. minute( ) ) ;
740
+ assert_eq ! ( result. second( ) , now. second( ) ) ;
741
+
742
+ let result = at_date ( parse ( & mut "2025-01-01 3 weeks" ) . unwrap ( ) , now) . unwrap ( ) ;
743
+ assert_eq ! ( result. hour( ) , 0 ) ;
744
+ assert_eq ! ( result. minute( ) , 0 ) ;
745
+ assert_eq ! ( result. second( ) , 0 ) ;
746
+ }
613
747
}
0 commit comments