11use crate :: config:: ScheduleConfig ;
22use crate :: error:: SessionError ;
3- use chrono:: { DateTime , Datelike , NaiveTime , Utc , Weekday } ;
3+ use chrono:: { DateTime , Datelike , Days , NaiveDate , NaiveTime , TimeDelta , Utc , Weekday } ;
44use chrono_tz:: Tz ;
55
6+ type Result < T , E = SessionError > = std:: result:: Result < T , E > ;
7+
68#[ derive( Clone , Debug ) ]
79#[ allow( dead_code) ]
810pub enum SessionSchedule {
@@ -53,6 +55,81 @@ impl SessionSchedule {
5355 }
5456 }
5557
58+ pub fn is_same_session_period ( & self , dt1 : & DateTime < Utc > , dt2 : & DateTime < Utc > ) -> Result < bool > {
59+ if !self . is_active_at ( dt1) || !self . is_active_at ( dt2) {
60+ return Err ( SessionError :: InvalidSchedule (
61+ "Time doesn't fall in any session period" . to_string ( ) ,
62+ ) ) ;
63+ }
64+
65+ let ( start, end) = self . get_session_start_and_end ( dt1) ?;
66+ Ok ( start <= * dt2 && * dt2 < end)
67+ }
68+
69+ fn get_session_start_and_end (
70+ & self ,
71+ datetime : & DateTime < Utc > ,
72+ ) -> Result < ( DateTime < Utc > , DateTime < Utc > ) > {
73+ match self {
74+ SessionSchedule :: NonStop => {
75+ Ok ( ( DateTime :: default ( ) , Utc :: now ( ) + TimeDelta :: weeks ( 1000 ) ) )
76+ }
77+ SessionSchedule :: Daily {
78+ start_time,
79+ end_time,
80+ timezone,
81+ } => {
82+ let local_datetime = datetime. with_timezone ( timezone) ;
83+
84+ if local_datetime. time ( ) >= * start_time {
85+ // if the datetime is greater than the start_time, they fall on the same day
86+ let start =
87+ Self :: construct_utc ( & local_datetime. date_naive ( ) , start_time, timezone) ?;
88+
89+ // if the end_time is smaller than the start_time, then it must be the next day
90+ let end_date = if end_time < start_time {
91+ local_datetime
92+ . date_naive ( )
93+ . checked_add_days ( Days :: new ( 1 ) )
94+ . ok_or_else ( || {
95+ SessionError :: InvalidSchedule ( "Failed to add day" . to_string ( ) )
96+ } ) ?
97+ } else {
98+ local_datetime. date_naive ( )
99+ } ;
100+ let end = Self :: construct_utc ( & end_date, end_time, timezone) ?;
101+ Ok ( ( start, end) )
102+ } else {
103+ // if the datetime is lesser than the start_time, it must fall on the previous day
104+ let start_date = local_datetime
105+ . date_naive ( )
106+ . checked_sub_days ( Days :: new ( 1 ) )
107+ . ok_or_else ( || {
108+ SessionError :: InvalidSchedule ( "Failed to get previous day" . to_string ( ) )
109+ } ) ?;
110+ let start = Self :: construct_utc ( & start_date, start_time, timezone) ?;
111+ let end =
112+ Self :: construct_utc ( & local_datetime. date_naive ( ) , end_time, timezone) ?;
113+ Ok ( ( start, end) )
114+ }
115+ }
116+ SessionSchedule :: Weekdays { .. } => unimplemented ! ( ) ,
117+ SessionSchedule :: Weekly { .. } => unimplemented ! ( ) ,
118+ }
119+ }
120+
121+ fn construct_utc ( date : & NaiveDate , time : & NaiveTime , timezone : & Tz ) -> Result < DateTime < Utc > > {
122+ // TODO: do we want to handle Ambiguous and None outcomes?
123+ // these variants correspond to Python's gap and fold: https://peps.python.org/pep-0495/#terminology
124+ if let Some ( dt) = date. and_time ( * time) . and_local_timezone ( * timezone) . single ( ) {
125+ Ok ( dt. to_utc ( ) )
126+ } else {
127+ Err ( SessionError :: InvalidSchedule (
128+ "Invalid schedule configuration: invalid time" . to_string ( ) ,
129+ ) )
130+ }
131+ }
132+
56133 fn check_daily_schedule (
57134 datetime : & DateTime < Tz > ,
58135 start_time : & NaiveTime ,
@@ -288,7 +365,7 @@ mod tests {
288365 } ;
289366
290367 let before_start = DateTime :: from_naive_utc_and_offset (
291- chrono :: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
368+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
292369 . unwrap ( )
293370 . and_hms_opt ( 7 , 59 , 59 )
294371 . unwrap ( ) ,
@@ -298,7 +375,7 @@ mod tests {
298375
299376 // 8AM UTC is 9AM London time, so already in session
300377 let at_start = DateTime :: from_naive_utc_and_offset (
301- chrono :: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
378+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
302379 . unwrap ( )
303380 . and_hms_opt ( 8 , 0 , 0 )
304381 . unwrap ( ) ,
@@ -1056,4 +1133,53 @@ mod tests {
10561133 let schedule = SessionSchedule :: try_from ( & config) ;
10571134 assert ! ( schedule. is_err( ) ) ;
10581135 }
1136+
1137+ #[ test]
1138+ fn test_is_same_session_period_daily_simple_utc ( ) {
1139+ let schedule = SessionSchedule :: Daily {
1140+ start_time : NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
1141+ end_time : NaiveTime :: from_hms_opt ( 17 , 0 , 0 ) . unwrap ( ) ,
1142+ timezone : Tz :: UTC ,
1143+ } ;
1144+
1145+ // two times within the same session period return true
1146+ let dt1 = DateTime :: from_naive_utc_and_offset (
1147+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
1148+ . unwrap ( )
1149+ . and_hms_opt ( 10 , 0 , 0 )
1150+ . unwrap ( ) ,
1151+ Utc ,
1152+ ) ;
1153+ let dt2 = DateTime :: from_naive_utc_and_offset (
1154+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
1155+ . unwrap ( )
1156+ . and_hms_opt ( 15 , 0 , 0 )
1157+ . unwrap ( ) ,
1158+ Utc ,
1159+ ) ;
1160+ assert ! ( schedule. is_same_session_period( & dt1, & dt2) . unwrap( ) ) ;
1161+ assert ! ( schedule. is_same_session_period( & dt2, & dt1) . unwrap( ) ) ;
1162+
1163+ // time for the next session period returns false
1164+ let dt3 = DateTime :: from_naive_utc_and_offset (
1165+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 28 )
1166+ . unwrap ( )
1167+ . and_hms_opt ( 10 , 0 , 0 )
1168+ . unwrap ( ) ,
1169+ Utc ,
1170+ ) ;
1171+ assert ! ( !schedule. is_same_session_period( & dt1, & dt3) . unwrap( ) ) ;
1172+ assert ! ( !schedule. is_same_session_period( & dt3, & dt1) . unwrap( ) ) ;
1173+
1174+ // time on the same day but outside session time returns error
1175+ let dt4 = DateTime :: from_naive_utc_and_offset (
1176+ NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
1177+ . unwrap ( )
1178+ . and_hms_opt ( 19 , 0 , 0 )
1179+ . unwrap ( ) ,
1180+ Utc ,
1181+ ) ;
1182+ assert ! ( schedule. is_same_session_period( & dt1, & dt4) . is_err( ) ) ;
1183+ assert ! ( schedule. is_same_session_period( & dt4, & dt1) . is_err( ) ) ;
1184+ }
10591185}
0 commit comments