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