use super::types::{Window, WindowBounds};
use chrono::{DateTime, Duration, TimeZone, Utc};
use std::fmt;
pub trait WindowAssigner: Send + Sync + fmt::Debug {
fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window>;
fn max_windows_per_event(&self) -> usize {
1
}
fn is_session_window(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct TumblingWindowAssigner {
size: Duration,
offset: Duration,
}
impl TumblingWindowAssigner {
pub fn new(size: Duration) -> Self {
assert!(size > Duration::zero(), "Window size must be positive");
Self {
size,
offset: Duration::zero(),
}
}
pub fn with_offset(mut self, offset: Duration) -> Self {
self.offset = offset;
self
}
fn window_start(&self, timestamp: DateTime<Utc>) -> DateTime<Utc> {
let ts_millis = timestamp.timestamp_millis();
let offset_millis = self.offset.num_milliseconds();
let size_millis = self.size.num_milliseconds();
let aligned = ((ts_millis - offset_millis) / size_millis) * size_millis + offset_millis;
Utc.timestamp_millis_opt(aligned).unwrap()
}
}
impl WindowAssigner for TumblingWindowAssigner {
fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
let start = self.window_start(timestamp);
let end = start + self.size;
let bounds = WindowBounds::new(start, end);
vec![Window::new(bounds)]
}
}
#[derive(Debug, Clone)]
pub struct SlidingWindowAssigner {
size: Duration,
slide: Duration,
offset: Duration,
}
impl SlidingWindowAssigner {
pub fn new(size: Duration, slide: Duration) -> Self {
assert!(size > Duration::zero(), "Window size must be positive");
assert!(slide > Duration::zero(), "Slide must be positive");
assert!(slide <= size, "Slide must not be larger than window size");
Self {
size,
slide,
offset: Duration::zero(),
}
}
pub fn with_offset(mut self, offset: Duration) -> Self {
self.offset = offset;
self
}
fn window_starts(&self, timestamp: DateTime<Utc>) -> Vec<DateTime<Utc>> {
let ts_millis = timestamp.timestamp_millis();
let offset_millis = self.offset.num_milliseconds();
let size_millis = self.size.num_milliseconds();
let slide_millis = self.slide.num_milliseconds();
let first_start = ((ts_millis - offset_millis - size_millis + 1) / slide_millis) * slide_millis + offset_millis;
let mut starts = Vec::new();
let mut current_start = first_start;
while current_start <= ts_millis - offset_millis {
let start_time = Utc.timestamp_millis_opt(current_start).unwrap();
let end_time = start_time + self.size;
if timestamp >= start_time && timestamp < end_time {
starts.push(start_time);
}
current_start += slide_millis;
}
starts
}
}
impl WindowAssigner for SlidingWindowAssigner {
fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
self.window_starts(timestamp)
.into_iter()
.map(|start| {
let end = start + self.size;
let bounds = WindowBounds::new(start, end);
Window::new(bounds)
})
.collect()
}
fn max_windows_per_event(&self) -> usize {
(self.size.num_milliseconds() / self.slide.num_milliseconds()) as usize
}
}
#[derive(Debug, Clone)]
pub struct SessionWindowAssigner {
gap: Duration,
}
impl SessionWindowAssigner {
pub fn new(gap: Duration) -> Self {
assert!(gap > Duration::zero(), "Session gap must be positive");
Self { gap }
}
pub fn gap(&self) -> Duration {
self.gap
}
}
impl WindowAssigner for SessionWindowAssigner {
fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
let start = timestamp;
let end = timestamp + self.gap;
let bounds = WindowBounds::new(start, end);
vec![Window::new(bounds)]
}
fn is_session_window(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn create_timestamp(millis: i64) -> DateTime<Utc> {
Utc.timestamp_millis_opt(millis).unwrap()
}
#[test]
fn test_tumbling_window_assignment() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
let windows = assigner.assign_windows(create_timestamp(500));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(0));
assert_eq!(windows[0].bounds.end, create_timestamp(1000));
let windows = assigner.assign_windows(create_timestamp(1500));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(1000));
assert_eq!(windows[0].bounds.end, create_timestamp(2000));
let windows = assigner.assign_windows(create_timestamp(2000));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(2000));
assert_eq!(windows[0].bounds.end, create_timestamp(3000));
}
#[test]
fn test_tumbling_window_with_offset() {
let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000))
.with_offset(Duration::milliseconds(200));
let windows = assigner.assign_windows(create_timestamp(500));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(200));
assert_eq!(windows[0].bounds.end, create_timestamp(1200));
}
#[test]
fn test_sliding_window_assignment() {
let assigner = SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(500),
);
let windows = assigner.assign_windows(create_timestamp(700));
assert_eq!(windows.len(), 2);
assert_eq!(windows[0].bounds.start, create_timestamp(0));
assert_eq!(windows[0].bounds.end, create_timestamp(1000));
assert_eq!(windows[1].bounds.start, create_timestamp(500));
assert_eq!(windows[1].bounds.end, create_timestamp(1500));
let windows = assigner.assign_windows(create_timestamp(1200));
assert_eq!(windows.len(), 2);
assert_eq!(windows[0].bounds.start, create_timestamp(500));
assert_eq!(windows[0].bounds.end, create_timestamp(1500));
assert_eq!(windows[1].bounds.start, create_timestamp(1000));
assert_eq!(windows[1].bounds.end, create_timestamp(2000));
}
#[test]
fn test_sliding_window_max_windows() {
let assigner = SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(500),
);
assert_eq!(assigner.max_windows_per_event(), 2);
let assigner = SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(250),
);
assert_eq!(assigner.max_windows_per_event(), 4);
}
#[test]
fn test_sliding_window_with_offset() {
let assigner = SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(500),
).with_offset(Duration::milliseconds(100));
let windows = assigner.assign_windows(create_timestamp(700));
assert!(windows.iter().all(|w| w.bounds.start.timestamp_millis() % 500 == 100));
}
#[test]
fn test_session_window_assignment() {
let assigner = SessionWindowAssigner::new(Duration::milliseconds(5000));
let windows = assigner.assign_windows(create_timestamp(1000));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(1000));
assert_eq!(windows[0].bounds.end, create_timestamp(6000));
let windows = assigner.assign_windows(create_timestamp(2000));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].bounds.start, create_timestamp(2000));
assert_eq!(windows[0].bounds.end, create_timestamp(7000));
}
#[test]
fn test_session_window_is_session() {
let assigner = SessionWindowAssigner::new(Duration::milliseconds(5000));
assert!(assigner.is_session_window());
let tumbling = TumblingWindowAssigner::new(Duration::milliseconds(1000));
assert!(!tumbling.is_session_window());
}
#[test]
#[should_panic(expected = "Window size must be positive")]
fn test_tumbling_invalid_size() {
TumblingWindowAssigner::new(Duration::zero());
}
#[test]
#[should_panic(expected = "Slide must not be larger than window size")]
fn test_sliding_invalid_slide() {
SlidingWindowAssigner::new(
Duration::milliseconds(1000),
Duration::milliseconds(2000),
);
}
#[test]
#[should_panic(expected = "Session gap must be positive")]
fn test_session_invalid_gap() {
SessionWindowAssigner::new(Duration::zero());
}
}