1
1
// For the full copyright and license information, please view the LICENSE
2
2
// file that was distributed with this source code.
3
- use crate :: ParseDateTimeError ;
3
+ use crate :: { parse_weekday :: parse_weekday , ParseDateTimeError } ;
4
4
use chrono:: {
5
5
DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , TimeZone ,
6
+ Weekday ,
6
7
} ;
7
8
use regex:: Regex ;
8
9
@@ -61,7 +62,7 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
61
62
r"(?x)
62
63
(?:(?P<value>[-+]?\d*)\s*)?
63
64
(\s*(?P<direction>next|this|last)?\s*)?
64
- (?P<unit>years?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today)
65
+ (?P<unit>years?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today|(?P<weekday>[a-z]{3,9}))\b
65
66
(\s*(?P<separator>and|,)?\s*)?
66
67
(\s*(?P<ago>ago)?)?" ,
67
68
) ?;
@@ -77,16 +78,19 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
77
78
. name ( "value" )
78
79
. ok_or ( ParseDateTimeError :: InvalidInput ) ?
79
80
. as_str ( ) ;
81
+ let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
80
82
let value = if value_str. is_empty ( ) {
81
- 1
83
+ if direction == "this" {
84
+ 0
85
+ } else {
86
+ 1
87
+ }
82
88
} else {
83
89
value_str
84
90
. parse :: < i64 > ( )
85
91
. map_err ( |_| ParseDateTimeError :: InvalidInput ) ?
86
92
} ;
87
93
88
- let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
89
-
90
94
if direction == "last" {
91
95
is_ago = true ;
92
96
}
@@ -100,27 +104,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
100
104
is_ago = true ;
101
105
}
102
106
103
- let new_datetime = if direction == "this" {
104
- add_days ( datetime, 0 , is_ago)
105
- } else {
106
- match unit {
107
- "years" | "year" => add_months ( datetime, value * 12 , is_ago) ,
108
- "months" | "month" => add_months ( datetime, value, is_ago) ,
109
- "fortnights" | "fortnight" => add_days ( datetime, value * 14 , is_ago) ,
110
- "weeks" | "week" => add_days ( datetime, value * 7 , is_ago) ,
111
- "days" | "day" => add_days ( datetime, value, is_ago) ,
112
- "hours" | "hour" | "h" => add_duration ( datetime, Duration :: hours ( value) , is_ago) ,
113
- "minutes" | "minute" | "mins" | "min" | "m" => {
114
- add_duration ( datetime, Duration :: minutes ( value) , is_ago)
115
- }
116
- "seconds" | "second" | "secs" | "sec" | "s" => {
117
- add_duration ( datetime, Duration :: seconds ( value) , is_ago)
118
- }
119
- "yesterday" => add_days ( datetime, 1 , true ) ,
120
- "tomorrow" => add_days ( datetime, 1 , false ) ,
121
- "now" | "today" => Some ( datetime) ,
122
- _ => None ,
107
+ let new_datetime = match unit {
108
+ "years" | "year" => add_months ( datetime, value * 12 , is_ago) ,
109
+ "months" | "month" => add_months ( datetime, value, is_ago) ,
110
+ "fortnights" | "fortnight" => add_days ( datetime, value * 14 , is_ago) ,
111
+ "weeks" | "week" => add_days ( datetime, value * 7 , is_ago) ,
112
+ "days" | "day" => add_days ( datetime, value, is_ago) ,
113
+ "hours" | "hour" | "h" => add_duration ( datetime, Duration :: hours ( value) , is_ago) ,
114
+ "minutes" | "minute" | "mins" | "min" | "m" => {
115
+ add_duration ( datetime, Duration :: minutes ( value) , is_ago)
116
+ }
117
+ "seconds" | "second" | "secs" | "sec" | "s" => {
118
+ add_duration ( datetime, Duration :: seconds ( value) , is_ago)
123
119
}
120
+ "yesterday" => add_days ( datetime, 1 , true ) ,
121
+ "tomorrow" => add_days ( datetime, 1 , false ) ,
122
+ "now" | "today" => Some ( datetime) ,
123
+ _ => capture
124
+ . name ( "weekday" )
125
+ . and_then ( |weekday| parse_weekday ( weekday. as_str ( ) ) )
126
+ . and_then ( |weekday| adjust_for_weekday ( datetime, weekday, value, is_ago) ) ,
124
127
} ;
125
128
datetime = match new_datetime {
126
129
Some ( dt) => dt,
@@ -145,6 +148,23 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
145
148
}
146
149
}
147
150
151
+ fn adjust_for_weekday < T : TimeZone > (
152
+ mut datetime : DateTime < T > ,
153
+ weekday : Weekday ,
154
+ mut amount : i64 ,
155
+ is_ago : bool ,
156
+ ) -> Option < DateTime < T > > {
157
+ let mut same_day = true ;
158
+ while datetime. weekday ( ) != weekday {
159
+ datetime = add_days ( datetime, 1 , is_ago) ?;
160
+ same_day = false ;
161
+ }
162
+ if !same_day && 0 < amount {
163
+ amount -= 1 ;
164
+ }
165
+ add_days ( datetime, amount * 7 , is_ago)
166
+ }
167
+
148
168
fn add_months < T : TimeZone > (
149
169
datetime : DateTime < T > ,
150
170
months : i64 ,
@@ -794,4 +814,168 @@ mod tests {
794
814
let result = parse_relative_time_at_date ( now, "invalid 1r" ) ;
795
815
assert_eq ! ( result, Err ( ParseDateTimeError :: InvalidInput ) ) ;
796
816
}
817
+
818
+ #[ test]
819
+ fn test_parse_relative_time_at_date_this_weekday ( ) {
820
+ // Jan 1 2025 is a Wed
821
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
822
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
823
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
824
+ ) ) ;
825
+ // Check "this <same weekday>"
826
+ assert_eq ! (
827
+ parse_relative_time_at_date( now, "this wednesday" ) . unwrap( ) ,
828
+ now
829
+ ) ;
830
+ assert_eq ! ( parse_relative_time_at_date( now, "this wed" ) . unwrap( ) , now) ;
831
+ // Other days
832
+ assert_eq ! (
833
+ parse_relative_time_at_date( now, "this thursday" ) . unwrap( ) ,
834
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
835
+ ) ;
836
+ assert_eq ! (
837
+ parse_relative_time_at_date( now, "this thur" ) . unwrap( ) ,
838
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
839
+ ) ;
840
+ assert_eq ! (
841
+ parse_relative_time_at_date( now, "this thu" ) . unwrap( ) ,
842
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
843
+ ) ;
844
+ assert_eq ! (
845
+ parse_relative_time_at_date( now, "this friday" ) . unwrap( ) ,
846
+ now. checked_add_days( Days :: new( 2 ) ) . unwrap( )
847
+ ) ;
848
+ assert_eq ! (
849
+ parse_relative_time_at_date( now, "this fri" ) . unwrap( ) ,
850
+ now. checked_add_days( Days :: new( 2 ) ) . unwrap( )
851
+ ) ;
852
+ assert_eq ! (
853
+ parse_relative_time_at_date( now, "this saturday" ) . unwrap( ) ,
854
+ now. checked_add_days( Days :: new( 3 ) ) . unwrap( )
855
+ ) ;
856
+ assert_eq ! (
857
+ parse_relative_time_at_date( now, "this sat" ) . unwrap( ) ,
858
+ now. checked_add_days( Days :: new( 3 ) ) . unwrap( )
859
+ ) ;
860
+ // "this" with a day of the week that comes before today should return the next instance of
861
+ // that day
862
+ assert_eq ! (
863
+ parse_relative_time_at_date( now, "this sunday" ) . unwrap( ) ,
864
+ now. checked_add_days( Days :: new( 4 ) ) . unwrap( )
865
+ ) ;
866
+ assert_eq ! (
867
+ parse_relative_time_at_date( now, "this sun" ) . unwrap( ) ,
868
+ now. checked_add_days( Days :: new( 4 ) ) . unwrap( )
869
+ ) ;
870
+ assert_eq ! (
871
+ parse_relative_time_at_date( now, "this monday" ) . unwrap( ) ,
872
+ now. checked_add_days( Days :: new( 5 ) ) . unwrap( )
873
+ ) ;
874
+ assert_eq ! (
875
+ parse_relative_time_at_date( now, "this mon" ) . unwrap( ) ,
876
+ now. checked_add_days( Days :: new( 5 ) ) . unwrap( )
877
+ ) ;
878
+ assert_eq ! (
879
+ parse_relative_time_at_date( now, "this tuesday" ) . unwrap( ) ,
880
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
881
+ ) ;
882
+ assert_eq ! (
883
+ parse_relative_time_at_date( now, "this tue" ) . unwrap( ) ,
884
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
885
+ ) ;
886
+ }
887
+
888
+ #[ test]
889
+ fn test_parse_relative_time_at_date_last_weekday ( ) {
890
+ // Jan 1 2025 is a Wed
891
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
892
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
893
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
894
+ ) ) ;
895
+ // Check "last <same weekday>"
896
+ assert_eq ! (
897
+ parse_relative_time_at_date( now, "last wed" ) . unwrap( ) ,
898
+ now. checked_sub_days( Days :: new( 7 ) ) . unwrap( )
899
+ ) ;
900
+ // Check "last <day after today>"
901
+ assert_eq ! (
902
+ parse_relative_time_at_date( now, "last thu" ) . unwrap( ) ,
903
+ now. checked_sub_days( Days :: new( 6 ) ) . unwrap( )
904
+ ) ;
905
+ // Check "last <day before today>"
906
+ assert_eq ! (
907
+ parse_relative_time_at_date( now, "last tue" ) . unwrap( ) ,
908
+ now. checked_sub_days( Days :: new( 1 ) ) . unwrap( )
909
+ ) ;
910
+ }
911
+
912
+ #[ test]
913
+ fn test_parse_relative_time_at_date_next_weekday ( ) {
914
+ // Jan 1 2025 is a Wed
915
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
916
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
917
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
918
+ ) ) ;
919
+ // Check "next <same weekday>"
920
+ assert_eq ! (
921
+ parse_relative_time_at_date( now, "next wed" ) . unwrap( ) ,
922
+ now. checked_add_days( Days :: new( 7 ) ) . unwrap( )
923
+ ) ;
924
+ // Check "next <day after today>"
925
+ assert_eq ! (
926
+ parse_relative_time_at_date( now, "next thu" ) . unwrap( ) ,
927
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
928
+ ) ;
929
+ // Check "next <day before today>"
930
+ assert_eq ! (
931
+ parse_relative_time_at_date( now, "next tue" ) . unwrap( ) ,
932
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
933
+ ) ;
934
+ }
935
+
936
+ #[ test]
937
+ fn test_parse_relative_time_at_date_number_weekday ( ) {
938
+ // Jan 1 2025 is a Wed
939
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
940
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
941
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
942
+ ) ) ;
943
+ assert_eq ! (
944
+ parse_relative_time_at_date( now, "1 wed" ) . unwrap( ) ,
945
+ now. checked_add_days( Days :: new( 7 ) ) . unwrap( )
946
+ ) ;
947
+ assert_eq ! (
948
+ parse_relative_time_at_date( now, "1 thu" ) . unwrap( ) ,
949
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
950
+ ) ;
951
+ assert_eq ! (
952
+ parse_relative_time_at_date( now, "1 tue" ) . unwrap( ) ,
953
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
954
+ ) ;
955
+ assert_eq ! (
956
+ parse_relative_time_at_date( now, "2 wed" ) . unwrap( ) ,
957
+ now. checked_add_days( Days :: new( 14 ) ) . unwrap( )
958
+ ) ;
959
+ assert_eq ! (
960
+ parse_relative_time_at_date( now, "2 thu" ) . unwrap( ) ,
961
+ now. checked_add_days( Days :: new( 8 ) ) . unwrap( )
962
+ ) ;
963
+ assert_eq ! (
964
+ parse_relative_time_at_date( now, "2 tue" ) . unwrap( ) ,
965
+ now. checked_add_days( Days :: new( 13 ) ) . unwrap( )
966
+ ) ;
967
+ }
968
+
969
+ #[ test]
970
+ fn test_parse_relative_time_at_date_invalid_weekday ( ) {
971
+ // Jan 1 2025 is a Wed
972
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
973
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
974
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
975
+ ) ) ;
976
+ assert_eq ! (
977
+ parse_relative_time_at_date( now, "this fooday" ) ,
978
+ Err ( ParseDateTimeError :: InvalidInput )
979
+ ) ;
980
+ }
797
981
}
0 commit comments