pub const TIME_WINDOW_CHECK_NAME: &str = "time_window";
pub const TIME_WINDOW_BLOCK_STATUS: u16 = 403;
pub const TIME_WINDOW_BLOCK_BODY: &str = "Access not allowed at this time";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TimeWindow {
pub start: Option<String>,
pub end: Option<String>,
pub timezone: Option<String>,
}
#[must_use]
pub fn hhmm_in_zone(now_utc: chrono::DateTime<chrono::Utc>, timezone: Option<&str>) -> String {
let zone: chrono_tz::Tz = timezone
.and_then(|name| name.parse().ok())
.unwrap_or(chrono_tz::UTC);
now_utc.with_timezone(&zone).format("%H:%M").to_string()
}
#[must_use]
pub fn is_within(window: &TimeWindow, current_hhmm: &str) -> bool {
let (Some(start), Some(end)) = (window.start.as_deref(), window.end.as_deref()) else {
return true;
};
if start > end {
current_hhmm >= start || current_hhmm <= end
} else {
start <= current_hhmm && current_hhmm <= end
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn at(hour: u32, minute: u32) -> TimeWindow {
TimeWindow {
start: Some(format!("{hour:02}:{minute:02}")),
..TimeWindow::default()
}
}
#[test]
fn plain_windows_admit_only_the_span() {
let window = TimeWindow {
start: Some("09:00".into()),
end: Some("17:00".into()),
timezone: None,
};
assert!(!is_within(&window, "08:59"));
assert!(is_within(&window, "09:00"));
assert!(is_within(&window, "17:00"));
assert!(!is_within(&window, "17:01"));
assert!(!is_within(&window, "23:59"));
assert!(!is_within(&window, "00:00"));
}
#[test]
fn midnight_wrapping_windows_admit_both_sides() {
let window = TimeWindow {
start: Some("22:00".into()),
end: Some("06:00".into()),
timezone: None,
};
assert!(is_within(&window, "22:00"));
assert!(is_within(&window, "23:59"));
assert!(is_within(&window, "00:00"));
assert!(is_within(&window, "06:00"));
assert!(!is_within(&window, "06:01"));
assert!(!is_within(&window, "21:59"));
assert!(!is_within(&window, "12:00"));
}
#[test]
fn missing_bounds_fail_open_like_the_reference() {
let empty = TimeWindow::default();
assert!(is_within(&empty, "12:00"));
let no_start = TimeWindow {
end: Some("06:00".into()),
..TimeWindow::default()
};
assert!(is_within(&no_start, "12:00"));
let no_end = at(9, 0);
assert!(is_within(&no_end, "12:00"));
}
#[test]
fn zones_shift_the_wall_clock_and_unknown_zones_fall_back() {
let utc = chrono::Utc.with_ymd_and_hms(2026, 1, 15, 12, 0, 0).unwrap();
assert_eq!(hhmm_in_zone(utc, None), "12:00");
assert_eq!(hhmm_in_zone(utc, Some("UTC")), "12:00");
assert_eq!(
hhmm_in_zone(utc, Some("America/New_York")),
"07:00",
"EST in January"
);
assert_eq!(hhmm_in_zone(utc, Some("Asia/Tokyo")), "21:00");
assert_eq!(
hhmm_in_zone(utc, Some("Not/AZone")),
"12:00",
"an unknown zone falls back to UTC, the reference's except arm"
);
let summer = chrono::Utc.with_ymd_and_hms(2026, 7, 15, 12, 0, 0).unwrap();
assert_eq!(
hhmm_in_zone(summer, Some("America/New_York")),
"08:00",
"EDT in July"
);
}
#[test]
fn the_reference_window_example_over_a_real_zone() {
let window = TimeWindow {
start: Some("09:00".into()),
end: Some("17:00".into()),
timezone: Some("Asia/Tokyo".into()),
};
let utc = chrono::Utc.with_ymd_and_hms(2026, 1, 15, 0, 0, 0).unwrap();
assert_eq!(hhmm_in_zone(utc, Some("Asia/Tokyo")), "09:00");
assert!(is_within(&window, &hhmm_in_zone(utc, Some("Asia/Tokyo"))));
}
}