Skip to main content

reifydb_core/window/
span.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	fmt::Debug,
6	ops::{Add, Rem, Sub},
7};
8
9use reifydb_value::value::{date::Date, datetime::DateTime, duration::Duration, time::Time};
10use serde::{Deserialize, Serialize};
11
12pub trait Slot:
13	Copy
14	+ Ord
15	+ Debug
16	+ Add<Self::Duration, Output = Self>
17	+ Sub<Self, Output = Self::Duration>
18	+ Rem<Self::Duration, Output = Self::Duration>
19	+ Sub<Self::Duration, Output = Self>
20{
21	type Duration: Copy + Ord + Debug + IsZero;
22
23	fn order_key(&self) -> u64;
24}
25
26pub trait IsZero {
27	fn is_zero(&self) -> bool;
28}
29
30impl IsZero for u64 {
31	#[inline]
32	fn is_zero(&self) -> bool {
33		*self == 0
34	}
35}
36
37impl IsZero for Duration {
38	#[inline]
39	fn is_zero(&self) -> bool {
40		*self == Duration::zero()
41	}
42}
43
44impl IsZero for DateTime {
45	#[inline]
46	fn is_zero(&self) -> bool {
47		*self == DateTime::default()
48	}
49}
50
51impl IsZero for Date {
52	#[inline]
53	fn is_zero(&self) -> bool {
54		*self == Date::default()
55	}
56}
57
58impl IsZero for Time {
59	#[inline]
60	fn is_zero(&self) -> bool {
61		*self == Time::default()
62	}
63}
64
65impl Slot for u64 {
66	type Duration = u64;
67
68	fn order_key(&self) -> u64 {
69		*self
70	}
71}
72
73impl Slot for DateTime {
74	type Duration = Duration;
75
76	fn order_key(&self) -> u64 {
77		self.to_nanos()
78	}
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
82pub struct WindowSpan<T> {
83	pub start: T,
84	pub end: T,
85}
86
87impl<T> WindowSpan<T>
88where
89	T: Slot,
90{
91	#[inline]
92	pub fn for_slot(slot: T, duration: T::Duration) -> Self {
93		assert!(!duration.is_zero(), "WindowSpan::for_slot: duration must be > 0");
94		let start = slot - (slot % duration);
95		Self {
96			start,
97			end: start + duration,
98		}
99	}
100
101	#[inline]
102	pub fn new(start: T, end: T) -> Self {
103		assert!(start < end, "WindowSpan::new: start ({start:?}) must be < end ({end:?})");
104		Self {
105			start,
106			end,
107		}
108	}
109
110	#[inline]
111	pub fn duration(&self) -> T::Duration {
112		self.end - self.start
113	}
114
115	#[inline]
116	pub fn contains(&self, slot: T) -> bool {
117		slot >= self.start && slot < self.end
118	}
119
120	#[inline]
121	pub fn next(&self) -> Self {
122		let d = self.duration();
123		Self {
124			start: self.end,
125			end: self.end + d,
126		}
127	}
128}
129
130#[cfg(test)]
131mod tests {
132	use super::*;
133
134	#[test]
135	fn for_slot_aligns_to_duration() {
136		assert_eq!(WindowSpan::<u64>::for_slot(123, 60), WindowSpan::new(120u64, 180));
137		assert_eq!(WindowSpan::<u64>::for_slot(0, 60), WindowSpan::new(0u64, 60));
138		assert_eq!(WindowSpan::<u64>::for_slot(60, 60), WindowSpan::new(60u64, 120));
139	}
140
141	#[test]
142	fn for_slot_aligns_datetime_to_duration() {
143		let coord = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
144		let one_second = Duration::from_seconds(1).unwrap();
145		let one_minute = Duration::from_seconds(60).unwrap();
146
147		// A sub-minute (1s) window must stay 1s, not round up to a minute.
148		let sec = WindowSpan::for_slot(coord, one_second);
149		assert_eq!(sec.start, DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap());
150		assert_eq!(sec.end, DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 26).unwrap());
151		assert_eq!(sec.duration(), one_second);
152
153		// A 1m window aligns the coord down to the minute boundary.
154		let min = WindowSpan::for_slot(coord, one_minute);
155		assert_eq!(min.start, DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap());
156		assert_eq!(min.end, DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 0).unwrap());
157		assert!(min.contains(coord));
158		assert!(!min.contains(min.end));
159	}
160
161	#[test]
162	fn contains_is_half_open() {
163		let span = WindowSpan::new(100u64, 200);
164		assert!(span.contains(100));
165		assert!(span.contains(199));
166		assert!(!span.contains(200));
167		assert!(!span.contains(99));
168	}
169
170	#[test]
171	fn boundary_slot_belongs_to_next_window() {
172		// The recurring off-by-one bug: an event at exactly window_end
173		// must NOT be claimed by the current window. Encoded once, here.
174		let cur = WindowSpan::<u64>::for_slot(60, 60);
175		let nxt = cur.next();
176		assert!(!cur.contains(120));
177		assert!(nxt.contains(120));
178		assert_eq!(nxt, WindowSpan::new(120u64, 180));
179	}
180
181	#[test]
182	#[should_panic(expected = "duration must be > 0")]
183	fn zero_duration_panics() {
184		WindowSpan::<u64>::for_slot(10, 0);
185	}
186
187	#[test]
188	#[should_panic(expected = "must be <")]
189	fn empty_span_panics() {
190		WindowSpan::new(100u64, 100);
191	}
192
193	/// A toy newtype demonstrating that any well-behaved coordinate works,
194	/// not just `u64`. This is what a `Slot` or `DateTime` wrapper would do.
195	#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
196	struct Tick(u64);
197
198	impl Add<u64> for Tick {
199		type Output = Tick;
200		fn add(self, rhs: u64) -> Tick {
201			Tick(self.0 + rhs)
202		}
203	}
204	impl Sub<Tick> for Tick {
205		type Output = u64;
206		fn sub(self, rhs: Tick) -> u64 {
207			self.0 - rhs.0
208		}
209	}
210	impl Sub<u64> for Tick {
211		type Output = Tick;
212		fn sub(self, rhs: u64) -> Tick {
213			Tick(self.0 - rhs)
214		}
215	}
216	impl Rem<u64> for Tick {
217		type Output = u64;
218		fn rem(self, rhs: u64) -> u64 {
219			self.0 % rhs
220		}
221	}
222	impl Slot for Tick {
223		type Duration = u64;
224
225		fn order_key(&self) -> u64 {
226			self.0
227		}
228	}
229
230	#[test]
231	fn newtype_coord_works() {
232		let span = WindowSpan::<Tick>::for_slot(Tick(125), 10);
233		assert_eq!(span, WindowSpan::new(Tick(120), Tick(130)));
234		assert!(span.contains(Tick(120)));
235		assert!(!span.contains(Tick(130)));
236	}
237}