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
- DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , TimeZone ,
5
+ DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , NaiveTime ,
6
+ TimeZone , 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>[-+]?\s*\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
) ?;
@@ -80,16 +81,19 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
80
81
. chars ( )
81
82
. filter ( |c| !c. is_whitespace ( ) ) // Remove potential space between +/- and number
82
83
. collect ( ) ;
84
+ let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
83
85
let value = if value_str. is_empty ( ) {
84
- 1
86
+ if direction == "this" {
87
+ 0
88
+ } else {
89
+ 1
90
+ }
85
91
} else {
86
92
value_str
87
93
. parse :: < i64 > ( )
88
94
. map_err ( |_| ParseDateTimeError :: InvalidInput ) ?
89
95
} ;
90
96
91
- let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
92
-
93
97
if direction == "last" {
94
98
is_ago = true ;
95
99
}
@@ -103,27 +107,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
103
107
is_ago = true ;
104
108
}
105
109
106
- let new_datetime = if direction == "this" {
107
- add_days ( datetime, 0 , is_ago)
108
- } else {
109
- match unit {
110
- "years" | "year" => add_months ( datetime, value * 12 , is_ago) ,
111
- "months" | "month" => add_months ( datetime, value, is_ago) ,
112
- "fortnights" | "fortnight" => add_days ( datetime, value * 14 , is_ago) ,
113
- "weeks" | "week" => add_days ( datetime, value * 7 , is_ago) ,
114
- "days" | "day" => add_days ( datetime, value, is_ago) ,
115
- "hours" | "hour" | "h" => add_duration ( datetime, Duration :: hours ( value) , is_ago) ,
116
- "minutes" | "minute" | "mins" | "min" | "m" => {
117
- add_duration ( datetime, Duration :: minutes ( value) , is_ago)
118
- }
119
- "seconds" | "second" | "secs" | "sec" | "s" => {
120
- add_duration ( datetime, Duration :: seconds ( value) , is_ago)
121
- }
122
- "yesterday" => add_days ( datetime, 1 , true ) ,
123
- "tomorrow" => add_days ( datetime, 1 , false ) ,
124
- "now" | "today" => Some ( datetime) ,
125
- _ => None ,
110
+ let new_datetime = match unit {
111
+ "years" | "year" => add_months ( datetime, value * 12 , is_ago) ,
112
+ "months" | "month" => add_months ( datetime, value, is_ago) ,
113
+ "fortnights" | "fortnight" => add_days ( datetime, value * 14 , is_ago) ,
114
+ "weeks" | "week" => add_days ( datetime, value * 7 , is_ago) ,
115
+ "days" | "day" => add_days ( datetime, value, is_ago) ,
116
+ "hours" | "hour" | "h" => add_duration ( datetime, Duration :: hours ( value) , is_ago) ,
117
+ "minutes" | "minute" | "mins" | "min" | "m" => {
118
+ add_duration ( datetime, Duration :: minutes ( value) , is_ago)
119
+ }
120
+ "seconds" | "second" | "secs" | "sec" | "s" => {
121
+ add_duration ( datetime, Duration :: seconds ( value) , is_ago)
126
122
}
123
+ "yesterday" => add_days ( datetime, 1 , true ) ,
124
+ "tomorrow" => add_days ( datetime, 1 , false ) ,
125
+ "now" | "today" => Some ( datetime) ,
126
+ _ => capture
127
+ . name ( "weekday" )
128
+ . and_then ( |weekday| parse_weekday ( weekday. as_str ( ) ) )
129
+ . and_then ( |weekday| adjust_for_weekday ( datetime, weekday, value, is_ago) ) ,
127
130
} ;
128
131
datetime = match new_datetime {
129
132
Some ( dt) => dt,
@@ -148,6 +151,25 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
148
151
}
149
152
}
150
153
154
+ fn adjust_for_weekday < T : TimeZone > (
155
+ mut datetime : DateTime < T > ,
156
+ weekday : Weekday ,
157
+ mut amount : i64 ,
158
+ is_ago : bool ,
159
+ ) -> Option < DateTime < T > > {
160
+ let mut same_day = true ;
161
+ // last/this/next <weekday> truncates the time to midnight
162
+ datetime = datetime. with_time ( NaiveTime :: MIN ) . unwrap ( ) ;
163
+ while datetime. weekday ( ) != weekday {
164
+ datetime = add_days ( datetime, 1 , is_ago) ?;
165
+ same_day = false ;
166
+ }
167
+ if !same_day && 0 < amount {
168
+ amount -= 1 ;
169
+ }
170
+ add_days ( datetime, amount * 7 , is_ago)
171
+ }
172
+
151
173
fn add_months < T : TimeZone > (
152
174
datetime : DateTime < T > ,
153
175
months : i64 ,
@@ -810,4 +832,193 @@ mod tests {
810
832
let result = parse_relative_time_at_date ( now, "invalid 1r" ) ;
811
833
assert_eq ! ( result, Err ( ParseDateTimeError :: InvalidInput ) ) ;
812
834
}
835
+
836
+ #[ test]
837
+ fn test_parse_relative_time_at_date_this_weekday ( ) {
838
+ // Jan 1 2025 is a Wed
839
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
840
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
841
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
842
+ ) ) ;
843
+ // Check "this <same weekday>"
844
+ assert_eq ! (
845
+ parse_relative_time_at_date( now, "this wednesday" ) . unwrap( ) ,
846
+ now
847
+ ) ;
848
+ assert_eq ! ( parse_relative_time_at_date( now, "this wed" ) . unwrap( ) , now) ;
849
+ // Other days
850
+ assert_eq ! (
851
+ parse_relative_time_at_date( now, "this thursday" ) . unwrap( ) ,
852
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
853
+ ) ;
854
+ assert_eq ! (
855
+ parse_relative_time_at_date( now, "this thur" ) . unwrap( ) ,
856
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
857
+ ) ;
858
+ assert_eq ! (
859
+ parse_relative_time_at_date( now, "this thu" ) . unwrap( ) ,
860
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
861
+ ) ;
862
+ assert_eq ! (
863
+ parse_relative_time_at_date( now, "this friday" ) . unwrap( ) ,
864
+ now. checked_add_days( Days :: new( 2 ) ) . unwrap( )
865
+ ) ;
866
+ assert_eq ! (
867
+ parse_relative_time_at_date( now, "this fri" ) . unwrap( ) ,
868
+ now. checked_add_days( Days :: new( 2 ) ) . unwrap( )
869
+ ) ;
870
+ assert_eq ! (
871
+ parse_relative_time_at_date( now, "this saturday" ) . unwrap( ) ,
872
+ now. checked_add_days( Days :: new( 3 ) ) . unwrap( )
873
+ ) ;
874
+ assert_eq ! (
875
+ parse_relative_time_at_date( now, "this sat" ) . unwrap( ) ,
876
+ now. checked_add_days( Days :: new( 3 ) ) . unwrap( )
877
+ ) ;
878
+ // "this" with a day of the week that comes before today should return the next instance of
879
+ // that day
880
+ assert_eq ! (
881
+ parse_relative_time_at_date( now, "this sunday" ) . unwrap( ) ,
882
+ now. checked_add_days( Days :: new( 4 ) ) . unwrap( )
883
+ ) ;
884
+ assert_eq ! (
885
+ parse_relative_time_at_date( now, "this sun" ) . unwrap( ) ,
886
+ now. checked_add_days( Days :: new( 4 ) ) . unwrap( )
887
+ ) ;
888
+ assert_eq ! (
889
+ parse_relative_time_at_date( now, "this monday" ) . unwrap( ) ,
890
+ now. checked_add_days( Days :: new( 5 ) ) . unwrap( )
891
+ ) ;
892
+ assert_eq ! (
893
+ parse_relative_time_at_date( now, "this mon" ) . unwrap( ) ,
894
+ now. checked_add_days( Days :: new( 5 ) ) . unwrap( )
895
+ ) ;
896
+ assert_eq ! (
897
+ parse_relative_time_at_date( now, "this tuesday" ) . unwrap( ) ,
898
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
899
+ ) ;
900
+ assert_eq ! (
901
+ parse_relative_time_at_date( now, "this tue" ) . unwrap( ) ,
902
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
903
+ ) ;
904
+ }
905
+
906
+ #[ test]
907
+ fn test_parse_relative_time_at_date_last_weekday ( ) {
908
+ // Jan 1 2025 is a Wed
909
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
910
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
911
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
912
+ ) ) ;
913
+ // Check "last <same weekday>"
914
+ assert_eq ! (
915
+ parse_relative_time_at_date( now, "last wed" ) . unwrap( ) ,
916
+ now. checked_sub_days( Days :: new( 7 ) ) . unwrap( )
917
+ ) ;
918
+ // Check "last <day after today>"
919
+ assert_eq ! (
920
+ parse_relative_time_at_date( now, "last thu" ) . unwrap( ) ,
921
+ now. checked_sub_days( Days :: new( 6 ) ) . unwrap( )
922
+ ) ;
923
+ // Check "last <day before today>"
924
+ assert_eq ! (
925
+ parse_relative_time_at_date( now, "last tue" ) . unwrap( ) ,
926
+ now. checked_sub_days( Days :: new( 1 ) ) . unwrap( )
927
+ ) ;
928
+ }
929
+
930
+ #[ test]
931
+ fn test_parse_relative_time_at_date_next_weekday ( ) {
932
+ // Jan 1 2025 is a Wed
933
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
934
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
935
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
936
+ ) ) ;
937
+ // Check "next <same weekday>"
938
+ assert_eq ! (
939
+ parse_relative_time_at_date( now, "next wed" ) . unwrap( ) ,
940
+ now. checked_add_days( Days :: new( 7 ) ) . unwrap( )
941
+ ) ;
942
+ // Check "next <day after today>"
943
+ assert_eq ! (
944
+ parse_relative_time_at_date( now, "next thu" ) . unwrap( ) ,
945
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
946
+ ) ;
947
+ // Check "next <day before today>"
948
+ assert_eq ! (
949
+ parse_relative_time_at_date( now, "next tue" ) . unwrap( ) ,
950
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
951
+ ) ;
952
+ }
953
+
954
+ #[ test]
955
+ fn test_parse_relative_time_at_date_number_weekday ( ) {
956
+ // Jan 1 2025 is a Wed
957
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
958
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
959
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
960
+ ) ) ;
961
+ assert_eq ! (
962
+ parse_relative_time_at_date( now, "1 wed" ) . unwrap( ) ,
963
+ now. checked_add_days( Days :: new( 7 ) ) . unwrap( )
964
+ ) ;
965
+ assert_eq ! (
966
+ parse_relative_time_at_date( now, "1 thu" ) . unwrap( ) ,
967
+ now. checked_add_days( Days :: new( 1 ) ) . unwrap( )
968
+ ) ;
969
+ assert_eq ! (
970
+ parse_relative_time_at_date( now, "1 tue" ) . unwrap( ) ,
971
+ now. checked_add_days( Days :: new( 6 ) ) . unwrap( )
972
+ ) ;
973
+ assert_eq ! (
974
+ parse_relative_time_at_date( now, "2 wed" ) . unwrap( ) ,
975
+ now. checked_add_days( Days :: new( 14 ) ) . unwrap( )
976
+ ) ;
977
+ assert_eq ! (
978
+ parse_relative_time_at_date( now, "2 thu" ) . unwrap( ) ,
979
+ now. checked_add_days( Days :: new( 8 ) ) . unwrap( )
980
+ ) ;
981
+ assert_eq ! (
982
+ parse_relative_time_at_date( now, "2 tue" ) . unwrap( ) ,
983
+ now. checked_add_days( Days :: new( 13 ) ) . unwrap( )
984
+ ) ;
985
+ }
986
+
987
+ #[ test]
988
+ fn test_parse_relative_time_at_date_weekday_truncates_time ( ) {
989
+ // Jan 1 2025 is a Wed
990
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
991
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
992
+ NaiveTime :: from_hms_opt ( 12 , 0 , 0 ) . unwrap ( ) ,
993
+ ) ) ;
994
+ let now_midnight = Utc . from_utc_datetime ( & NaiveDateTime :: new (
995
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
996
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
997
+ ) ) ;
998
+ assert_eq ! (
999
+ parse_relative_time_at_date( now, "this wed" ) . unwrap( ) ,
1000
+ now_midnight
1001
+ ) ;
1002
+ assert_eq ! (
1003
+ parse_relative_time_at_date( now, "last wed" ) . unwrap( ) ,
1004
+ now_midnight. checked_sub_days( Days :: new( 7 ) ) . unwrap( )
1005
+ ) ;
1006
+ assert_eq ! (
1007
+ parse_relative_time_at_date( now, "next wed" ) . unwrap( ) ,
1008
+ now_midnight. checked_add_days( Days :: new( 7 ) ) . unwrap( )
1009
+ ) ;
1010
+ }
1011
+
1012
+ #[ test]
1013
+ fn test_parse_relative_time_at_date_invalid_weekday ( ) {
1014
+ // Jan 1 2025 is a Wed
1015
+ let now = Utc . from_utc_datetime ( & NaiveDateTime :: new (
1016
+ NaiveDate :: from_ymd_opt ( 2025 , 1 , 1 ) . unwrap ( ) ,
1017
+ NaiveTime :: from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ,
1018
+ ) ) ;
1019
+ assert_eq ! (
1020
+ parse_relative_time_at_date( now, "this fooday" ) ,
1021
+ Err ( ParseDateTimeError :: InvalidInput )
1022
+ ) ;
1023
+ }
813
1024
}
0 commit comments