Skip to main content

proto_types/common/
interval.rs

1use core::cmp::Ordering;
2
3use thiserror::Error;
4
5use crate::{Duration, String, Timestamp, ToString, common::Interval, constants::NANOS_PER_SECOND};
6
7/// Errors that can occur during the creation, conversion or validation of an [`Interval`].
8#[derive(Debug, Error, PartialEq, Eq, Clone)]
9#[non_exhaustive]
10pub enum IntervalError {
11	#[error("Interval's end_time is before its start_time")]
12	EndTimeBeforeStartTime,
13	#[error("Interval conversion error: {0}")]
14	ConversionError(String),
15}
16
17fn validate_interval(
18	start: Option<Timestamp>,
19	end: Option<Timestamp>,
20) -> Result<(), IntervalError> {
21	if start.is_some_and(|s| end.is_some_and(|e| e < s)) {
22		Err(IntervalError::EndTimeBeforeStartTime)
23	} else {
24		Ok(())
25	}
26}
27
28impl Interval {
29	/// Creates a new [`Interval`] instance, checking that `end_time` is not before `start_time`.
30	#[inline]
31	pub fn new(
32		start_time: Option<Timestamp>,
33		end_time: Option<Timestamp>,
34	) -> Result<Self, IntervalError> {
35		validate_interval(start_time, end_time)?;
36
37		Ok(Self {
38			start_time,
39			end_time,
40		})
41	}
42
43	#[cfg(any(feature = "std", feature = "chrono-wasm"))]
44	/// Creates an [`Interval`] going from now to the `end_time` specified.
45	#[must_use]
46	#[inline]
47	pub fn from_now_to(end_time: Timestamp) -> Self {
48		Self {
49			start_time: Some(Timestamp::now()),
50			end_time: Some(end_time),
51		}
52	}
53
54	#[cfg(any(feature = "std", feature = "chrono-wasm"))]
55	/// Creates a new [`Interval`] going from the specified `start_time` to the present moment.
56	#[must_use]
57	#[inline]
58	pub fn from_start_to_now(start_time: Timestamp) -> Self {
59		Self {
60			start_time: Some(start_time),
61			end_time: Some(Timestamp::now()),
62		}
63	}
64
65	/// Checks that `end_time` is not before `start_time`.
66	#[must_use]
67	pub fn is_valid(&self) -> bool {
68		validate_interval(self.start_time, self.end_time).is_ok()
69	}
70
71	/// Returns `true` if the `Interval` is empty (`start_time` equals `end_time`).
72	#[must_use]
73	#[inline]
74	pub fn is_empty(&self) -> bool {
75		self.start_time
76			.as_ref()
77			.zip(self.end_time.as_ref())
78			.map_or_else(|| false, |(start, end)| start == end)
79	}
80
81	/// Returns `true` if the `Interval` is unspecified (no `start_time` and no `end_time`)
82	#[must_use]
83	#[inline]
84	pub const fn is_unspecified(&self) -> bool {
85		self.start_time.is_none() && self.end_time.is_none()
86	}
87}
88
89impl TryFrom<Interval> for Duration {
90	type Error = IntervalError;
91	fn try_from(value: Interval) -> Result<Self, Self::Error> {
92		let result = value
93			.start_time
94			.zip(value.end_time)
95			.map(|(start, end)| {
96				let mut seconds_diff = end.seconds - start.seconds;
97				let mut nanos_diff = end.nanos - start.nanos;
98
99				if nanos_diff < 0 {
100					seconds_diff -= 1;
101					nanos_diff += NANOS_PER_SECOND;
102				} else if nanos_diff >= NANOS_PER_SECOND {
103					seconds_diff += 1;
104					nanos_diff -= NANOS_PER_SECOND;
105				}
106
107				Self {
108					seconds: seconds_diff,
109					nanos: nanos_diff,
110				}
111			});
112
113		result.ok_or(IntervalError::ConversionError(
114			"Cannot convert to Duration due to missing start or end time".to_string(),
115		))
116	}
117}
118
119impl PartialOrd for Interval {
120	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
121		if !self.is_valid() || !other.is_valid() {
122			return None;
123		}
124
125		// 1. Check Empty (Zero Duration)
126		if self.is_empty() {
127			return if other.is_empty() {
128				Some(Ordering::Equal)
129			} else {
130				Some(Ordering::Less)
131			};
132		}
133		if other.is_empty() {
134			return Some(Ordering::Greater);
135		}
136
137		// We utilize the fact that TryFrom fails for infinite/open-ended intervals
138		// and succeeds for bounded ones.
139		let self_dur = Duration::try_from(*self);
140		let other_dur = Duration::try_from(*other);
141
142		match (self_dur, other_dur) {
143			// Both Finite: Compare the actual time span
144			(Ok(d1), Ok(d2)) => d1.partial_cmp(&d2),
145
146			// Finite < Infinite
147			(Ok(_), Err(_)) => Some(Ordering::Less),
148
149			// Infinite > Finite
150			(Err(_), Ok(_)) => Some(Ordering::Greater),
151
152			// Infinite == Infinite
153			// (Treat all infinite intervals as equal for sorting stability)
154			(Err(_), Err(_)) => Some(Ordering::Equal),
155		}
156	}
157}
158
159#[cfg(test)]
160mod tests {
161	use super::*;
162
163	fn ts(s: i64) -> Timestamp {
164		Timestamp {
165			seconds: s,
166			nanos: 0,
167		}
168	}
169
170	#[test]
171	fn test_constructor() {
172		let t = ts(100);
173
174		// Empty
175		let empty = Interval::new(Some(t), Some(t)).unwrap();
176		assert!(empty.is_empty());
177		assert!(!empty.is_unspecified());
178
179		// Unspecified
180		let unspec = Interval::new(None, None).unwrap();
181		assert!(unspec.is_unspecified());
182
183		// Open Ended
184		let open = Interval::new(Some(t), None).unwrap();
185		assert!(!open.is_empty());
186	}
187
188	#[test]
189	fn test_partial_ord_ranking() {
190		let t0 = ts(0);
191		let t10 = ts(10);
192		let t20 = ts(20);
193
194		let empty = Interval::new(Some(t0), Some(t0)).unwrap(); // Duration 0
195		let finite_small = Interval::new(Some(t0), Some(t10)).unwrap(); // Duration 10
196		let finite_large = Interval::new(Some(t0), Some(t20)).unwrap(); // Duration 20
197		let infinite_end = Interval::new(Some(t0), None).unwrap(); // Duration Infinity
198		let infinite_start = Interval::new(None, Some(t20)).unwrap(); // Duration Infinity
199		let infinite_all = Interval::new(None, None).unwrap(); // Duration Infinity
200
201		// 1. Empty < Finite
202		assert!(empty < finite_small);
203
204		// 2. Finite < Finite (Duration comparison)
205		assert!(finite_small < finite_large);
206
207		// 3. Finite < Infinite
208		assert!(finite_large < infinite_end);
209		assert!(finite_large < infinite_start);
210
211		// 4. Infinite == Infinite (Stability)
212		// Note: Even though "All Time" is conceptually larger than "From 2024",
213		// without finite bounds we can't mathematically compare them, so Equality is safest.
214		assert!(infinite_end.partial_cmp(&infinite_start) == Some(Ordering::Equal));
215		assert!(infinite_end.partial_cmp(&infinite_all) == Some(Ordering::Equal));
216	}
217}