Skip to content

Commit ec1cd7b

Browse files
committed
Add same range check for daily sessions
1 parent 4536b56 commit ec1cd7b

2 files changed

Lines changed: 132 additions & 6 deletions

File tree

crates/hotfix/src/session.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -675,11 +675,11 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
675675
async fn handle_schedule_check(&mut self) {
676676
let is_active = self.schedule.is_active_at(&Utc::now());
677677

678-
if !is_active {
678+
if is_active {
679+
self.state.notify_session_awaiter();
680+
} else {
679681
// we are currently outside scheduled session time
680682
self.initiate_graceful_logout("End of session time").await;
681-
} else {
682-
self.state.notify_session_awaiter();
683683
}
684684

685685
let deadline = Instant::now() + Duration::from_secs(SCHEDULE_CHECK_INTERVAL);

crates/hotfix/src/session_schedule.rs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::config::ScheduleConfig;
22
use crate::error::SessionError;
3-
use chrono::{DateTime, Datelike, NaiveTime, Utc, Weekday};
3+
use chrono::{DateTime, Datelike, Days, NaiveDate, NaiveTime, TimeDelta, Utc, Weekday};
44
use chrono_tz::Tz;
55

6+
type Result<T, E = SessionError> = std::result::Result<T, E>;
7+
68
#[derive(Clone, Debug)]
79
#[allow(dead_code)]
810
pub 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

Comments
 (0)