@@ -35,8 +35,11 @@ impl SessionSchedule {
3535 SessionSchedule :: Daily {
3636 start_time,
3737 end_time,
38- timezone : _,
39- } => Self :: check_daily_schedule ( datetime, start_time, end_time) ,
38+ timezone,
39+ } => {
40+ let adjusted_datetime = datetime. with_timezone ( timezone) ;
41+ Self :: check_daily_schedule ( & adjusted_datetime, start_time, end_time)
42+ }
4043 SessionSchedule :: Weekdays {
4144 weekdays,
4245 start_time,
@@ -48,14 +51,14 @@ impl SessionSchedule {
4851 }
4952
5053 fn check_daily_schedule (
51- datetime : & DateTime < Utc > ,
54+ datetime : & DateTime < Tz > ,
5255 start_time : & NaiveTime ,
5356 end_time : & NaiveTime ,
5457 ) -> bool {
5558 if start_time < end_time {
56- & datetime. time ( ) >= start_time && & datetime. time ( ) <= end_time
59+ & datetime. time ( ) >= start_time && & datetime. time ( ) < end_time
5760 } else {
58- & datetime. time ( ) >= start_time || & datetime. time ( ) <= end_time
61+ & datetime. time ( ) >= start_time || & datetime. time ( ) < end_time
5962 }
6063 }
6164
@@ -184,6 +187,216 @@ mod tests {
184187 use super :: * ;
185188 use chrono:: { NaiveTime , Weekday } ;
186189
190+ #[ test]
191+ fn test_active_at_non_stop_schedule ( ) {
192+ // non-stop schedules are always active
193+ let schedule = SessionSchedule :: NonStop ;
194+ assert ! ( schedule. is_active_at( & Utc :: now( ) ) )
195+ }
196+
197+ #[ test]
198+ fn test_active_at_daily_schedule_utc ( ) {
199+ let schedule = SessionSchedule :: Daily {
200+ start_time : NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
201+ end_time : NaiveTime :: from_hms_opt ( 17 , 0 , 0 ) . unwrap ( ) ,
202+ timezone : Tz :: UTC ,
203+ } ;
204+
205+ // just before start time (8:59:59)
206+ let before_start = DateTime :: from_naive_utc_and_offset (
207+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
208+ . unwrap ( )
209+ . and_hms_opt ( 8 , 59 , 59 )
210+ . unwrap ( ) ,
211+ Utc ,
212+ ) ;
213+ assert ! ( !schedule. is_active_at( & before_start) ) ;
214+
215+ // just after start time (9:00:01)
216+ let after_start = DateTime :: from_naive_utc_and_offset (
217+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
218+ . unwrap ( )
219+ . and_hms_opt ( 9 , 0 , 1 )
220+ . unwrap ( ) ,
221+ Utc ,
222+ ) ;
223+ assert ! ( schedule. is_active_at( & after_start) ) ;
224+
225+ // in the middle (13:00:00)
226+ let middle = DateTime :: from_naive_utc_and_offset (
227+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
228+ . unwrap ( )
229+ . and_hms_opt ( 13 , 0 , 0 )
230+ . unwrap ( ) ,
231+ Utc ,
232+ ) ;
233+ assert ! ( schedule. is_active_at( & middle) ) ;
234+
235+ // just before end time (16:59:59)
236+ let before_end = DateTime :: from_naive_utc_and_offset (
237+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
238+ . unwrap ( )
239+ . and_hms_opt ( 16 , 59 , 59 )
240+ . unwrap ( ) ,
241+ Utc ,
242+ ) ;
243+ assert ! ( schedule. is_active_at( & before_end) ) ;
244+
245+ // at end time (17:00:00) - we expect false at exactly the end time (non-inclusive)
246+ let at_end = DateTime :: from_naive_utc_and_offset (
247+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
248+ . unwrap ( )
249+ . and_hms_opt ( 17 , 0 , 0 )
250+ . unwrap ( ) ,
251+ Utc ,
252+ ) ;
253+ assert ! ( !schedule. is_active_at( & at_end) ) ;
254+
255+ // after end time (17:00:01)
256+ let after_end = DateTime :: from_naive_utc_and_offset (
257+ chrono:: NaiveDate :: from_ymd_opt ( 2024 , 1 , 1 )
258+ . unwrap ( )
259+ . and_hms_opt ( 17 , 0 , 1 )
260+ . unwrap ( ) ,
261+ Utc ,
262+ ) ;
263+ assert ! ( !schedule. is_active_at( & after_end) ) ;
264+ }
265+
266+ #[ test]
267+ fn test_active_at_daily_schedule_london ( ) {
268+ // we'll use 27/06/2025 as the date
269+ // London is an hour ahead of UTC
270+ let schedule = SessionSchedule :: Daily {
271+ start_time : NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
272+ end_time : NaiveTime :: from_hms_opt ( 17 , 0 , 0 ) . unwrap ( ) ,
273+ timezone : Tz :: Europe__London ,
274+ } ;
275+
276+ let before_start = DateTime :: from_naive_utc_and_offset (
277+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
278+ . unwrap ( )
279+ . and_hms_opt ( 7 , 59 , 59 )
280+ . unwrap ( ) ,
281+ Utc ,
282+ ) ;
283+ assert ! ( !schedule. is_active_at( & before_start) ) ;
284+
285+ // 8AM UTC is 9AM London time, so already in session
286+ let at_start = DateTime :: from_naive_utc_and_offset (
287+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
288+ . unwrap ( )
289+ . and_hms_opt ( 8 , 0 , 0 )
290+ . unwrap ( ) ,
291+ Utc ,
292+ ) ;
293+ assert ! ( schedule. is_active_at( & at_start) ) ;
294+
295+ // 4PM UTC is 5PM London time, so already out of session
296+ let at_end = DateTime :: from_naive_utc_and_offset (
297+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
298+ . unwrap ( )
299+ . and_hms_opt ( 16 , 0 , 0 )
300+ . unwrap ( ) ,
301+ Utc ,
302+ ) ;
303+ assert ! ( !schedule. is_active_at( & at_end) ) ;
304+ }
305+
306+ #[ test]
307+ fn test_active_at_daily_schedule_london_end_before_start ( ) {
308+ // we'll use 27/06/2025 as the date
309+ // London is an hour ahead of UTC
310+ let schedule_1 = SessionSchedule :: Daily {
311+ start_time : NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
312+ end_time : NaiveTime :: from_hms_opt ( 2 , 0 , 0 ) . unwrap ( ) ,
313+ timezone : Tz :: Europe__London ,
314+ } ;
315+
316+ let before_start = DateTime :: from_naive_utc_and_offset (
317+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
318+ . unwrap ( )
319+ . and_hms_opt ( 7 , 59 , 59 )
320+ . unwrap ( ) ,
321+ Utc ,
322+ ) ;
323+ assert ! ( !schedule_1. is_active_at( & before_start) ) ;
324+
325+ let at_start = DateTime :: from_naive_utc_and_offset (
326+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
327+ . unwrap ( )
328+ . and_hms_opt ( 8 , 0 , 0 )
329+ . unwrap ( ) ,
330+ Utc ,
331+ ) ;
332+ assert ! ( schedule_1. is_active_at( & at_start) ) ;
333+
334+ let before_end = DateTime :: from_naive_utc_and_offset (
335+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
336+ . unwrap ( )
337+ . and_hms_opt ( 0 , 59 , 59 )
338+ . unwrap ( ) ,
339+ Utc ,
340+ ) ;
341+ assert ! ( schedule_1. is_active_at( & before_end) ) ;
342+
343+ let at_end = DateTime :: from_naive_utc_and_offset (
344+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
345+ . unwrap ( )
346+ . and_hms_opt ( 1 , 0 , 0 )
347+ . unwrap ( ) ,
348+ Utc ,
349+ ) ;
350+ assert ! ( !schedule_1. is_active_at( & at_end) ) ;
351+ }
352+
353+ #[ test]
354+ fn test_active_at_daily_schedule_london_end_before_start_tz_crossing_midnight ( ) {
355+ // we'll use 27/06/2025 as the date
356+ // London is an hour ahead of UTC
357+ let schedule_1 = SessionSchedule :: Daily {
358+ start_time : NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
359+ end_time : NaiveTime :: from_hms_opt ( 0 , 30 , 0 ) . unwrap ( ) ,
360+ timezone : Tz :: Europe__London ,
361+ } ;
362+
363+ let before_start = DateTime :: from_naive_utc_and_offset (
364+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
365+ . unwrap ( )
366+ . and_hms_opt ( 7 , 59 , 59 )
367+ . unwrap ( ) ,
368+ Utc ,
369+ ) ;
370+ assert ! ( !schedule_1. is_active_at( & before_start) ) ;
371+
372+ let at_start = DateTime :: from_naive_utc_and_offset (
373+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
374+ . unwrap ( )
375+ . and_hms_opt ( 8 , 0 , 0 )
376+ . unwrap ( ) ,
377+ Utc ,
378+ ) ;
379+ assert ! ( schedule_1. is_active_at( & at_start) ) ;
380+
381+ let before_end = DateTime :: from_naive_utc_and_offset (
382+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
383+ . unwrap ( )
384+ . and_hms_opt ( 23 , 29 , 59 )
385+ . unwrap ( ) ,
386+ Utc ,
387+ ) ;
388+ assert ! ( schedule_1. is_active_at( & before_end) ) ;
389+
390+ let at_end = DateTime :: from_naive_utc_and_offset (
391+ chrono:: NaiveDate :: from_ymd_opt ( 2025 , 6 , 27 )
392+ . unwrap ( )
393+ . and_hms_opt ( 23 , 30 , 0 )
394+ . unwrap ( ) ,
395+ Utc ,
396+ ) ;
397+ assert ! ( !schedule_1. is_active_at( & at_end) ) ;
398+ }
399+
187400 #[ test]
188401 fn test_into_non_stop_no_config ( ) {
189402 let config = ScheduleConfig {
0 commit comments