9
9
chrono:: {
10
10
naive:: NaiveTime ,
11
11
DateTime ,
12
+ Datelike ,
12
13
Duration ,
13
- TimeZone ,
14
+ Utc ,
14
15
Weekday ,
15
16
} ,
16
17
chrono_tz:: Tz ,
@@ -27,8 +28,8 @@ lazy_static! {
27
28
}
28
29
29
30
/// Weekly market hours schedule
30
- #[ derive( Default , Debug , Eq , PartialEq ) ]
31
- pub struct MarketHours {
31
+ #[ derive( Clone , Default , Debug , Eq , PartialEq ) ]
32
+ pub struct WeeklySchedule {
32
33
pub timezone : Tz ,
33
34
pub mon : MHKind ,
34
35
pub tue : MHKind ,
@@ -39,7 +40,7 @@ pub struct MarketHours {
39
40
pub sun : MHKind ,
40
41
}
41
42
42
- impl MarketHours {
43
+ impl WeeklySchedule {
43
44
pub fn all_closed ( ) -> Self {
44
45
Self {
45
46
timezone : Default :: default ( ) ,
@@ -53,13 +54,11 @@ impl MarketHours {
53
54
}
54
55
}
55
56
56
- pub fn can_publish_at < Tz : TimeZone > ( & self , when : & DateTime < Tz > ) -> Result < bool > {
57
+ pub fn can_publish_at ( & self , when : & DateTime < Utc > ) -> bool {
57
58
// Convert to time local to the market
58
59
let when_market_local = when. with_timezone ( & self . timezone ) ;
59
60
60
- // NOTE(2023-11-21): Strangely enough, I couldn't find a
61
- // method that gets the programmatic Weekday from a DateTime.
62
- let market_weekday: Weekday = when_market_local. format ( "%A" ) . to_string ( ) . parse ( ) ?;
61
+ let market_weekday: Weekday = when_market_local. date_naive ( ) . weekday ( ) ;
63
62
64
63
let market_time = when_market_local. time ( ) ;
65
64
@@ -73,11 +72,11 @@ impl MarketHours {
73
72
Weekday :: Sun => self . sun . can_publish_at ( market_time) ,
74
73
} ;
75
74
76
- Ok ( ret)
75
+ ret
77
76
}
78
77
}
79
78
80
- impl FromStr for MarketHours {
79
+ impl FromStr for WeeklySchedule {
81
80
type Err = anyhow:: Error ;
82
81
fn from_str ( s : & str ) -> Result < Self > {
83
82
let mut split_by_commas = s. split ( "," ) ;
@@ -163,7 +162,7 @@ impl FromStr for MarketHours {
163
162
}
164
163
165
164
/// Helper enum for denoting per-day schedules: time range, all-day open and all-day closed.
166
- #[ derive( Debug , Eq , PartialEq ) ]
165
+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
167
166
pub enum MHKind {
168
167
Open ,
169
168
Closed ,
@@ -236,9 +235,9 @@ mod tests {
236
235
// Mon-Fri 9-5, inconsistent leading space on Tuesday, leading 0 on Friday (expected to be fine)
237
236
let s = "Europe/Warsaw,9:00-17:00, 9:00-17:00,9:00-17:00,9:00-17:00,09:00-17:00,C,C" ;
238
237
239
- let parsed: MarketHours = s. parse ( ) ?;
238
+ let parsed: WeeklySchedule = s. parse ( ) ?;
240
239
241
- let expected = MarketHours {
240
+ let expected = WeeklySchedule {
242
241
timezone : Tz :: Europe__Warsaw ,
243
242
mon : MHKind :: TimeRange (
244
243
NaiveTime :: from_hms_opt ( 9 , 0 , 0 ) . unwrap ( ) ,
@@ -274,7 +273,7 @@ mod tests {
274
273
// Valid but missing a timezone
275
274
let s = "O,C,O,C,O,C,O" ;
276
275
277
- let parsing_result: Result < MarketHours > = s. parse ( ) ;
276
+ let parsing_result: Result < WeeklySchedule > = s. parse ( ) ;
278
277
279
278
dbg ! ( & parsing_result) ;
280
279
assert ! ( parsing_result. is_err( ) ) ;
@@ -285,7 +284,7 @@ mod tests {
285
284
// One day short
286
285
let s = "Asia/Hong_Kong,C,O,C,O,C,O" ;
287
286
288
- let parsing_result: Result < MarketHours > = s. parse ( ) ;
287
+ let parsing_result: Result < WeeklySchedule > = s. parse ( ) ;
289
288
290
289
dbg ! ( & parsing_result) ;
291
290
assert ! ( parsing_result. is_err( ) ) ;
@@ -295,7 +294,7 @@ mod tests {
295
294
fn test_parsing_gibberish_timezone_is_error ( ) {
296
295
// Pretty sure that one's extinct
297
296
let s = "Pangea/New_Dino_City,O,O,O,O,O,O,O" ;
298
- let parsing_result: Result < MarketHours > = s. parse ( ) ;
297
+ let parsing_result: Result < WeeklySchedule > = s. parse ( ) ;
299
298
300
299
dbg ! ( & parsing_result) ;
301
300
assert ! ( parsing_result. is_err( ) ) ;
@@ -304,7 +303,7 @@ mod tests {
304
303
#[ test]
305
304
fn test_parsing_gibberish_day_schedule_is_error ( ) {
306
305
let s = "Europe/Amsterdam,mondays are alright I guess,O,O,O,O,O,O" ;
307
- let parsing_result: Result < MarketHours > = s. parse ( ) ;
306
+ let parsing_result: Result < WeeklySchedule > = s. parse ( ) ;
308
307
309
308
dbg ! ( & parsing_result) ;
310
309
assert ! ( parsing_result. is_err( ) ) ;
@@ -314,7 +313,7 @@ mod tests {
314
313
fn test_parsing_too_many_days_is_error ( ) {
315
314
// One day too many
316
315
let s = "Europe/Lisbon,O,O,O,O,O,O,O,O,C" ;
317
- let parsing_result: Result < MarketHours > = s. parse ( ) ;
316
+ let parsing_result: Result < WeeklySchedule > = s. parse ( ) ;
318
317
319
318
dbg ! ( & parsing_result) ;
320
319
assert ! ( parsing_result. is_err( ) ) ;
@@ -323,7 +322,7 @@ mod tests {
323
322
#[ test]
324
323
fn test_market_hours_happy_path ( ) -> Result < ( ) > {
325
324
// Prepare a schedule of narrow ranges
326
- let mh : MarketHours = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00" . parse ( ) ?;
325
+ let wsched : WeeklySchedule = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00" . parse ( ) ?;
327
326
328
327
// Prepare UTC datetimes that fall before, within and after market hours
329
328
let format = "%Y-%m-%d %H:%M" ;
@@ -357,7 +356,7 @@ mod tests {
357
356
NaiveDateTime :: parse_from_str( "2023-11-26 12:30" , format) ?. and_utc( ) ,
358
357
] ;
359
358
360
- dbg ! ( & mh ) ;
359
+ dbg ! ( & wsched ) ;
361
360
362
361
for ( ( before_dt, ok_dt) , after_dt) in bad_datetimes_before
363
362
. iter ( )
@@ -368,9 +367,9 @@ mod tests {
368
367
dbg ! ( & ok_dt) ;
369
368
dbg ! ( & after_dt) ;
370
369
371
- assert ! ( !mh . can_publish_at( before_dt) ? ) ;
372
- assert ! ( mh . can_publish_at( ok_dt) ? ) ;
373
- assert ! ( !mh . can_publish_at( after_dt) ? ) ;
370
+ assert ! ( !wsched . can_publish_at( before_dt) ) ;
371
+ assert ! ( wsched . can_publish_at( ok_dt) ) ;
372
+ assert ! ( !wsched . can_publish_at( after_dt) ) ;
374
373
}
375
374
376
375
Ok ( ( ) )
@@ -380,7 +379,8 @@ mod tests {
380
379
#[ test]
381
380
fn test_market_hours_midnight_00_24 ( ) -> Result < ( ) > {
382
381
// Prepare a schedule of midnight-neighboring ranges
383
- let mh: MarketHours = "Europe/Amsterdam,23:00-24:00,00:00-01:00,O,C,C,C,C" . parse ( ) ?;
382
+ let wsched: WeeklySchedule =
383
+ "Europe/Amsterdam,23:00-24:00,00:00-01:00,O,C,C,C,C" . parse ( ) ?;
384
384
385
385
let format = "%Y-%m-%d %H:%M" ;
386
386
let ok_datetimes = vec ! [
@@ -408,16 +408,122 @@ mod tests {
408
408
. unwrap( ) ,
409
409
] ;
410
410
411
- dbg ! ( & mh ) ;
411
+ dbg ! ( & wsched ) ;
412
412
413
413
for ( ok_dt, bad_dt) in ok_datetimes. iter ( ) . zip ( bad_datetimes. iter ( ) ) {
414
414
dbg ! ( & ok_dt) ;
415
415
dbg ! ( & bad_dt) ;
416
416
417
- assert ! ( mh . can_publish_at( ok_dt) ? ) ;
418
- assert ! ( !mh . can_publish_at( bad_dt) ? ) ;
417
+ assert ! ( wsched . can_publish_at( & ok_dt. with_timezone ( & Utc ) ) ) ;
418
+ assert ! ( !wsched . can_publish_at( & bad_dt. with_timezone ( & Utc ) ) ) ;
419
419
}
420
420
421
421
Ok ( ( ) )
422
422
}
423
+
424
+ /// Performs a scenario on 2023 autumn DST change. During that
425
+ /// time, most of the EU switched on the weekend one week earlier
426
+ /// (Oct 28-29) than most of the US (Nov 4-5).
427
+ #[ test]
428
+ fn test_market_hours_dst_shenanigans ( ) -> Result < ( ) > {
429
+ // The Monday schedule is equivalent between Amsterdam and
430
+ // Chicago for most of 2023 (7h difference), except for two
431
+ // instances of Amsterdam/Chicago DST change lag:
432
+ // * Spring 2023: Mar12(US)-Mar26(EU) (clocks go forward 1h,
433
+ // CDT/CET 6h offset in use for 2 weeks, CDT/CEST 7h offset after)
434
+ // * Autumn 2023: Oct29(EU)-Nov5(US) (clocks go back 1h,
435
+ // CDT/CET 6h offset in use 1 week, CST/CET 7h offset after)
436
+ let wsched_eu: WeeklySchedule = "Europe/Amsterdam,9:00-17:00,O,O,O,O,O,O" . parse ( ) ?;
437
+ let wsched_us: WeeklySchedule = "America/Chicago,2:00-10:00,O,O,O,O,O,O" . parse ( ) ?;
438
+
439
+ let format = "%Y-%m-%d %H:%M" ;
440
+
441
+ // Monday after EU change, before US change, from Amsterdam
442
+ // perspective. Okay for publishing Amsterdam market, outside hours for Chicago market
443
+ let dt1 = NaiveDateTime :: parse_from_str ( "2023-10-30 16:01" , format) ?
444
+ . and_local_timezone ( Tz :: Europe__Amsterdam )
445
+ . unwrap ( ) ;
446
+ dbg ! ( & dt1) ;
447
+
448
+ assert ! ( wsched_eu. can_publish_at( & dt1. with_timezone( & Utc ) ) ) ;
449
+ assert ! ( !wsched_us. can_publish_at( & dt1. with_timezone( & Utc ) ) ) ;
450
+
451
+ // Same point in time, from Chicago perspective. Still okay
452
+ // for Amsterdam, still outside hours for Chicago.
453
+ let dt2 = NaiveDateTime :: parse_from_str ( "2023-10-30 10:01" , format) ?
454
+ . and_local_timezone ( Tz :: America__Chicago )
455
+ . unwrap ( ) ;
456
+ dbg ! ( & dt2) ;
457
+
458
+ assert ! ( wsched_eu. can_publish_at( & dt2. with_timezone( & Utc ) ) ) ;
459
+ assert ! ( !wsched_us. can_publish_at( & dt2. with_timezone( & Utc ) ) ) ;
460
+
461
+ assert_eq ! ( dt1, dt2) ;
462
+
463
+ // Monday after EU change, before US change, from Chicago
464
+ // perspective. Okay for publishing Chicago market, outside
465
+ // hours for publishing Amsterdam market.
466
+ let dt3 = NaiveDateTime :: parse_from_str ( "2023-10-30 02:01" , format) ?
467
+ . and_local_timezone ( Tz :: America__Chicago )
468
+ . unwrap ( ) ;
469
+ dbg ! ( & dt3) ;
470
+
471
+ assert ! ( !wsched_eu. can_publish_at( & dt3. with_timezone( & Utc ) ) ) ;
472
+ assert ! ( wsched_us. can_publish_at( & dt3. with_timezone( & Utc ) ) ) ;
473
+
474
+ // Same point in time, from Amsterdam perspective. Still okay
475
+ // for Chicago, still outside hours for Amsterdam.
476
+ let dt4 = NaiveDateTime :: parse_from_str ( "2023-10-30 08:01" , format) ?
477
+ . and_local_timezone ( Tz :: Europe__Amsterdam )
478
+ . unwrap ( ) ;
479
+ dbg ! ( & dt4) ;
480
+
481
+ assert ! ( !wsched_eu. can_publish_at( & dt4. with_timezone( & Utc ) ) ) ;
482
+ assert ! ( wsched_us. can_publish_at( & dt4. with_timezone( & Utc ) ) ) ;
483
+
484
+ assert_eq ! ( dt3, dt4) ;
485
+
486
+ // Monday after both Amsterdam and Chicago get over their DST
487
+ // change, from Amsterdam perspective. Okay for publishing
488
+ // both markets.
489
+ let dt5 = NaiveDateTime :: parse_from_str ( "2023-11-06 09:01" , format) ?
490
+ . and_local_timezone ( Tz :: Europe__Amsterdam )
491
+ . unwrap ( ) ;
492
+ dbg ! ( & dt5) ;
493
+ assert ! ( wsched_eu. can_publish_at( & dt5. with_timezone( & Utc ) ) ) ;
494
+ assert ! ( wsched_us. can_publish_at( & dt5. with_timezone( & Utc ) ) ) ;
495
+
496
+ // Same point in time, from Chicago perspective
497
+ let dt6 = NaiveDateTime :: parse_from_str ( "2023-11-06 02:01" , format) ?
498
+ . and_local_timezone ( Tz :: America__Chicago )
499
+ . unwrap ( ) ;
500
+ dbg ! ( & dt6) ;
501
+ assert ! ( wsched_eu. can_publish_at( & dt6. with_timezone( & Utc ) ) ) ;
502
+ assert ! ( wsched_us. can_publish_at( & dt6. with_timezone( & Utc ) ) ) ;
503
+
504
+ assert_eq ! ( dt5, dt6) ;
505
+
506
+ // Monday after both Amsterdam and Chicago get over their DST
507
+ // change, from Amsterdam perspective. Outside both markets'
508
+ // hours.
509
+ let dt7 = NaiveDateTime :: parse_from_str ( "2023-11-06 17:01" , format) ?
510
+ . and_local_timezone ( Tz :: Europe__Amsterdam )
511
+ . unwrap ( ) ;
512
+ dbg ! ( & dt7) ;
513
+ assert ! ( !wsched_eu. can_publish_at( & dt7. with_timezone( & Utc ) ) ) ;
514
+ assert ! ( !wsched_us. can_publish_at( & dt7. with_timezone( & Utc ) ) ) ;
515
+
516
+ // Same point in time, from Chicago perspective, still outside
517
+ // hours for both markets.
518
+ let dt8 = NaiveDateTime :: parse_from_str ( "2023-11-06 10:01" , format) ?
519
+ . and_local_timezone ( Tz :: America__Chicago )
520
+ . unwrap ( ) ;
521
+ dbg ! ( & dt8) ;
522
+ assert ! ( !wsched_eu. can_publish_at( & dt8. with_timezone( & Utc ) ) ) ;
523
+ assert ! ( !wsched_us. can_publish_at( & dt8. with_timezone( & Utc ) ) ) ;
524
+
525
+ assert_eq ! ( dt7, dt8) ;
526
+
527
+ Ok ( ( ) )
528
+ }
423
529
}
0 commit comments