use chrono::{Datelike, Duration, NaiveDateTime, Timelike, Weekday};
pub const SLOT_MINUTES: i64 = 5;
pub const SLOTS_PER_HOUR: usize = (60 / SLOT_MINUTES) as usize;
pub const SLOTS_PER_DAY: usize = 24 * SLOTS_PER_HOUR;
pub const SLOTS_PER_WEEK: usize = 7 * SLOTS_PER_DAY;
pub const BUFFER_SLOTS: usize = SLOTS_PER_DAY;
#[inline]
pub fn slot_span(slots: i64) -> Duration {
Duration::minutes(slots * SLOT_MINUTES)
}
#[inline]
pub fn minutes_to_slots(minutes: usize) -> Option<usize> {
if !minutes.is_multiple_of(SLOT_MINUTES as usize) {
return None;
}
Some(minutes / SLOT_MINUTES as usize)
}
#[inline]
pub fn slots_to_minutes(slots: usize) -> usize {
slots * SLOT_MINUTES as usize
}
#[inline]
pub fn is_datetime_aligned(dt: NaiveDateTime) -> bool {
dt.second() == 0 && dt.nanosecond() == 0 && (dt.minute() as i64) % SLOT_MINUTES == 0
}
#[inline]
pub fn datetime_at(start: NaiveDateTime, index: usize) -> NaiveDateTime {
start + slot_span(index as i64 - BUFFER_SLOTS as i64)
}
#[inline]
pub fn index_at(start: NaiveDateTime, datetime: NaiveDateTime) -> usize {
let minutes_from_buffer_start =
((datetime - start) + slot_span(BUFFER_SLOTS as i64)).num_minutes();
debug_assert_eq!(
minutes_from_buffer_start % SLOT_MINUTES,
0,
"datetime must be slot-aligned"
);
(minutes_from_buffer_start / SLOT_MINUTES) as usize
}
#[inline]
pub fn weekday_at(start: NaiveDateTime, index: usize) -> Weekday {
datetime_at(start, index).weekday()
}
pub fn format_slot_time_of_day(slot: usize) -> String {
let minutes = (slot % SLOTS_PER_DAY) * SLOT_MINUTES as usize;
format!("{:02}:{:02}", minutes / 60, minutes % 60)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
fn dt(y: i32, m: u32, d: u32, h: u32) -> NaiveDateTime {
NaiveDate::from_ymd_opt(y, m, d)
.unwrap()
.and_hms_opt(h, 0, 0)
.unwrap()
}
fn dt_min(y: i32, m: u32, d: u32, h: u32, min: u32) -> NaiveDateTime {
NaiveDate::from_ymd_opt(y, m, d)
.unwrap()
.and_hms_opt(h, min, 0)
.unwrap()
}
#[test]
fn slot_constants_are_consistent() {
assert_eq!(SLOTS_PER_HOUR, 12);
assert_eq!(SLOTS_PER_DAY, 288);
assert_eq!(SLOTS_PER_WEEK, 2016);
assert_eq!(BUFFER_SLOTS, SLOTS_PER_DAY);
assert_eq!(SLOT_MINUTES * SLOTS_PER_HOUR as i64, 60);
}
#[test]
fn buffer_index_maps_to_start() {
let start = dt(2022, 1, 1, 0);
assert_eq!(datetime_at(start, BUFFER_SLOTS), start);
assert_eq!(index_at(start, start), BUFFER_SLOTS);
}
#[test]
fn datetime_and_index_are_inverse_at_five_minute_resolution() {
let start = dt(2022, 1, 1, 0);
for index in 0..(2 * SLOTS_PER_WEEK) {
assert_eq!(index_at(start, datetime_at(start, index)), index);
}
}
#[test]
fn five_minute_datetimes_round_trip() {
let start = dt(2022, 1, 1, 0);
let at_1815 = dt_min(2022, 1, 1, 18, 15);
let index = index_at(start, at_1815);
assert_eq!(datetime_at(start, index), at_1815);
}
#[test]
fn weekday_tracks_calendar() {
let start = dt(2022, 1, 1, 0);
assert_eq!(weekday_at(start, BUFFER_SLOTS), Weekday::Sat);
assert_eq!(
weekday_at(start, BUFFER_SLOTS + SLOTS_PER_DAY),
Weekday::Sun
);
assert_eq!(
weekday_at(start, BUFFER_SLOTS + 7 * SLOTS_PER_DAY),
Weekday::Sat
);
}
#[test]
fn minutes_to_slots_rejects_non_aligned() {
assert_eq!(minutes_to_slots(60), Some(12));
assert_eq!(minutes_to_slots(25), Some(5));
assert_eq!(minutes_to_slots(7), None);
}
#[test]
fn alignment_detection() {
assert!(is_datetime_aligned(dt_min(2022, 1, 1, 18, 15)));
assert!(is_datetime_aligned(dt_min(2022, 1, 1, 18, 0)));
assert!(!is_datetime_aligned(dt_min(2022, 1, 1, 18, 7)));
}
}