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:: {
55 DateTime , Datelike , Days , Duration , LocalResult , Months , NaiveDate , NaiveDateTime , TimeZone ,
6+ 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>[-+]?\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 ) ?;
@@ -77,16 +78,19 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
7778 . name ( "value" )
7879 . ok_or ( ParseDateTimeError :: InvalidInput ) ?
7980 . as_str ( ) ;
81+ let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
8082 let value = if value_str. is_empty ( ) {
81- 1
83+ if direction == "this" {
84+ 0
85+ } else {
86+ 1
87+ }
8288 } else {
8389 value_str
8490 . parse :: < i64 > ( )
8591 . map_err ( |_| ParseDateTimeError :: InvalidInput ) ?
8692 } ;
8793
88- let direction = capture. name ( "direction" ) . map_or ( "" , |d| d. as_str ( ) ) ;
89-
9094 if direction == "last" {
9195 is_ago = true ;
9296 }
@@ -100,27 +104,26 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
100104 is_ago = true ;
101105 }
102106
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)
123119 }
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) ) ,
124127 } ;
125128 datetime = match new_datetime {
126129 Some ( dt) => dt,
@@ -145,6 +148,23 @@ pub fn parse_relative_time_at_date<T: TimeZone>(
145148 }
146149}
147150
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+
148168fn add_months < T : TimeZone > (
149169 datetime : DateTime < T > ,
150170 months : i64 ,
@@ -794,4 +814,168 @@ mod tests {
794814 let result = parse_relative_time_at_date ( now, "invalid 1r" ) ;
795815 assert_eq ! ( result, Err ( ParseDateTimeError :: InvalidInput ) ) ;
796816 }
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+ }
797981}
0 commit comments