use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Window {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
pub fn floor_to_minute(dt: DateTime<Utc>) -> DateTime<Utc> {
Utc.with_ymd_and_hms(
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
0,
)
.single()
.expect("valid timestamp — flooring to minute should never produce an ambiguous or invalid time")
}
pub fn plan_next_window(
last_complete_minute: Option<DateTime<Utc>>,
now: DateTime<Utc>,
safety_lag_minutes: u32,
) -> Option<Window> {
let start = last_complete_minute?;
let target = floor_to_minute(now) - Duration::minutes(i64::from(safety_lag_minutes));
if target <= start {
return None;
}
Some(Window { start, end: target })
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn ts(h: u32, m: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2024, 6, 1, h, m, 0).single().unwrap()
}
fn ts_secs(h: u32, m: u32, s: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2024, 6, 1, h, m, s).single().unwrap()
}
#[test]
fn floor_strips_seconds_and_nanos() {
let dt = ts_secs(10, 30, 45).with_nanosecond(123_456_789).unwrap();
let floored = floor_to_minute(dt);
assert_eq!(floored, ts(10, 30));
assert_eq!(floored.second(), 0);
assert_eq!(floored.nanosecond(), 0);
}
#[test]
fn floor_already_on_boundary_is_idempotent() {
let dt = ts(10, 0);
assert_eq!(floor_to_minute(dt), dt);
}
#[test]
fn none_when_last_is_none_backfill_mode() {
let result = plan_next_window(None, ts(10, 5), 2);
assert!(result.is_none());
}
#[test]
fn none_when_target_equals_last() {
let result = plan_next_window(Some(ts(10, 5)), ts(10, 7), 2);
assert!(result.is_none());
}
#[test]
fn none_when_target_behind_last() {
let result = plan_next_window(Some(ts(10, 5)), ts(10, 6), 2);
assert!(result.is_none());
}
#[test]
fn some_when_normal_advance() {
let result = plan_next_window(Some(ts(10, 5)), ts(10, 10), 2);
let w = result.expect("should produce a window");
assert_eq!(w.start, ts(10, 5));
assert_eq!(w.end, ts(10, 8));
}
#[test]
fn now_with_sub_minute_noise_is_floored() {
let now_noisy = ts_secs(10, 10, 45);
let result = plan_next_window(Some(ts(10, 5)), now_noisy, 2);
let w = result.expect("should produce a window");
assert_eq!(w.end, ts(10, 8));
}
#[test]
fn exactly_one_minute_of_progress() {
let result = plan_next_window(Some(ts(10, 5)), ts(10, 8), 2);
let w = result.expect("should produce a window");
assert_eq!(w.start, ts(10, 5));
assert_eq!(w.end, ts(10, 6));
}
}