use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WindowBounds {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
impl WindowBounds {
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
assert!(start < end, "Window start must be before end");
Self { start, end }
}
pub fn duration(&self) -> Duration {
self.end.signed_duration_since(self.start)
}
pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
timestamp >= self.start && timestamp < self.end
}
pub fn overlaps(&self, other: &WindowBounds) -> bool {
self.start < other.end && other.start < self.end
}
pub fn merge(&self, other: &WindowBounds) -> WindowBounds {
WindowBounds {
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
pub fn gap(&self, other: &WindowBounds) -> Option<Duration> {
if self.overlaps(other) {
return None;
}
if self.end <= other.start {
Some(other.start.signed_duration_since(self.end))
} else {
Some(self.start.signed_duration_since(other.end))
}
}
}
impl fmt::Display for WindowBounds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{} - {})",
self.start.format("%Y-%m-%d %H:%M:%S%.3f"),
self.end.format("%Y-%m-%d %H:%M:%S%.3f")
)
}
}
impl PartialOrd for WindowBounds {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for WindowBounds {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.start.cmp(&other.start)
.then_with(|| self.end.cmp(&other.end))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Window {
pub bounds: WindowBounds,
pub id: String,
}
impl Window {
pub fn new(bounds: WindowBounds) -> Self {
let id = format!("{}_{}", bounds.start.timestamp_millis(), bounds.end.timestamp_millis());
Self { bounds, id }
}
pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
self.bounds.contains(timestamp)
}
pub fn duration(&self) -> Duration {
self.bounds.duration()
}
pub fn can_merge(&self, other: &Window, max_gap: Duration) -> bool {
if self.bounds.overlaps(&other.bounds) {
return true;
}
if let Some(gap) = self.bounds.gap(&other.bounds) {
gap <= max_gap
} else {
false
}
}
pub fn merge(&self, other: &Window) -> Window {
Window::new(self.bounds.merge(&other.bounds))
}
}
impl fmt::Display for Window {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Window[{}]", self.bounds)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WindowType {
Tumbling,
Sliding,
Session,
}
#[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_window_bounds_creation() {
let start = create_timestamp(1000);
let end = create_timestamp(2000);
let bounds = WindowBounds::new(start, end);
assert_eq!(bounds.start, start);
assert_eq!(bounds.end, end);
assert_eq!(bounds.duration(), Duration::milliseconds(1000));
}
#[test]
#[should_panic(expected = "Window start must be before end")]
fn test_window_bounds_invalid() {
let start = create_timestamp(2000);
let end = create_timestamp(1000);
WindowBounds::new(start, end);
}
#[test]
fn test_window_bounds_contains() {
let bounds = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
assert!(!bounds.contains(create_timestamp(999)));
assert!(bounds.contains(create_timestamp(1000)));
assert!(bounds.contains(create_timestamp(1500)));
assert!(!bounds.contains(create_timestamp(2000)));
assert!(!bounds.contains(create_timestamp(2001)));
}
#[test]
fn test_window_bounds_overlaps() {
let bounds1 = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
let bounds2 = WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
);
let bounds3 = WindowBounds::new(
create_timestamp(2500),
create_timestamp(3500),
);
assert!(bounds1.overlaps(&bounds2));
assert!(bounds2.overlaps(&bounds1));
assert!(!bounds1.overlaps(&bounds3));
assert!(!bounds3.overlaps(&bounds1));
}
#[test]
fn test_window_bounds_merge() {
let bounds1 = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
let bounds2 = WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
);
let merged = bounds1.merge(&bounds2);
assert_eq!(merged.start, create_timestamp(1000));
assert_eq!(merged.end, create_timestamp(2500));
}
#[test]
fn test_window_bounds_gap() {
let bounds1 = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
let bounds2 = WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
);
let bounds3 = WindowBounds::new(
create_timestamp(3000),
create_timestamp(4000),
);
assert_eq!(bounds1.gap(&bounds2), None);
assert_eq!(bounds1.gap(&bounds3), Some(Duration::milliseconds(1000)));
assert_eq!(bounds3.gap(&bounds1), Some(Duration::milliseconds(1000)));
}
#[test]
fn test_window_creation() {
let bounds = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
let window = Window::new(bounds);
assert_eq!(window.bounds, bounds);
assert_eq!(window.id, "1000_2000");
}
#[test]
fn test_window_can_merge() {
let window1 = Window::new(WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
));
let window2 = Window::new(WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
));
let window3 = Window::new(WindowBounds::new(
create_timestamp(3000),
create_timestamp(4000),
));
assert!(window1.can_merge(&window2, Duration::milliseconds(0)));
assert!(window1.can_merge(&window3, Duration::milliseconds(1000)));
assert!(!window1.can_merge(&window3, Duration::milliseconds(500)));
}
#[test]
fn test_window_merge() {
let window1 = Window::new(WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
));
let window2 = Window::new(WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
));
let merged = window1.merge(&window2);
assert_eq!(merged.bounds.start, create_timestamp(1000));
assert_eq!(merged.bounds.end, create_timestamp(2500));
}
#[test]
fn test_window_ordering() {
let bounds1 = WindowBounds::new(
create_timestamp(1000),
create_timestamp(2000),
);
let bounds2 = WindowBounds::new(
create_timestamp(1500),
create_timestamp(2500),
);
let bounds3 = WindowBounds::new(
create_timestamp(3000),
create_timestamp(4000),
);
assert!(bounds1 < bounds2);
assert!(bounds2 < bounds3);
assert!(bounds1 < bounds3);
}
}