Skip to main content

proto_types/duration/
duration_operations.rs

1use crate::Duration;
2use core::cmp::Ordering;
3use core::ops::{Add, Div, Mul, Sub};
4use core::time::Duration as StdDuration;
5
6impl PartialEq<StdDuration> for Duration {
7	#[inline]
8	fn eq(&self, other: &StdDuration) -> bool {
9		self.total_nanos() == other.as_nanos().cast_signed()
10	}
11}
12
13impl PartialEq<Duration> for StdDuration {
14	#[inline]
15	fn eq(&self, other: &Duration) -> bool {
16		other == self
17	}
18}
19
20impl PartialOrd<StdDuration> for Duration {
21	#[inline]
22	fn partial_cmp(&self, other: &StdDuration) -> Option<Ordering> {
23		Some(
24			self.total_nanos()
25				.cmp(&other.as_nanos().cast_signed()),
26		)
27	}
28}
29
30impl PartialOrd<Duration> for StdDuration {
31	#[inline]
32	fn partial_cmp(&self, other: &Duration) -> Option<Ordering> {
33		other.partial_cmp(self).map(Ordering::reverse)
34	}
35}
36
37#[cfg(feature = "chrono")]
38impl PartialEq<chrono::TimeDelta> for Duration {
39	#[inline]
40	fn eq(&self, other: &chrono::TimeDelta) -> bool {
41		let other_total = (i128::from(other.num_seconds()) * Self::NANOS_PER_SEC_I128)
42			+ i128::from(other.subsec_nanos());
43
44		self.total_nanos() == other_total
45	}
46}
47
48#[cfg(feature = "chrono")]
49impl PartialEq<Duration> for chrono::TimeDelta {
50	#[inline]
51	fn eq(&self, other: &Duration) -> bool {
52		other == self
53	}
54}
55
56#[cfg(feature = "chrono")]
57impl PartialOrd<chrono::TimeDelta> for Duration {
58	#[inline]
59	fn partial_cmp(&self, other: &chrono::TimeDelta) -> Option<Ordering> {
60		let other_total = (i128::from(other.num_seconds()) * Self::NANOS_PER_SEC_I128)
61			+ i128::from(other.subsec_nanos());
62
63		Some(self.total_nanos().cmp(&other_total))
64	}
65}
66
67#[cfg(feature = "chrono")]
68impl PartialOrd<Duration> for chrono::TimeDelta {
69	#[inline]
70	fn partial_cmp(&self, other: &Duration) -> Option<Ordering> {
71		other.partial_cmp(self).map(Ordering::reverse)
72	}
73}
74
75impl Add<StdDuration> for Duration {
76	type Output = Self;
77
78	#[inline]
79	fn add(self, rhs: StdDuration) -> Self::Output {
80		let rhs_s = i64::try_from(rhs.as_secs()).expect("overflow in duration addition");
81		let rhs_n = i64::from(rhs.subsec_nanos());
82
83		self.checked_add_raw(rhs_s, rhs_n)
84			.expect("overflow in duration addition")
85	}
86}
87
88impl Add for Duration {
89	type Output = Self;
90	#[inline]
91	fn add(self, rhs: Self) -> Self::Output {
92		self.checked_add(&rhs)
93			.expect("overflow in duration addition")
94	}
95}
96
97impl Sub for Duration {
98	type Output = Self;
99
100	#[inline]
101	fn sub(self, other: Self) -> Self {
102		self.checked_sub(&other)
103			.expect("overflow in duration subtraction")
104	}
105}
106
107impl Sub<StdDuration> for Duration {
108	type Output = Self;
109
110	#[inline]
111	fn sub(self, rhs: StdDuration) -> Self::Output {
112		let rhs_s = i64::try_from(rhs.as_secs()).expect("overflow in duration subtraction");
113		let rhs_n = i64::from(rhs.subsec_nanos());
114
115		self.checked_sub_raw(rhs_s, rhs_n)
116			.expect("overflow in duration subtraction")
117	}
118}
119
120#[cfg(feature = "chrono")]
121impl Add<chrono::TimeDelta> for Duration {
122	type Output = Self;
123
124	#[inline]
125	fn add(self, rhs: chrono::TimeDelta) -> Self::Output {
126		self.checked_add_raw(rhs.num_seconds(), i64::from(rhs.subsec_nanos()))
127			.expect("overflow in duration addition")
128	}
129}
130
131#[cfg(feature = "chrono")]
132impl Sub<chrono::TimeDelta> for Duration {
133	type Output = Self;
134
135	#[inline]
136	fn sub(self, rhs: chrono::TimeDelta) -> Self::Output {
137		self.checked_sub_raw(rhs.num_seconds(), i64::from(rhs.subsec_nanos()))
138			.expect("overflow in duration subtraction")
139	}
140}
141
142impl Mul<i64> for Duration {
143	type Output = Self;
144
145	#[inline]
146	fn mul(self, rhs: i64) -> Self {
147		self.checked_mul(rhs)
148			.expect("Duration multiplication by i64 overflowed")
149	}
150}
151
152impl Mul<i32> for Duration {
153	type Output = Self;
154
155	#[inline]
156	fn mul(self, rhs: i32) -> Self {
157		self.checked_mul(i64::from(rhs)) // Simply cast to i64 and use the i64 implementation
158			.expect("Duration multiplication by i32 overflowed")
159	}
160}
161
162impl Div<i64> for Duration {
163	type Output = Self;
164
165	#[inline]
166	fn div(self, rhs: i64) -> Self {
167		self.checked_div(rhs)
168			.expect("Duration division by i64 overflowed or divided by zero")
169	}
170}
171
172impl Div<i32> for Duration {
173	type Output = Self;
174
175	#[inline]
176	fn div(self, rhs: i32) -> Self {
177		self.checked_div(i64::from(rhs))
178			.expect("Duration division by i32 overflowed or divided by zero")
179	}
180}
181
182impl Duration {
183	const NANOS_PER_SEC: i64 = 1_000_000_000;
184	const NANOS_PER_SEC_I128: i128 = 1_000_000_000;
185
186	fn align_signs(mut s: i64, mut n: i32) -> Option<Self> {
187		if s > 0 && n < 0 {
188			s = s.checked_sub(1)?;
189			n += 1_000_000_000;
190		} else if s < 0 && n > 0 {
191			s = s.checked_add(1)?;
192			n -= 1_000_000_000;
193		}
194		Some(Self {
195			seconds: s,
196			nanos: n,
197		})
198	}
199
200	fn checked_add_raw(&self, rhs_s: i64, rhs_n: i64) -> Option<Self> {
201		let mut s = self.seconds.checked_add(rhs_s)?;
202		let mut n_total = i64::from(self.nanos) + rhs_n;
203
204		if n_total >= 1_000_000_000 {
205			s = s.checked_add(1)?;
206			n_total -= 1_000_000_000;
207		} else if n_total <= -1_000_000_000 {
208			s = s.checked_sub(1)?;
209			n_total += 1_000_000_000;
210		}
211
212		if s > 0 && n_total < 0 {
213			s = s.checked_sub(1)?;
214			n_total += 1_000_000_000;
215		} else if s < 0 && n_total > 0 {
216			s = s.checked_add(1)?;
217			n_total -= 1_000_000_000;
218		}
219
220		Some(Self {
221			seconds: s,
222			#[allow(clippy::cast_possible_truncation)]
223			nanos: n_total as i32,
224		})
225	}
226
227	fn checked_sub_raw(&self, rhs_s: i64, rhs_n: i64) -> Option<Self> {
228		let mut s = self.seconds.checked_sub(rhs_s)?;
229		let mut n_total = i64::from(self.nanos) - rhs_n;
230
231		if n_total >= Self::NANOS_PER_SEC {
232			s = s.checked_add(1)?;
233			n_total -= Self::NANOS_PER_SEC;
234		} else if n_total <= -Self::NANOS_PER_SEC {
235			s = s.checked_sub(1)?;
236			n_total += Self::NANOS_PER_SEC;
237		}
238
239		#[allow(clippy::cast_possible_truncation)]
240		Self::align_signs(s, n_total as i32)
241	}
242
243	/// Returns the total nanoseconds for this instance.
244	#[inline]
245	#[must_use]
246	pub const fn total_nanos(&self) -> i128 {
247		(self.seconds as i128) * (Self::NANOS_PER_SEC_I128) + (self.nanos as i128)
248	}
249
250	/// Creates a new normalized instance from a given amount of nanoseconds.
251	#[must_use]
252	#[inline]
253	pub fn from_total_nanos(total: i128) -> Option<Self> {
254		let factor = Self::NANOS_PER_SEC_I128;
255
256		// Integer division truncates towards zero (correct for seconds)
257		let seconds_val = total / factor;
258		let seconds = i64::try_from(seconds_val).ok()?;
259
260		// Remainder has same sign as dividend
261		let nanos_val = total % factor;
262		// Remainder guaranteed to fit in i32
263		#[allow(clippy::cast_possible_truncation)]
264		let nanos = nanos_val as i32;
265
266		Some(Self { seconds, nanos })
267	}
268
269	/// Multiplies the Duration by an i64 scalar, returning `Some(Duration)` or `None` on overflow.
270	#[must_use]
271	#[inline]
272	pub fn checked_mul(&self, rhs: i64) -> Option<Self> {
273		let total = self.total_nanos().checked_mul(i128::from(rhs))?;
274		Self::from_total_nanos(total)
275	}
276
277	/// Adds another Duration to this one, returning `Some(Duration)` or `None` on overflow.
278	#[must_use]
279	#[inline]
280	pub fn checked_add(&self, other: &Self) -> Option<Self> {
281		self.checked_add_raw(other.seconds, other.nanos.into())
282	}
283
284	/// Subtracts another Duration from this one, returning `Some(Duration)` or `None` on overflow.
285	#[must_use]
286	#[inline]
287	pub fn checked_sub(&self, other: &Self) -> Option<Self> {
288		self.checked_sub_raw(other.seconds, other.nanos.into())
289	}
290
291	/// Divides the Duration by an i64 scalar, returning `Some(Duration)` or `None` on overflow.
292	#[must_use]
293	#[inline]
294	pub fn checked_div(&self, rhs: i64) -> Option<Self> {
295		if rhs == 0 {
296			return None;
297		}
298		let total = self.total_nanos().checked_div(i128::from(rhs))?;
299		Self::from_total_nanos(total)
300	}
301}
302
303#[cfg(test)]
304mod tests {
305	use super::*;
306	use core::cmp::Ordering;
307
308	macro_rules! get_duration {
309		(duration, $secs:literal, $nanos:literal) => {
310			Duration::new($secs, $nanos)
311		};
312
313		(std, $secs:literal, $nanos:literal) => {
314			StdDuration::new($secs, $nanos)
315		};
316
317		(chrono, $secs:literal, $nanos:literal) => {
318			TimeDelta::new($secs, $nanos).unwrap()
319		};
320	}
321
322	macro_rules! test_ops {
323		($duration:ident) => {
324			#[test]
325			fn test_add_sub() {
326				// 1. Add causing carry
327				let d1 = dur(1, 900_000_000);
328				let d2 = get_duration!($duration, 0, 200_000_000);
329				let sum = d1 + d2;
330				assert_eq!(sum.seconds, 2);
331				assert_eq!(sum.nanos, 100_000_000);
332
333				// 2. Sub causing borrow
334				let d1 = dur(2, 100_000_000);
335				let d2 = get_duration!($duration, 0, 200_000_000);
336				let diff = d1 - d2;
337				assert_eq!(diff.seconds, 1);
338				assert_eq!(diff.nanos, 900_000_000);
339
340				// 3. Sub causing negative result
341				let d1 = dur(1, 0);
342				let d2 = get_duration!($duration, 2, 0);
343				let diff = d1 - d2;
344				// -1s
345				assert_eq!(diff.seconds, -1);
346				assert_eq!(diff.nanos, 0);
347			}
348
349			#[test]
350			fn test_eq_ord() {
351				let d1 = dur(1, 500);
352				let d2 = get_duration!($duration, 1, 600);
353				let d3 = get_duration!($duration, 2, 0);
354
355				assert!(d1 < d2);
356				assert!(d2 < d3);
357
358				// Test normalization-independent comparison
359				// 0s + 1.5B nanos vs 1s + 500M nanos (Should be Equal)
360				let unnormalized = dur(0, 1_500_000_000);
361				let normalized = get_duration!($duration, 1, 500_000_000);
362				assert_eq!(unnormalized.partial_cmp(&normalized), Some(Ordering::Equal));
363			}
364		};
365	}
366
367	test_ops!(duration);
368
369	mod std_test {
370		use super::*;
371
372		test_ops!(std);
373	}
374
375	#[cfg(feature = "chrono")]
376	mod chrono_test {
377		use super::*;
378		use chrono::TimeDelta;
379
380		test_ops!(chrono);
381	}
382
383	fn dur(s: i64, n: i32) -> Duration {
384		Duration {
385			seconds: s,
386			nanos: n,
387		}
388	}
389
390	#[test]
391	fn test_mul_overflow_checks() {
392		// 1. Basic
393		let d = dur(10, 0);
394		assert_eq!(d * 2, dur(20, 0));
395
396		// 2. Overflow i64 seconds via huge multiplier
397		let huge = dur(i64::MAX / 2 + 100, 0);
398		assert!(huge.checked_mul(2).is_none());
399
400		// 3. Nanos overflow contributing to seconds overflow
401		let edge = dur(i64::MAX, 0);
402		assert!(edge.checked_mul(1).is_some());
403
404		// i64::MAX s + 1s via nanos carry -> Overflow
405		let edge_nanos = dur(i64::MAX - 1, 600_000_000);
406		// * 2 -> (MAX-1)*2 s + 1.2s -> Overflow
407		assert!(edge_nanos.checked_mul(2).is_none());
408	}
409
410	#[test]
411	fn test_div_bug_fix() {
412		// Before the i128-based impl, this would have failed
413		let numerator = dur(10_000_000_000, 0); // 10B seconds
414		let divisor = 11_000_000_000; // 11B
415
416		// Expected: 0 seconds, ~0.909s (909,090,909 nanos)
417		let res = numerator.checked_div(divisor).unwrap();
418		assert_eq!(res.seconds, 0);
419		// 10B * 1e9 / 11B = 10 * 1e9 / 11 = 909090909
420		assert_eq!(res.nanos, 909_090_909);
421	}
422
423	#[test]
424	fn test_div_edge_cases() {
425		// Division by zero
426		let d = dur(1, 0);
427		assert!(d.checked_div(0).is_none());
428
429		// Clean division
430		let d = dur(1, 0);
431		let res: Duration = d / 2;
432		assert_eq!(res.seconds, 0);
433		assert_eq!(res.nanos, 500_000_000);
434
435		// Negative division
436		let d = dur(-1, 0);
437		let res: Duration = d / 2;
438		assert_eq!(res.seconds, 0);
439		assert_eq!(res.nanos, -500_000_000);
440	}
441
442	#[test]
443	fn test_get_data_smoke_test() {
444		// Just verifying the math helper runs without panic
445		// 1 Year + 1 Month + 1 Day ...
446		let d = dur(31_557_600 + 1, 0); // Approx 1 year + 1 sec
447		let data = d.get_data();
448		assert_eq!(data.years.value, 1);
449	}
450}