11// For the full copyright and license information, please view the LICENSE
22// file that was distributed with this source code.
3- use crate :: ParseDateTimeError ;
3+ use crate :: { parse_weekday :: parse_weekday , ParseDateTimeError } ;
44use chrono:: {
5- DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , TimeZone ,
5+ DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , NaiveTime ,
6+ TimeZone , Weekday ,
67} ;
78use regex:: Regex ;
89
@@ -61,7 +62,7 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
6162 r"(?x)
6263 (?:(?P<value>[-+]?\s*\d*)\s*)?
6364 (\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
6566 (\s*(?P<separator>and|,)?\s*)?
6667 (\s*(?P<ago>ago)?)?" ,
6768 ) ?;
@@ -80,16 +81,19 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
8081 . chars ( )
8182 . filter ( |c| !c. is_whitespace ( ) ) // Remove potential space between +/- and number
8283 . collect ( ) ;
84+ let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
8385 let value = if value_str. is_empty ( ) {
84- 1
86+ if direction == "this" {
87+ 0
88+ } else {
89+ 1
90+ }
8591 } else {
8692 value_str
8793 . parse :: < i64 > ( )
8894 . map_err ( |_| ParseDateTimeError :: InvalidInput ) ?
8995 } ;
9096
91- let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
92-
9397 if direction == "last" {
9498 is_ago = true ;
9599 }
@@ -103,27 +107,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
103107 is_ago = true ;
104108 }
105109
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)
126122 }
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) ) ,
127130 } ;
128131 datetime = match new_datetime {
129132 Some ( dt) => dt,
@@ -148,6 +151,25 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
148151 }
149152}
150153
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+
151173fn add_months < T : TimeZone > (
152174 datetime : DateTime < T > ,
153175 months : i64 ,
@@ -810,4 +832,193 @@ mod tests {
810832 let result = parse_relative_time_at_date ( now, "invalid 1r" ) ;
811833 assert_eq ! ( result, Err ( ParseDateTimeError :: InvalidInput ) ) ;
812834 }
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+ }
8131024}
0 commit comments