Skip to main content

reifydb_value/value/
duration.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#![allow(clippy::disallowed_types)]
5
6use std::{
7	fmt::{self, Display, Formatter, Write},
8	str::FromStr,
9	time::Duration as StdDuration,
10};
11
12use serde::{Deserialize, Deserializer, Serialize, de::Error as DeserializeError};
13
14use crate::{
15	error::{Error, TemporalKind, TypeError},
16	fragment::Fragment,
17	reifydb_assertions,
18	value::temporal::parse::duration::parse_duration,
19};
20
21#[repr(C)]
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
23pub struct Duration {
24	months: i32,
25	days: i32,
26	nanos: i64,
27}
28
29impl<'de> Deserialize<'de> for Duration {
30	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31	where
32		D: Deserializer<'de>,
33	{
34		#[derive(Deserialize)]
35		struct RawDuration {
36			months: i32,
37			days: i32,
38			nanos: i64,
39		}
40
41		let raw = RawDuration::deserialize(deserializer)?;
42		Duration::normalized(raw.months, raw.days, raw.nanos).map_err(DeserializeError::custom)
43	}
44}
45
46const NANOS_PER_DAY: i64 = 86_400_000_000_000;
47const SECONDS_PER_DAY: i64 = 86_400;
48const DAYS_PER_MONTH: i64 = 30;
49const SECONDS_PER_YEAR: i64 = 31_557_600;
50
51impl Default for Duration {
52	fn default() -> Self {
53		Self::zero()
54	}
55}
56
57impl Duration {
58	fn overflow_err(message: impl Into<String>) -> TypeError {
59		TypeError::Temporal {
60			kind: TemporalKind::DurationOverflow {
61				message: message.into(),
62			},
63			message: "duration overflow".to_string(),
64			fragment: Fragment::None,
65		}
66	}
67
68	fn mixed_sign_err(days: i32, nanos: i64) -> TypeError {
69		TypeError::Temporal {
70			kind: TemporalKind::DurationMixedSign {
71				days,
72				nanos,
73			},
74			message: format!(
75				"duration days and nanos must share the same sign, got days={days}, nanos={nanos}"
76			),
77			fragment: Fragment::None,
78		}
79	}
80
81	fn normalized(months: i32, days: i32, nanos: i64) -> Result<Self, Box<TypeError>> {
82		let extra_days = i32::try_from(nanos / NANOS_PER_DAY)
83			.map_err(|_| Box::new(Self::overflow_err("the days component overflows Duration range")))?;
84		let nanos = nanos % NANOS_PER_DAY;
85		let days = days
86			.checked_add(extra_days)
87			.ok_or_else(|| Box::new(Self::overflow_err("the days component overflows Duration range")))?;
88
89		if (days > 0 && nanos < 0) || (days < 0 && nanos > 0) {
90			return Err(Box::new(Self::mixed_sign_err(days, nanos)));
91		}
92
93		Ok(Self {
94			months,
95			days,
96			nanos,
97		})
98	}
99
100	pub fn new(months: i32, days: i32, nanos: i64) -> Result<Self, Box<TypeError>> {
101		Self::normalized(months, days, nanos)
102	}
103
104	pub fn from_seconds(seconds: i64) -> Result<Self, Box<TypeError>> {
105		Self::normalized(0, 0, Self::scaled(seconds, 1_000_000_000, "seconds")?)
106	}
107
108	pub fn from_milliseconds(milliseconds: i64) -> Result<Self, Box<TypeError>> {
109		Self::normalized(0, 0, Self::scaled(milliseconds, 1_000_000, "milliseconds")?)
110	}
111
112	fn scaled(amount: i64, nanos_per_unit: i64, unit: &str) -> Result<i64, Box<TypeError>> {
113		amount.checked_mul(nanos_per_unit).ok_or_else(|| {
114			Box::new(Self::overflow_err(format!("{} {} overflows Duration range", amount, unit)))
115		})
116	}
117
118	pub fn from_microseconds(microseconds: i64) -> Result<Self, Box<TypeError>> {
119		let nanos = microseconds.checked_mul(1_000).ok_or_else(|| {
120			Box::new(Self::overflow_err(format!("{} microseconds overflows Duration range", microseconds)))
121		})?;
122		Self::normalized(0, 0, nanos)
123	}
124
125	pub fn from_micros_infallible(microseconds: u64) -> Self {
126		const US_PER_DAY: u64 = 86_400_000_000;
127		let whole_days = microseconds / US_PER_DAY;
128		let remainder_us = microseconds % US_PER_DAY;
129		let days = if whole_days > i32::MAX as u64 {
130			i32::MAX
131		} else {
132			whole_days as i32
133		};
134		Self {
135			months: 0,
136			days,
137			nanos: (remainder_us * 1_000) as i64,
138		}
139	}
140
141	pub fn from_nanos_infallible(nanoseconds: u64) -> Self {
142		const NS_PER_DAY: u64 = 86_400_000_000_000;
143		let whole_days = nanoseconds / NS_PER_DAY;
144		let remainder_ns = nanoseconds % NS_PER_DAY;
145		let days = if whole_days > i32::MAX as u64 {
146			i32::MAX
147		} else {
148			whole_days as i32
149		};
150		Self {
151			months: 0,
152			days,
153			nanos: remainder_ns as i64,
154		}
155	}
156
157	pub const fn from_nanoseconds_const(nanoseconds: i64) -> Self {
158		reifydb_assertions! {
159			let day_count = nanoseconds / NANOS_PER_DAY;
160			assert!(
161				day_count >= i32::MIN as i64 && day_count <= i32::MAX as i64,
162				"nanoseconds overflows Duration range"
163			);
164		}
165		Self {
166			months: 0,
167			days: (nanoseconds / NANOS_PER_DAY) as i32,
168			nanos: nanoseconds % NANOS_PER_DAY,
169		}
170	}
171
172	pub const fn from_microseconds_const(microseconds: i64) -> Self {
173		reifydb_assertions! {
174			assert!(
175				microseconds.checked_mul(1_000).is_some(),
176				"microseconds overflows Duration range"
177			);
178		}
179		Self::from_nanoseconds_const(microseconds * 1_000)
180	}
181
182	pub const fn from_milliseconds_const(milliseconds: i64) -> Self {
183		reifydb_assertions! {
184			assert!(
185				milliseconds.checked_mul(1_000_000).is_some(),
186				"milliseconds overflows Duration range"
187			);
188		}
189		Self::from_nanoseconds_const(milliseconds * 1_000_000)
190	}
191
192	pub const fn from_seconds_const(seconds: i64) -> Self {
193		reifydb_assertions! {
194			assert!(
195				seconds.checked_mul(1_000_000_000).is_some(),
196				"seconds overflows Duration range"
197			);
198		}
199		Self::from_nanoseconds_const(seconds * 1_000_000_000)
200	}
201
202	pub const fn from_minutes_const(minutes: i64) -> Self {
203		reifydb_assertions! {
204			assert!(
205				minutes.checked_mul(60_000_000_000).is_some(),
206				"minutes overflows Duration range"
207			);
208		}
209		Self::from_nanoseconds_const(minutes * 60_000_000_000)
210	}
211
212	pub const fn from_hours_const(hours: i64) -> Self {
213		reifydb_assertions! {
214			assert!(
215				hours.checked_mul(3_600_000_000_000).is_some(),
216				"hours overflows Duration range"
217			);
218		}
219		Self::from_nanoseconds_const(hours * 3_600_000_000_000)
220	}
221
222	pub fn from_nanoseconds(nanoseconds: i64) -> Result<Self, Box<TypeError>> {
223		Self::normalized(0, 0, nanoseconds)
224	}
225
226	pub fn from_minutes(minutes: i64) -> Result<Self, Box<TypeError>> {
227		Self::normalized(0, 0, Self::scaled(minutes, 60_000_000_000, "minutes")?)
228	}
229
230	pub fn from_hours(hours: i64) -> Result<Self, Box<TypeError>> {
231		Self::normalized(0, 0, Self::scaled(hours, 3_600_000_000_000, "hours")?)
232	}
233
234	pub fn from_days(days: i64) -> Result<Self, Box<TypeError>> {
235		let days = i32::try_from(days)
236			.map_err(|_| Box::new(Self::overflow_err(format!("{} days overflows Duration range", days))))?;
237		Self::normalized(0, days, 0)
238	}
239
240	pub fn from_weeks(weeks: i64) -> Result<Self, Box<TypeError>> {
241		let days = weeks.checked_mul(7).ok_or_else(|| {
242			Box::new(Self::overflow_err(format!("{} weeks overflows Duration range", weeks)))
243		})?;
244		let days = i32::try_from(days).map_err(|_| {
245			Box::new(Self::overflow_err(format!("{} weeks overflows Duration range", weeks)))
246		})?;
247		Self::normalized(0, days, 0)
248	}
249
250	pub fn from_months(months: i64) -> Result<Self, Box<TypeError>> {
251		let months = i32::try_from(months).map_err(|_| {
252			Box::new(Self::overflow_err(format!("{} months overflows Duration range", months)))
253		})?;
254		Self::normalized(months, 0, 0)
255	}
256
257	pub fn from_years(years: i64) -> Result<Self, Box<TypeError>> {
258		let months = years.checked_mul(12).ok_or_else(|| {
259			Box::new(Self::overflow_err(format!("{} years overflows Duration range", years)))
260		})?;
261		let months = i32::try_from(months).map_err(|_| {
262			Box::new(Self::overflow_err(format!("{} years overflows Duration range", years)))
263		})?;
264		Self::normalized(months, 0, 0)
265	}
266
267	pub fn zero() -> Self {
268		Self {
269			months: 0,
270			days: 0,
271			nanos: 0,
272		}
273	}
274
275	pub const MAX: Self = Self {
276		months: i32::MAX,
277		days: i32::MAX,
278		nanos: NANOS_PER_DAY - 1,
279	};
280
281	fn checked_total(
282		&self,
283		per_year: i64,
284		per_month: i64,
285		per_day: i64,
286		sub_day: i64,
287		unit: &str,
288	) -> Result<i64, Box<TypeError>> {
289		let years = (self.months / 12) as i64;
290		let rem_months = (self.months % 12) as i64;
291		years.checked_mul(per_year)
292			.and_then(|a| rem_months.checked_mul(per_month).and_then(|b| a.checked_add(b)))
293			.and_then(|a| (self.days as i64).checked_mul(per_day).and_then(|b| a.checked_add(b)))
294			.and_then(|a| a.checked_add(sub_day))
295			.ok_or_else(|| Box::new(Self::overflow_err(format!("Duration overflows {} range", unit))))
296	}
297
298	pub fn seconds(&self) -> Result<i64, Box<TypeError>> {
299		self.checked_total(
300			SECONDS_PER_YEAR,
301			DAYS_PER_MONTH * SECONDS_PER_DAY,
302			SECONDS_PER_DAY,
303			self.nanos / 1_000_000_000,
304			"seconds",
305		)
306	}
307
308	pub fn milliseconds(&self) -> Result<i64, Box<TypeError>> {
309		self.checked_total(
310			SECONDS_PER_YEAR * 1_000,
311			DAYS_PER_MONTH * SECONDS_PER_DAY * 1_000,
312			SECONDS_PER_DAY * 1_000,
313			self.nanos / 1_000_000,
314			"milliseconds",
315		)
316	}
317
318	pub fn microseconds(&self) -> Result<i64, Box<TypeError>> {
319		self.checked_total(
320			SECONDS_PER_YEAR * 1_000_000,
321			DAYS_PER_MONTH * SECONDS_PER_DAY * 1_000_000,
322			SECONDS_PER_DAY * 1_000_000,
323			self.nanos / 1_000,
324			"microseconds",
325		)
326	}
327
328	pub fn nanoseconds(&self) -> Result<i64, Box<TypeError>> {
329		self.checked_total(
330			SECONDS_PER_YEAR * 1_000_000_000,
331			NANOS_PER_DAY * DAYS_PER_MONTH,
332			NANOS_PER_DAY,
333			self.nanos,
334			"nanoseconds",
335		)
336	}
337
338	pub fn get_months(&self) -> i32 {
339		self.months
340	}
341
342	pub fn get_days(&self) -> i32 {
343		self.days
344	}
345
346	pub fn get_nanos(&self) -> i64 {
347		self.nanos
348	}
349
350	pub fn as_nanos(&self) -> Result<i64, Box<TypeError>> {
351		self.nanoseconds()
352	}
353
354	pub fn is_positive(&self) -> bool {
355		self.months >= 0
356			&& self.days >= 0 && self.nanos >= 0
357			&& (self.months > 0 || self.days > 0 || self.nanos > 0)
358	}
359
360	pub fn is_negative(&self) -> bool {
361		self.months <= 0
362			&& self.days <= 0 && self.nanos <= 0
363			&& (self.months < 0 || self.days < 0 || self.nanos < 0)
364	}
365
366	pub fn abs(&self) -> Result<Self, Box<TypeError>> {
367		let absed = self
368			.months
369			.checked_abs()
370			.zip(self.days.checked_abs())
371			.zip(self.nanos.checked_abs())
372			.ok_or_else(|| {
373				Box::new(Self::overflow_err(
374					"taking the absolute value of the Duration overflows its range".to_string(),
375				))
376			})?;
377		let ((months, days), nanos) = absed;
378		Ok(Self {
379			months,
380			days,
381			nanos,
382		})
383	}
384
385	pub fn negate(&self) -> Result<Self, Box<TypeError>> {
386		let negated = self
387			.months
388			.checked_neg()
389			.zip(self.days.checked_neg())
390			.zip(self.nanos.checked_neg())
391			.ok_or_else(|| {
392				Box::new(Self::overflow_err("negating the Duration overflows its range".to_string()))
393			})?;
394		let ((months, days), nanos) = negated;
395		Ok(Self {
396			months,
397			days,
398			nanos,
399		})
400	}
401
402	pub fn to_iso_string(&self) -> String {
403		if self.months == 0 && self.days == 0 && self.nanos == 0 {
404			return "PT0S".to_string();
405		}
406
407		let mut result = String::from("P");
408
409		let years = self.months / 12;
410		let months = self.months % 12;
411
412		if years != 0 {
413			write!(result, "{}Y", years).unwrap();
414		}
415		if months != 0 {
416			write!(result, "{}M", months).unwrap();
417		}
418
419		let total_seconds = self.nanos / 1_000_000_000;
420		let remaining_nanos = self.nanos % 1_000_000_000;
421
422		let extra_days = total_seconds / 86400;
423		let remaining_seconds = total_seconds % 86400;
424
425		let display_days = self.days + extra_days as i32;
426		let hours = remaining_seconds / 3600;
427		let minutes = (remaining_seconds % 3600) / 60;
428		let seconds = remaining_seconds % 60;
429
430		if display_days != 0 {
431			write!(result, "{}D", display_days).unwrap();
432		}
433
434		if hours != 0 || minutes != 0 || seconds != 0 || remaining_nanos != 0 {
435			result.push('T');
436
437			if hours != 0 {
438				write!(result, "{}H", hours).unwrap();
439			}
440			if minutes != 0 {
441				write!(result, "{}M", minutes).unwrap();
442			}
443			if seconds != 0 || remaining_nanos != 0 {
444				if remaining_nanos != 0 {
445					let fractional = remaining_nanos as f64 / 1_000_000_000.0;
446					let total_seconds_f = seconds as f64 + fractional;
447					let formatted_str = format!("{:.9}", total_seconds_f);
448					let formatted = formatted_str.trim_end_matches('0').trim_end_matches('.');
449					write!(result, "{}S", formatted).unwrap();
450				} else {
451					write!(result, "{}S", seconds).unwrap();
452				}
453			}
454		}
455
456		result
457	}
458}
459
460impl Duration {
461	pub fn try_add(self, rhs: Self) -> Result<Self, Box<TypeError>> {
462		let months = self
463			.months
464			.checked_add(rhs.months)
465			.ok_or_else(|| Box::new(Self::overflow_err("the months component overflows Duration range")))?;
466		let days = self
467			.days
468			.checked_add(rhs.days)
469			.ok_or_else(|| Box::new(Self::overflow_err("the days component overflows Duration range")))?;
470		let nanos = self.nanos.checked_add(rhs.nanos).ok_or_else(|| {
471			Box::new(Self::overflow_err("the nanoseconds component overflows Duration range"))
472		})?;
473		Self::normalized(months, days, nanos)
474	}
475
476	pub fn try_sub(self, rhs: Self) -> Result<Self, Box<TypeError>> {
477		let months = self
478			.months
479			.checked_sub(rhs.months)
480			.ok_or_else(|| Box::new(Self::overflow_err("the months component overflows Duration range")))?;
481		let days = self
482			.days
483			.checked_sub(rhs.days)
484			.ok_or_else(|| Box::new(Self::overflow_err("the days component overflows Duration range")))?;
485		let nanos = self.nanos.checked_sub(rhs.nanos).ok_or_else(|| {
486			Box::new(Self::overflow_err("the nanoseconds component overflows Duration range"))
487		})?;
488		Self::normalized(months, days, nanos)
489	}
490
491	pub fn try_mul(self, rhs: i64) -> Result<Self, Box<TypeError>> {
492		let rhs_i32 = i32::try_from(rhs).map_err(|_| {
493			Box::new(Self::overflow_err(format!("multiplier {} overflows Duration range", rhs)))
494		})?;
495		let months = self
496			.months
497			.checked_mul(rhs_i32)
498			.ok_or_else(|| Box::new(Self::overflow_err("the months component overflows Duration range")))?;
499		let days = self
500			.days
501			.checked_mul(rhs_i32)
502			.ok_or_else(|| Box::new(Self::overflow_err("the days component overflows Duration range")))?;
503		let nanos = self.nanos.checked_mul(rhs).ok_or_else(|| {
504			Box::new(Self::overflow_err("the nanoseconds component overflows Duration range"))
505		})?;
506		Self::normalized(months, days, nanos)
507	}
508
509	pub fn div_duration(self, rhs: Self) -> Result<i64, Box<TypeError>> {
510		let divisor = rhs.as_nanos()?;
511		if divisor == 0 {
512			return Err(Box::new(Self::overflow_err("cannot divide a duration by a zero duration")));
513		}
514		Ok(self.as_nanos()? / divisor)
515	}
516
517	fn saturating_normalized(months: i32, days: i32, nanos: i64) -> Self {
518		let total = days as i128 * NANOS_PER_DAY as i128 + nanos as i128;
519		let new_days = (total / NANOS_PER_DAY as i128).clamp(i32::MIN as i128, i32::MAX as i128) as i32;
520		let new_nanos = (total % NANOS_PER_DAY as i128) as i64;
521		Self {
522			months,
523			days: new_days,
524			nanos: new_nanos,
525		}
526	}
527
528	pub fn saturating_add(self, rhs: Self) -> Self {
529		Self::saturating_normalized(
530			self.months.saturating_add(rhs.months),
531			self.days.saturating_add(rhs.days),
532			self.nanos.saturating_add(rhs.nanos),
533		)
534	}
535
536	pub fn saturating_sub(self, rhs: Self) -> Self {
537		Self::saturating_normalized(
538			self.months.saturating_sub(rhs.months),
539			self.days.saturating_sub(rhs.days),
540			self.nanos.saturating_sub(rhs.nanos),
541		)
542	}
543
544	pub fn saturating_mul(self, rhs: i64) -> Self {
545		let months = (self.months as i128 * rhs as i128).clamp(i32::MIN as i128, i32::MAX as i128) as i32;
546		let days = (self.days as i128 * rhs as i128).clamp(i32::MIN as i128, i32::MAX as i128) as i32;
547		Self::saturating_normalized(months, days, self.nanos.saturating_mul(rhs))
548	}
549}
550
551impl Display for Duration {
552	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
553		if self.months == 0 && self.days == 0 && self.nanos == 0 {
554			return write!(f, "0s");
555		}
556
557		let years = self.months / 12;
558		let months = self.months % 12;
559
560		let total_seconds = self.nanos / 1_000_000_000;
561		let remaining_nanos = self.nanos % 1_000_000_000;
562
563		let extra_days = total_seconds / 86400;
564		let remaining_seconds = total_seconds % 86400;
565
566		let display_days = self.days + extra_days as i32;
567		let hours = remaining_seconds / 3600;
568		let minutes = (remaining_seconds % 3600) / 60;
569		let seconds = remaining_seconds % 60;
570
571		let abs_remaining = remaining_nanos.abs();
572		let ms = abs_remaining / 1_000_000;
573		let us = (abs_remaining % 1_000_000) / 1_000;
574		let ns = abs_remaining % 1_000;
575
576		if years != 0 {
577			write!(f, "{}y", years)?;
578		}
579		if months != 0 {
580			write!(f, "{}mo", months)?;
581		}
582		if display_days != 0 {
583			write!(f, "{}d", display_days)?;
584		}
585		if hours != 0 {
586			write!(f, "{}h", hours)?;
587		}
588		if minutes != 0 {
589			write!(f, "{}m", minutes)?;
590		}
591		if seconds != 0 {
592			write!(f, "{}s", seconds)?;
593		}
594
595		if ms != 0 || us != 0 || ns != 0 {
596			if remaining_nanos < 0
597				&& seconds == 0 && hours == 0
598				&& minutes == 0 && display_days == 0
599				&& years == 0 && months == 0
600			{
601				write!(f, "-")?;
602			}
603			if ms != 0 {
604				write!(f, "{}ms", ms)?;
605			}
606			if us != 0 {
607				write!(f, "{}us", us)?;
608			}
609			if ns != 0 {
610				write!(f, "{}ns", ns)?;
611			}
612		}
613
614		Ok(())
615	}
616}
617
618impl Duration {
619	pub fn is_zero(&self) -> bool {
620		self.months == 0 && self.days == 0 && self.nanos == 0
621	}
622
623	pub fn to_std(&self) -> StdDuration {
624		let years = (self.months / 12) as i128;
625		let rem_months = (self.months % 12) as i128;
626		let total = years * SECONDS_PER_YEAR as i128 * 1_000_000_000
627			+ rem_months * DAYS_PER_MONTH as i128 * NANOS_PER_DAY as i128
628			+ self.days as i128 * NANOS_PER_DAY as i128
629			+ self.nanos as i128;
630		StdDuration::from_nanos(u64::try_from(total.max(0)).unwrap_or(u64::MAX))
631	}
632
633	pub fn from_std(duration: StdDuration) -> Self {
634		let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
635		Self::from_nanos_infallible(nanos)
636	}
637}
638
639impl From<Duration> for StdDuration {
640	fn from(duration: Duration) -> Self {
641		duration.to_std()
642	}
643}
644
645impl From<StdDuration> for Duration {
646	fn from(duration: StdDuration) -> Self {
647		Self::from_std(duration)
648	}
649}
650
651impl FromStr for Duration {
652	type Err = Error;
653
654	fn from_str(s: &str) -> Result<Self, Self::Err> {
655		parse_duration(Fragment::internal(s.trim()))
656	}
657}
658
659#[cfg(test)]
660pub mod tests {
661	use super::*;
662	use crate::error::TemporalKind;
663
664	mod arrow_layout {
665		use std::mem::offset_of;
666
667		use arrow_buffer::IntervalMonthDayNano;
668
669		use super::*;
670
671		#[test]
672		fn fields_sit_at_the_interval_month_day_nano_offsets() {
673			// Fields must sit at the interval offsets, otherwise the typed slice misreads every row.
674			assert_eq!(offset_of!(Duration, months), offset_of!(IntervalMonthDayNano, months));
675			assert_eq!(offset_of!(Duration, days), offset_of!(IntervalMonthDayNano, days));
676			assert_eq!(offset_of!(Duration, nanos), offset_of!(IntervalMonthDayNano, nanoseconds));
677			assert_eq!(size_of::<Duration>(), size_of::<IntervalMonthDayNano>());
678			assert_eq!(align_of::<Duration>(), align_of::<IntervalMonthDayNano>());
679		}
680	}
681
682	mod max {
683		use super::*;
684
685		#[test]
686		fn adding_max_to_a_duration_saturates_instead_of_panicking() {
687			// MAX backs the `forever` literal, added to an instant on every enqueue of a
688			// never-expiring record; panicking here would take down the insert path.
689			let saturated = Duration::from_hours_const(1).saturating_add(Duration::MAX);
690			assert_eq!(saturated.months, i32::MAX);
691			assert_eq!(saturated.days, i32::MAX);
692		}
693
694		#[test]
695		fn max_outranks_every_ordinary_window() {
696			// `forever` must outrank every window a user can spell, or an expiry check reads
697			// it as already elapsed.
698			assert_eq!(Duration::MAX, Duration::MAX);
699			for window in [
700				Duration::from_seconds_const(1),
701				Duration::from_hours_const(24),
702				Duration::from_seconds_const(365 * 86_400),
703			] {
704				assert!(window < Duration::MAX, "{window:?} must be shorter than forever");
705			}
706		}
707
708		#[test]
709		fn max_is_normalised() {
710			// A denormalised constant would compare wrongly against durations built through
711			// the constructors, which all keep nanos inside a single day.
712			const { assert!(Duration::MAX.nanos < NANOS_PER_DAY) };
713			const { assert!(Duration::MAX.nanos >= 0) };
714		}
715	}
716
717	mod ordering {
718		use std::cmp::Ordering;
719
720		use super::*;
721
722		#[test]
723		fn days_shorter_than_a_month_sort_below_it() {
724			// The inverse direction: a zero months field must not push a shorter span above a
725			// month.
726			let month = Duration::from_months(1).unwrap();
727			let twenty_nine_days = Duration::from_days(29).unwrap();
728
729			assert_eq!(
730				twenty_nine_days.cmp(&month),
731				Ordering::Less,
732				"twenty-nine days is shorter than a thirty day month and must sort below it"
733			);
734		}
735
736		#[test]
737		fn a_bound_expressed_in_seconds_rejects_shorter_spans_in_any_unit() {
738			// TTL validation compares against a nanos-only bound with a plain `<`, so the
739			// bound only holds if ordering measures elapsed time across units.
740			let one_second = Duration::from_seconds(1).unwrap();
741
742			assert!(
743				Duration::from_milliseconds(500).unwrap() < one_second,
744				"500ms is below a one second bound"
745			);
746			assert!(Duration::from_days(1).unwrap() > one_second, "a day is above a one second bound");
747			assert!(Duration::from_months(1).unwrap() > one_second, "a month is above a one second bound");
748		}
749	}
750
751	fn assert_overflow(result: Result<Duration, Box<TypeError>>) {
752		let err = result.expect_err("expected DurationOverflow error");
753		match *err {
754			TypeError::Temporal {
755				kind: TemporalKind::DurationOverflow {
756					..
757				},
758				..
759			} => {}
760			other => panic!("expected DurationOverflow, got: {:?}", other),
761		}
762	}
763
764	fn assert_mixed_sign(result: Result<Duration, Box<TypeError>>, expected_days: i32, expected_nanos: i64) {
765		let err = result.expect_err("expected DurationMixedSign error");
766		match *err {
767			TypeError::Temporal {
768				kind: TemporalKind::DurationMixedSign {
769					days,
770					nanos,
771				},
772				..
773			} => {
774				assert_eq!(days, expected_days, "days mismatch");
775				assert_eq!(nanos, expected_nanos, "nanos mismatch");
776			}
777			other => panic!("expected DurationMixedSign, got: {:?}", other),
778		}
779	}
780
781	#[test]
782	fn test_duration_iso_string_zero() {
783		assert_eq!(Duration::zero().to_iso_string(), "PT0S");
784		assert_eq!(Duration::from_seconds(0).unwrap().to_iso_string(), "PT0S");
785		assert_eq!(Duration::from_nanoseconds(0).unwrap().to_iso_string(), "PT0S");
786		assert_eq!(Duration::default().to_iso_string(), "PT0S");
787	}
788
789	#[test]
790	fn test_duration_iso_string_seconds() {
791		assert_eq!(Duration::from_seconds(1).unwrap().to_iso_string(), "PT1S");
792		assert_eq!(Duration::from_seconds(30).unwrap().to_iso_string(), "PT30S");
793		assert_eq!(Duration::from_seconds(59).unwrap().to_iso_string(), "PT59S");
794	}
795
796	#[test]
797	fn test_duration_iso_string_minutes() {
798		assert_eq!(Duration::from_minutes(1).unwrap().to_iso_string(), "PT1M");
799		assert_eq!(Duration::from_minutes(30).unwrap().to_iso_string(), "PT30M");
800		assert_eq!(Duration::from_minutes(59).unwrap().to_iso_string(), "PT59M");
801	}
802
803	#[test]
804	fn test_duration_iso_string_hours() {
805		assert_eq!(Duration::from_hours(1).unwrap().to_iso_string(), "PT1H");
806		assert_eq!(Duration::from_hours(12).unwrap().to_iso_string(), "PT12H");
807		assert_eq!(Duration::from_hours(23).unwrap().to_iso_string(), "PT23H");
808	}
809
810	#[test]
811	fn test_duration_iso_string_days() {
812		assert_eq!(Duration::from_days(1).unwrap().to_iso_string(), "P1D");
813		assert_eq!(Duration::from_days(7).unwrap().to_iso_string(), "P7D");
814		assert_eq!(Duration::from_days(365).unwrap().to_iso_string(), "P365D");
815	}
816
817	#[test]
818	fn test_duration_iso_string_weeks() {
819		assert_eq!(Duration::from_weeks(1).unwrap().to_iso_string(), "P7D");
820		assert_eq!(Duration::from_weeks(2).unwrap().to_iso_string(), "P14D");
821		assert_eq!(Duration::from_weeks(52).unwrap().to_iso_string(), "P364D");
822	}
823
824	#[test]
825	fn test_duration_iso_string_combined_time() {
826		let d = Duration::new(0, 0, (60 * 60 + 30 * 60) * 1_000_000_000).unwrap();
827		assert_eq!(d.to_iso_string(), "PT1H30M");
828
829		let d = Duration::new(0, 0, (5 * 60 + 45) * 1_000_000_000).unwrap();
830		assert_eq!(d.to_iso_string(), "PT5M45S");
831
832		let d = Duration::new(0, 0, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap();
833		assert_eq!(d.to_iso_string(), "PT2H30M45S");
834	}
835
836	#[test]
837	fn test_duration_iso_string_combined_date_time() {
838		assert_eq!(Duration::new(0, 1, 2 * 60 * 60 * 1_000_000_000).unwrap().to_iso_string(), "P1DT2H");
839		assert_eq!(Duration::new(0, 1, 30 * 60 * 1_000_000_000).unwrap().to_iso_string(), "P1DT30M");
840		assert_eq!(
841			Duration::new(0, 1, (2 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap().to_iso_string(),
842			"P1DT2H30M"
843		);
844		assert_eq!(
845			Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap().to_iso_string(),
846			"P1DT2H30M45S"
847		);
848	}
849
850	#[test]
851	fn test_duration_iso_string_milliseconds() {
852		assert_eq!(Duration::from_milliseconds(123).unwrap().to_iso_string(), "PT0.123S");
853		assert_eq!(Duration::from_milliseconds(1).unwrap().to_iso_string(), "PT0.001S");
854		assert_eq!(Duration::from_milliseconds(999).unwrap().to_iso_string(), "PT0.999S");
855		assert_eq!(Duration::from_milliseconds(1500).unwrap().to_iso_string(), "PT1.5S");
856	}
857
858	#[test]
859	fn test_duration_iso_string_microseconds() {
860		assert_eq!(Duration::from_microseconds(123456).unwrap().to_iso_string(), "PT0.123456S");
861		assert_eq!(Duration::from_microseconds(1).unwrap().to_iso_string(), "PT0.000001S");
862		assert_eq!(Duration::from_microseconds(999999).unwrap().to_iso_string(), "PT0.999999S");
863		assert_eq!(Duration::from_microseconds(1500000).unwrap().to_iso_string(), "PT1.5S");
864	}
865
866	#[test]
867	fn test_duration_iso_string_nanoseconds() {
868		assert_eq!(Duration::from_nanoseconds(123456789).unwrap().to_iso_string(), "PT0.123456789S");
869		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
870		assert_eq!(Duration::from_nanoseconds(999999999).unwrap().to_iso_string(), "PT0.999999999S");
871		assert_eq!(Duration::from_nanoseconds(1500000000).unwrap().to_iso_string(), "PT1.5S");
872	}
873
874	#[test]
875	fn test_duration_iso_string_fractional_seconds() {
876		let d = Duration::new(0, 0, 1_000_000_000 + 500 * 1_000_000).unwrap();
877		assert_eq!(d.to_iso_string(), "PT1.5S");
878
879		let d = Duration::new(0, 0, 2 * 1_000_000_000 + 123456 * 1_000).unwrap();
880		assert_eq!(d.to_iso_string(), "PT2.123456S");
881
882		let d = Duration::new(0, 0, 3 * 1_000_000_000 + 123456789).unwrap();
883		assert_eq!(d.to_iso_string(), "PT3.123456789S");
884	}
885
886	#[test]
887	fn test_duration_iso_string_complex() {
888		let d = Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000 + 123 * 1_000_000).unwrap();
889		assert_eq!(d.to_iso_string(), "P1DT2H30M45.123S");
890
891		let d = Duration::new(0, 7, (12 * 60 * 60 + 45 * 60 + 30) * 1_000_000_000 + 456789 * 1_000).unwrap();
892		assert_eq!(d.to_iso_string(), "P7DT12H45M30.456789S");
893	}
894
895	#[test]
896	fn test_duration_iso_string_trailing_zeros() {
897		assert_eq!(Duration::from_nanoseconds(100000000).unwrap().to_iso_string(), "PT0.1S");
898		assert_eq!(Duration::from_nanoseconds(120000000).unwrap().to_iso_string(), "PT0.12S");
899		assert_eq!(Duration::from_nanoseconds(123000000).unwrap().to_iso_string(), "PT0.123S");
900		assert_eq!(Duration::from_nanoseconds(123400000).unwrap().to_iso_string(), "PT0.1234S");
901		assert_eq!(Duration::from_nanoseconds(123450000).unwrap().to_iso_string(), "PT0.12345S");
902		assert_eq!(Duration::from_nanoseconds(123456000).unwrap().to_iso_string(), "PT0.123456S");
903		assert_eq!(Duration::from_nanoseconds(123456700).unwrap().to_iso_string(), "PT0.1234567S");
904		assert_eq!(Duration::from_nanoseconds(123456780).unwrap().to_iso_string(), "PT0.12345678S");
905		assert_eq!(Duration::from_nanoseconds(123456789).unwrap().to_iso_string(), "PT0.123456789S");
906	}
907
908	#[test]
909	fn test_duration_iso_string_negative() {
910		assert_eq!(Duration::from_seconds(-30).unwrap().to_iso_string(), "PT-30S");
911		assert_eq!(Duration::from_minutes(-5).unwrap().to_iso_string(), "PT-5M");
912		assert_eq!(Duration::from_hours(-2).unwrap().to_iso_string(), "PT-2H");
913		assert_eq!(Duration::from_days(-1).unwrap().to_iso_string(), "P-1D");
914	}
915
916	#[test]
917	fn test_duration_iso_string_large() {
918		assert_eq!(Duration::from_days(1000).unwrap().to_iso_string(), "P1000D");
919		assert_eq!(Duration::from_hours(25).unwrap().to_iso_string(), "P1DT1H");
920		assert_eq!(Duration::from_minutes(1500).unwrap().to_iso_string(), "P1DT1H");
921		assert_eq!(Duration::from_seconds(90000).unwrap().to_iso_string(), "P1DT1H");
922	}
923
924	#[test]
925	fn test_duration_iso_string_edge_cases() {
926		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
927		assert_eq!(Duration::from_nanoseconds(999999999).unwrap().to_iso_string(), "PT0.999999999S");
928		assert_eq!(Duration::from_nanoseconds(1000000000).unwrap().to_iso_string(), "PT1S");
929		assert_eq!(Duration::from_nanoseconds(60 * 1000000000).unwrap().to_iso_string(), "PT1M");
930		assert_eq!(Duration::from_nanoseconds(3600 * 1000000000).unwrap().to_iso_string(), "PT1H");
931		assert_eq!(Duration::from_nanoseconds(86400 * 1000000000).unwrap().to_iso_string(), "P1D");
932	}
933
934	#[test]
935	fn test_duration_iso_string_precision() {
936		assert_eq!(Duration::from_nanoseconds(100).unwrap().to_iso_string(), "PT0.0000001S");
937		assert_eq!(Duration::from_nanoseconds(10).unwrap().to_iso_string(), "PT0.00000001S");
938		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
939	}
940
941	#[test]
942	fn test_duration_display_zero() {
943		assert_eq!(format!("{}", Duration::zero()), "0s");
944		assert_eq!(format!("{}", Duration::from_seconds(0).unwrap()), "0s");
945		assert_eq!(format!("{}", Duration::from_nanoseconds(0).unwrap()), "0s");
946		assert_eq!(format!("{}", Duration::default()), "0s");
947	}
948
949	#[test]
950	fn test_duration_display_seconds_only() {
951		assert_eq!(format!("{}", Duration::from_seconds(1).unwrap()), "1s");
952		assert_eq!(format!("{}", Duration::from_seconds(30).unwrap()), "30s");
953		assert_eq!(format!("{}", Duration::from_seconds(59).unwrap()), "59s");
954	}
955
956	#[test]
957	fn test_duration_display_minutes_only() {
958		assert_eq!(format!("{}", Duration::from_minutes(1).unwrap()), "1m");
959		assert_eq!(format!("{}", Duration::from_minutes(30).unwrap()), "30m");
960		assert_eq!(format!("{}", Duration::from_minutes(59).unwrap()), "59m");
961	}
962
963	#[test]
964	fn test_duration_display_hours_only() {
965		assert_eq!(format!("{}", Duration::from_hours(1).unwrap()), "1h");
966		assert_eq!(format!("{}", Duration::from_hours(12).unwrap()), "12h");
967		assert_eq!(format!("{}", Duration::from_hours(23).unwrap()), "23h");
968	}
969
970	#[test]
971	fn test_duration_display_days_only() {
972		assert_eq!(format!("{}", Duration::from_days(1).unwrap()), "1d");
973		assert_eq!(format!("{}", Duration::from_days(7).unwrap()), "7d");
974		assert_eq!(format!("{}", Duration::from_days(365).unwrap()), "365d");
975	}
976
977	#[test]
978	fn test_duration_display_weeks_only() {
979		assert_eq!(format!("{}", Duration::from_weeks(1).unwrap()), "7d");
980		assert_eq!(format!("{}", Duration::from_weeks(2).unwrap()), "14d");
981		assert_eq!(format!("{}", Duration::from_weeks(52).unwrap()), "364d");
982	}
983
984	#[test]
985	fn test_duration_display_months_only() {
986		assert_eq!(format!("{}", Duration::from_months(1).unwrap()), "1mo");
987		assert_eq!(format!("{}", Duration::from_months(6).unwrap()), "6mo");
988		assert_eq!(format!("{}", Duration::from_months(11).unwrap()), "11mo");
989	}
990
991	#[test]
992	fn test_duration_display_years_only() {
993		assert_eq!(format!("{}", Duration::from_years(1).unwrap()), "1y");
994		assert_eq!(format!("{}", Duration::from_years(10).unwrap()), "10y");
995		assert_eq!(format!("{}", Duration::from_years(100).unwrap()), "100y");
996	}
997
998	#[test]
999	fn test_duration_display_combined_time() {
1000		let d = Duration::new(0, 0, (60 * 60 + 30 * 60) * 1_000_000_000).unwrap();
1001		assert_eq!(format!("{}", d), "1h30m");
1002
1003		let d = Duration::new(0, 0, (5 * 60 + 45) * 1_000_000_000).unwrap();
1004		assert_eq!(format!("{}", d), "5m45s");
1005
1006		let d = Duration::new(0, 0, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap();
1007		assert_eq!(format!("{}", d), "2h30m45s");
1008	}
1009
1010	#[test]
1011	fn test_duration_display_combined_date_time() {
1012		assert_eq!(format!("{}", Duration::new(0, 1, 2 * 60 * 60 * 1_000_000_000).unwrap()), "1d2h");
1013		assert_eq!(format!("{}", Duration::new(0, 1, 30 * 60 * 1_000_000_000).unwrap()), "1d30m");
1014		assert_eq!(
1015			format!("{}", Duration::new(0, 1, (2 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap()),
1016			"1d2h30m"
1017		);
1018		assert_eq!(
1019			format!("{}", Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap()),
1020			"1d2h30m45s"
1021		);
1022	}
1023
1024	#[test]
1025	fn test_duration_display_years_months() {
1026		assert_eq!(format!("{}", Duration::new(13, 0, 0).unwrap()), "1y1mo");
1027		assert_eq!(format!("{}", Duration::new(27, 0, 0).unwrap()), "2y3mo");
1028	}
1029
1030	#[test]
1031	fn test_duration_display_full_components() {
1032		let nanos = (4 * 60 * 60 + 5 * 60 + 6) * 1_000_000_000i64;
1033		assert_eq!(format!("{}", Duration::new(14, 3, nanos).unwrap()), "1y2mo3d4h5m6s");
1034	}
1035
1036	#[test]
1037	fn test_duration_display_milliseconds() {
1038		assert_eq!(format!("{}", Duration::from_milliseconds(123).unwrap()), "123ms");
1039		assert_eq!(format!("{}", Duration::from_milliseconds(1).unwrap()), "1ms");
1040		assert_eq!(format!("{}", Duration::from_milliseconds(999).unwrap()), "999ms");
1041		assert_eq!(format!("{}", Duration::from_milliseconds(1500).unwrap()), "1s500ms");
1042	}
1043
1044	#[test]
1045	fn test_duration_display_microseconds() {
1046		assert_eq!(format!("{}", Duration::from_microseconds(123456).unwrap()), "123ms456us");
1047		assert_eq!(format!("{}", Duration::from_microseconds(1).unwrap()), "1us");
1048		assert_eq!(format!("{}", Duration::from_microseconds(999999).unwrap()), "999ms999us");
1049		assert_eq!(format!("{}", Duration::from_microseconds(1500000).unwrap()), "1s500ms");
1050	}
1051
1052	#[test]
1053	fn test_duration_display_nanoseconds() {
1054		assert_eq!(format!("{}", Duration::from_nanoseconds(123456789).unwrap()), "123ms456us789ns");
1055		assert_eq!(format!("{}", Duration::from_nanoseconds(1).unwrap()), "1ns");
1056		assert_eq!(format!("{}", Duration::from_nanoseconds(999999999).unwrap()), "999ms999us999ns");
1057		assert_eq!(format!("{}", Duration::from_nanoseconds(1500000000).unwrap()), "1s500ms");
1058	}
1059
1060	#[test]
1061	fn test_duration_display_sub_second_decomposition() {
1062		let d = Duration::new(0, 0, 1_000_000_000 + 500 * 1_000_000).unwrap();
1063		assert_eq!(format!("{}", d), "1s500ms");
1064
1065		let d = Duration::new(0, 0, 2 * 1_000_000_000 + 123456 * 1_000).unwrap();
1066		assert_eq!(format!("{}", d), "2s123ms456us");
1067
1068		let d = Duration::new(0, 0, 3 * 1_000_000_000 + 123456789).unwrap();
1069		assert_eq!(format!("{}", d), "3s123ms456us789ns");
1070	}
1071
1072	#[test]
1073	fn test_duration_display_complex() {
1074		let d = Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000 + 123 * 1_000_000).unwrap();
1075		assert_eq!(format!("{}", d), "1d2h30m45s123ms");
1076
1077		let d = Duration::new(0, 7, (12 * 60 * 60 + 45 * 60 + 30) * 1_000_000_000 + 456789 * 1_000).unwrap();
1078		assert_eq!(format!("{}", d), "7d12h45m30s456ms789us");
1079	}
1080
1081	#[test]
1082	fn test_duration_display_sub_second_only() {
1083		assert_eq!(format!("{}", Duration::from_nanoseconds(100000000).unwrap()), "100ms");
1084		assert_eq!(format!("{}", Duration::from_nanoseconds(120000000).unwrap()), "120ms");
1085		assert_eq!(format!("{}", Duration::from_nanoseconds(123000000).unwrap()), "123ms");
1086		assert_eq!(format!("{}", Duration::from_nanoseconds(100).unwrap()), "100ns");
1087		assert_eq!(format!("{}", Duration::from_nanoseconds(10).unwrap()), "10ns");
1088		assert_eq!(format!("{}", Duration::from_nanoseconds(1000).unwrap()), "1us");
1089	}
1090
1091	#[test]
1092	fn test_duration_display_negative() {
1093		assert_eq!(format!("{}", Duration::from_seconds(-30).unwrap()), "-30s");
1094		assert_eq!(format!("{}", Duration::from_minutes(-5).unwrap()), "-5m");
1095		assert_eq!(format!("{}", Duration::from_hours(-2).unwrap()), "-2h");
1096		assert_eq!(format!("{}", Duration::from_days(-1).unwrap()), "-1d");
1097	}
1098
1099	#[test]
1100	fn test_duration_display_negative_sub_second() {
1101		assert_eq!(format!("{}", Duration::from_milliseconds(-500).unwrap()), "-500ms");
1102		assert_eq!(format!("{}", Duration::from_microseconds(-100).unwrap()), "-100us");
1103		assert_eq!(format!("{}", Duration::from_nanoseconds(-50).unwrap()), "-50ns");
1104	}
1105
1106	#[test]
1107	fn test_duration_display_large() {
1108		assert_eq!(format!("{}", Duration::from_days(1000).unwrap()), "1000d");
1109		assert_eq!(format!("{}", Duration::from_hours(25).unwrap()), "1d1h");
1110		assert_eq!(format!("{}", Duration::from_minutes(1500).unwrap()), "1d1h");
1111		assert_eq!(format!("{}", Duration::from_seconds(90000).unwrap()), "1d1h");
1112	}
1113
1114	#[test]
1115	fn test_duration_display_edge_cases() {
1116		assert_eq!(format!("{}", Duration::from_nanoseconds(1).unwrap()), "1ns");
1117		assert_eq!(format!("{}", Duration::from_nanoseconds(999999999).unwrap()), "999ms999us999ns");
1118		assert_eq!(format!("{}", Duration::from_nanoseconds(1000000000).unwrap()), "1s");
1119		assert_eq!(format!("{}", Duration::from_nanoseconds(60 * 1000000000).unwrap()), "1m");
1120		assert_eq!(format!("{}", Duration::from_nanoseconds(3600 * 1000000000).unwrap()), "1h");
1121		assert_eq!(format!("{}", Duration::from_nanoseconds(86400 * 1000000000).unwrap()), "1d");
1122	}
1123
1124	#[test]
1125	fn test_duration_display_abs_and_negate() {
1126		let d = Duration::from_seconds(-30).unwrap();
1127		assert_eq!(format!("{}", d.abs().unwrap()), "30s");
1128
1129		let d = Duration::from_seconds(30).unwrap();
1130		assert_eq!(format!("{}", d.negate().unwrap()), "-30s");
1131	}
1132
1133	#[test]
1134	fn test_nanos_normalize_to_days() {
1135		let d = Duration::new(0, 0, 86_400_000_000_000).unwrap();
1136		assert_eq!(d.get_days(), 1);
1137		assert_eq!(d.get_nanos(), 0);
1138	}
1139
1140	#[test]
1141	fn test_nanos_normalize_to_days_with_remainder() {
1142		let d = Duration::new(0, 0, 86_400_000_000_000 + 1_000_000_000).unwrap();
1143		assert_eq!(d.get_days(), 1);
1144		assert_eq!(d.get_nanos(), 1_000_000_000);
1145	}
1146
1147	#[test]
1148	fn test_nanos_normalize_negative() {
1149		let d = Duration::new(0, 0, -86_400_000_000_000).unwrap();
1150		assert_eq!(d.get_days(), -1);
1151		assert_eq!(d.get_nanos(), 0);
1152	}
1153
1154	#[test]
1155	fn test_normalized_equality() {
1156		let d1 = Duration::new(0, 0, 86_400_000_000_000).unwrap();
1157		let d2 = Duration::new(0, 1, 0).unwrap();
1158		assert_eq!(d1, d2);
1159	}
1160
1161	#[test]
1162	fn test_normalized_ordering() {
1163		let d1 = Duration::new(0, 0, 86_400_000_000_000 + 1).unwrap();
1164		let d2 = Duration::new(0, 1, 0).unwrap();
1165		assert!(d1 > d2);
1166	}
1167
1168	#[test]
1169	fn test_mixed_sign_months_days_allowed() {
1170		// Months are variable-length so they may disagree in sign with days and nanos, which
1171		// are commensurable with each other and may not.
1172		let d = Duration::new(1, -15, 0).unwrap();
1173		assert_eq!(d.get_months(), 1);
1174		assert_eq!(d.get_days(), -15);
1175	}
1176
1177	#[test]
1178	fn test_mixed_sign_months_nanos_allowed() {
1179		let d = Duration::new(-1, 0, 1_000_000_000).unwrap();
1180		assert_eq!(d.get_months(), -1);
1181		assert_eq!(d.get_nanos(), 1_000_000_000);
1182	}
1183
1184	#[test]
1185	fn test_mixed_sign_days_positive_nanos_negative() {
1186		assert_mixed_sign(Duration::new(0, 1, -1), 1, -1);
1187	}
1188
1189	#[test]
1190	fn test_mixed_sign_days_negative_nanos_positive() {
1191		assert_mixed_sign(Duration::new(0, -1, 1), -1, 1);
1192	}
1193
1194	#[test]
1195	fn test_is_positive_negative_mutually_exclusive() {
1196		let durations = [
1197			Duration::new(1, 0, 0).unwrap(),
1198			Duration::new(0, 1, 0).unwrap(),
1199			Duration::new(0, 0, 1).unwrap(),
1200			Duration::new(-1, 0, 0).unwrap(),
1201			Duration::new(0, -1, 0).unwrap(),
1202			Duration::new(0, 0, -1).unwrap(),
1203			Duration::new(1, 1, 1).unwrap(),
1204			Duration::new(-1, -1, -1).unwrap(),
1205			Duration::new(1, -15, 0).unwrap(), // mixed months/days
1206			Duration::new(-1, 15, 0).unwrap(), // mixed months/days
1207			Duration::zero(),
1208		];
1209		for d in durations {
1210			assert!(
1211				!(d.is_positive() && d.is_negative()),
1212				"Duration {:?} is both positive and negative",
1213				d
1214			);
1215		}
1216	}
1217
1218	#[test]
1219	fn test_mixed_months_days_is_neither_positive_nor_negative() {
1220		let d = Duration::new(1, -15, 0).unwrap();
1221		assert!(!d.is_positive());
1222		assert!(!d.is_negative());
1223	}
1224
1225	#[test]
1226	fn test_from_days_overflow() {
1227		assert_overflow(Duration::from_days(i32::MAX as i64 + 1));
1228	}
1229
1230	#[test]
1231	fn test_months_positive_days_negative_ok() {
1232		let d = Duration::new(1, -15, 0).unwrap();
1233		assert_eq!(d.get_months(), 1);
1234		assert_eq!(d.get_days(), -15);
1235		assert_eq!(d.get_nanos(), 0);
1236	}
1237
1238	#[test]
1239	fn test_months_negative_days_positive_ok() {
1240		let d = Duration::new(-1, 15, 0).unwrap();
1241		assert_eq!(d.get_months(), -1);
1242		assert_eq!(d.get_days(), 15);
1243	}
1244
1245	#[test]
1246	fn test_months_positive_nanos_negative_ok() {
1247		let d = Duration::new(1, 0, -1_000_000_000).unwrap();
1248		assert_eq!(d.get_months(), 1);
1249		assert_eq!(d.get_nanos(), -1_000_000_000);
1250	}
1251
1252	#[test]
1253	fn test_months_negative_nanos_positive_ok() {
1254		let d = Duration::new(-1, 0, 1_000_000_000).unwrap();
1255		assert_eq!(d.get_months(), -1);
1256		assert_eq!(d.get_nanos(), 1_000_000_000);
1257	}
1258
1259	#[test]
1260	fn test_months_positive_days_negative_nanos_negative_ok() {
1261		let d = Duration::new(2, -3, -1_000_000_000).unwrap();
1262		assert_eq!(d.get_months(), 2);
1263		assert_eq!(d.get_days(), -3);
1264		assert_eq!(d.get_nanos(), -1_000_000_000);
1265	}
1266
1267	#[test]
1268	fn test_months_negative_days_positive_nanos_positive_ok() {
1269		let d = Duration::new(-2, 3, 1_000_000_000).unwrap();
1270		assert_eq!(d.get_months(), -2);
1271		assert_eq!(d.get_days(), 3);
1272		assert_eq!(d.get_nanos(), 1_000_000_000);
1273	}
1274
1275	#[test]
1276	fn test_days_positive_nanos_negative_with_months_err() {
1277		assert_mixed_sign(Duration::new(5, 1, -1), 1, -1);
1278	}
1279
1280	#[test]
1281	fn test_days_negative_nanos_positive_with_months_err() {
1282		assert_mixed_sign(Duration::new(-5, -1, 1), -1, 1);
1283	}
1284
1285	#[test]
1286	fn test_nanos_normalization_causes_days_nanos_mixed_sign_err() {
1287		// Normalization itself can produce the mixed sign, not just the raw input.
1288		assert_mixed_sign(Duration::new(0, -3, 2 * 86_400_000_000_000 + 1), -1, 1);
1289	}
1290
1291	#[test]
1292	fn test_positive_months_negative_days_is_neither() {
1293		let d = Duration::new(1, -15, 0).unwrap();
1294		assert!(!d.is_positive());
1295		assert!(!d.is_negative());
1296	}
1297
1298	#[test]
1299	fn test_negative_months_positive_days_is_neither() {
1300		let d = Duration::new(-1, 15, 0).unwrap();
1301		assert!(!d.is_positive());
1302		assert!(!d.is_negative());
1303	}
1304
1305	#[test]
1306	fn test_positive_months_negative_days_negative_nanos_is_neither() {
1307		let d = Duration::new(2, -3, -1_000_000_000).unwrap();
1308		assert!(!d.is_positive());
1309		assert!(!d.is_negative());
1310	}
1311
1312	#[test]
1313	fn test_all_positive_is_positive() {
1314		let d = Duration::new(1, 2, 3).unwrap();
1315		assert!(d.is_positive());
1316		assert!(!d.is_negative());
1317	}
1318
1319	#[test]
1320	fn test_all_negative_is_negative() {
1321		let d = Duration::new(-1, -2, -3).unwrap();
1322		assert!(!d.is_positive());
1323		assert!(d.is_negative());
1324	}
1325
1326	#[test]
1327	fn test_zero_is_neither_positive_nor_negative() {
1328		assert!(!Duration::zero().is_positive());
1329		assert!(!Duration::zero().is_negative());
1330	}
1331
1332	#[test]
1333	fn test_only_months_positive() {
1334		let d = Duration::new(1, 0, 0).unwrap();
1335		assert!(d.is_positive());
1336	}
1337
1338	#[test]
1339	fn test_only_days_negative() {
1340		let d = Duration::new(0, -1, 0).unwrap();
1341		assert!(d.is_negative());
1342	}
1343
1344	#[test]
1345	fn test_normalization_nanos_into_negative_days() {
1346		let d = Duration::new(-5, 0, -2 * 86_400_000_000_000).unwrap();
1347		assert_eq!(d.get_months(), -5);
1348		assert_eq!(d.get_days(), -2);
1349		assert_eq!(d.get_nanos(), 0);
1350	}
1351
1352	#[test]
1353	fn test_normalization_nanos_into_days_with_mixed_months() {
1354		let d = Duration::new(3, 1, 86_400_000_000_000 + 500_000_000).unwrap();
1355		assert_eq!(d.get_months(), 3);
1356		assert_eq!(d.get_days(), 2);
1357		assert_eq!(d.get_nanos(), 500_000_000);
1358	}
1359
1360	#[test]
1361	fn test_try_sub_month_minus_days() {
1362		let a = Duration::new(1, 0, 0).unwrap();
1363		let b = Duration::new(0, 15, 0).unwrap();
1364		let result = a.try_sub(b).unwrap();
1365		assert_eq!(result.get_months(), 1);
1366		assert_eq!(result.get_days(), -15);
1367	}
1368
1369	#[test]
1370	fn test_try_sub_day_minus_month() {
1371		let a = Duration::new(0, 1, 0).unwrap();
1372		let b = Duration::new(1, 0, 0).unwrap();
1373		let result = a.try_sub(b).unwrap();
1374		assert_eq!(result.get_months(), -1);
1375		assert_eq!(result.get_days(), 1);
1376	}
1377
1378	#[test]
1379	fn test_try_add_mixed_months_days() {
1380		let a = Duration::new(2, -10, 0).unwrap();
1381		let b = Duration::new(-1, -5, 0).unwrap();
1382		let result = a.try_add(b).unwrap();
1383		assert_eq!(result.get_months(), 1);
1384		assert_eq!(result.get_days(), -15);
1385	}
1386
1387	#[test]
1388	fn test_try_sub_days_nanos_mixed_sign_err() {
1389		// Subtraction can borrow across the day boundary into a days/nanos sign mismatch.
1390		let a = Duration::new(0, 1, 0).unwrap();
1391		let b = Duration::new(0, 0, 1).unwrap();
1392		assert_mixed_sign(a.try_sub(b), 1, -1);
1393	}
1394
1395	#[test]
1396	fn test_try_mul_preserves_mixed_months() {
1397		let d = Duration::new(1, -3, 0).unwrap();
1398		let result = d.try_mul(2).unwrap();
1399		assert_eq!(result.get_months(), 2);
1400		assert_eq!(result.get_days(), -6);
1401	}
1402
1403	#[test]
1404	fn test_from_days_underflow() {
1405		assert_overflow(Duration::from_days(i32::MIN as i64 - 1));
1406	}
1407
1408	#[test]
1409	fn test_from_months_overflow() {
1410		assert_overflow(Duration::from_months(i32::MAX as i64 + 1));
1411	}
1412
1413	#[test]
1414	fn test_from_years_overflow() {
1415		assert_overflow(Duration::from_years(i32::MAX as i64 / 12 + 1));
1416	}
1417
1418	#[test]
1419	fn test_from_weeks_overflow() {
1420		assert_overflow(Duration::from_weeks(i32::MAX as i64 / 7 + 1));
1421	}
1422
1423	#[test]
1424	fn test_mul_months_truncation() {
1425		let d = Duration::from_months(1).unwrap();
1426		assert_overflow(d.try_mul(i32::MAX as i64 + 1));
1427	}
1428
1429	#[test]
1430	fn test_mul_days_truncation() {
1431		let d = Duration::from_days(1).unwrap();
1432		assert_overflow(d.try_mul(i32::MAX as i64 + 1));
1433	}
1434
1435	fn assert_total_overflow(result: Result<i64, Box<TypeError>>) {
1436		let err = result.expect_err("expected DurationOverflow error");
1437		match *err {
1438			TypeError::Temporal {
1439				kind: TemporalKind::DurationOverflow {
1440					..
1441				},
1442				..
1443			} => {}
1444			other => panic!("expected DurationOverflow, got: {:?}", other),
1445		}
1446	}
1447
1448	#[test]
1449	fn test_total_seconds_roundtrips_across_day_boundary() {
1450		// Construction normalizes whole days out of the nanos field, so a unit total that
1451		// ignores the days field reports only the sub-day remainder.
1452		let d = Duration::from_seconds(90_000).unwrap();
1453		assert_eq!(d.get_days(), 1);
1454		assert_eq!(d.seconds().unwrap(), 90_000);
1455	}
1456
1457	#[test]
1458	fn test_total_milliseconds_roundtrips_across_day_boundary() {
1459		let d = Duration::from_milliseconds(90_000_000).unwrap();
1460		assert_eq!(d.get_days(), 1);
1461		assert_eq!(d.milliseconds().unwrap(), 90_000_000);
1462	}
1463
1464	#[test]
1465	fn test_total_microseconds_roundtrips_across_day_boundary() {
1466		let d = Duration::from_microseconds(90_000_000_000).unwrap();
1467		assert_eq!(d.get_days(), 1);
1468		assert_eq!(d.microseconds().unwrap(), 90_000_000_000);
1469	}
1470
1471	#[test]
1472	fn test_total_nanoseconds_roundtrips_across_day_boundary() {
1473		let d = Duration::from_nanoseconds(90_000_000_000_000).unwrap();
1474		assert_eq!(d.get_days(), 1);
1475		assert_eq!(d.nanoseconds().unwrap(), 90_000_000_000_000);
1476		assert_eq!(d.as_nanos().unwrap(), 90_000_000_000_000);
1477	}
1478
1479	#[test]
1480	fn test_total_from_minutes_crossing_day() {
1481		let d = Duration::from_minutes(1_500).unwrap();
1482		assert_eq!(d.get_days(), 1);
1483		assert_eq!(d.seconds().unwrap(), 90_000);
1484	}
1485
1486	#[test]
1487	fn test_total_from_hours_crossing_day() {
1488		let d = Duration::from_hours(25).unwrap();
1489		assert_eq!(d.get_days(), 1);
1490		assert_eq!(d.seconds().unwrap(), 90_000);
1491	}
1492
1493	#[test]
1494	fn test_total_from_days_counts_days() {
1495		let d = Duration::from_days(3).unwrap();
1496		assert_eq!(d.seconds().unwrap(), 3 * 86_400);
1497		assert_eq!(d.nanoseconds().unwrap(), 3 * NANOS_PER_DAY);
1498	}
1499
1500	#[test]
1501	fn test_total_from_weeks_counts_days() {
1502		let d = Duration::from_weeks(2).unwrap();
1503		assert_eq!(d.get_days(), 14);
1504		assert_eq!(d.seconds().unwrap(), 14 * 86_400);
1505	}
1506
1507	#[test]
1508	fn test_total_from_months_uses_thirty_days() {
1509		// Residual months (< 12) count as 30 days each.
1510		let d = Duration::from_months(5).unwrap();
1511		assert_eq!(d.get_months(), 5);
1512		assert_eq!(d.seconds().unwrap(), 5 * 30 * 86_400);
1513	}
1514
1515	#[test]
1516	fn test_total_from_years_uses_three_six_five_quarter_days() {
1517		// Whole years count as 365.25 days each (the Postgres EXTRACT(EPOCH) convention), so
1518		// one year is deliberately not equal to 365 days.
1519		let one_year = Duration::from_years(1).unwrap();
1520		assert_eq!(one_year.get_months(), 12);
1521		assert_eq!(one_year.seconds().unwrap(), 31_557_600);
1522
1523		let three_sixty_five_days = Duration::from_days(365).unwrap();
1524		assert_eq!(three_sixty_five_days.seconds().unwrap(), 31_536_000);
1525		assert_ne!(one_year.seconds().unwrap(), three_sixty_five_days.seconds().unwrap());
1526
1527		assert_eq!(Duration::from_years(2).unwrap().seconds().unwrap(), 2 * 31_557_600);
1528	}
1529
1530	#[test]
1531	fn test_total_months_split_into_years_and_residual() {
1532		// Whole years convert at 365.25 days, residual months at 30.
1533		let d = Duration::from_months(13).unwrap();
1534		assert_eq!(d.seconds().unwrap(), 31_557_600 + 30 * 86_400);
1535	}
1536
1537	#[test]
1538	fn test_total_new_combined_mixed_sign() {
1539		// Months and days may carry opposite signs, so the total must accumulate them signed
1540		// rather than take a magnitude.
1541		let d = Duration::new(1, -5, 0).unwrap();
1542		assert_eq!(d.seconds().unwrap(), 30 * 86_400 - 5 * 86_400);
1543	}
1544
1545	#[test]
1546	fn test_total_from_micros_infallible_roundtrips() {
1547		let micros: u64 = 90_000_000_000;
1548		let d = Duration::from_micros_infallible(micros);
1549		assert_eq!(d.get_days(), 1);
1550		assert_eq!(d.microseconds().unwrap(), micros as i64);
1551	}
1552
1553	#[test]
1554	fn test_total_from_nanos_infallible_roundtrips() {
1555		// Spans arrive from monotonic counters on paths that must not panic, so the infallible
1556		// constructor has to round-trip rather than reject.
1557		let nanos: u64 = 7 * 86_400_000_000_000 + 1_500_000_000;
1558		let d = Duration::from_nanos_infallible(nanos);
1559		assert_eq!(d.get_days(), 7);
1560		assert_eq!(d.nanoseconds().unwrap(), nanos as i64);
1561	}
1562
1563	#[test]
1564	fn test_from_nanos_infallible_clamps_instead_of_wrapping() {
1565		// Wrapping would produce a negative span, which reads as a duration in the past.
1566		let d = Duration::from_nanos_infallible(u64::MAX);
1567		assert!(d.get_days() > 0, "an unrepresentable span must clamp to a positive duration");
1568	}
1569
1570	#[test]
1571	fn test_total_zero_is_zero_in_every_unit() {
1572		let d = Duration::zero();
1573		assert_eq!(d.seconds().unwrap(), 0);
1574		assert_eq!(d.milliseconds().unwrap(), 0);
1575		assert_eq!(d.microseconds().unwrap(), 0);
1576		assert_eq!(d.nanoseconds().unwrap(), 0);
1577		assert_eq!(d.as_nanos().unwrap(), 0);
1578	}
1579
1580	#[test]
1581	fn test_total_overflow_fails_loud_per_unit() {
1582		// A day count that overflows i64 nanoseconds still fits i64 seconds, so each unit is
1583		// checked independently and overflow surfaces as an error rather than wrapping.
1584		let d = Duration::new(0, i32::MAX, 0).unwrap();
1585		assert_eq!(d.seconds().unwrap(), i32::MAX as i64 * 86_400);
1586		assert_total_overflow(d.nanoseconds());
1587		assert_total_overflow(d.as_nanos());
1588	}
1589
1590	#[test]
1591	fn saturating_add_matches_try_add_in_range() {
1592		// Saturation must only engage at the i32/i64 boundaries, never in range.
1593		let a = Duration::new(1, 2, 3_000_000_000).unwrap();
1594		let b = Duration::new(0, 1, 1_000_000_000).unwrap();
1595		assert_eq!(a.saturating_add(b), a.try_add(b).unwrap());
1596		assert_eq!(a.saturating_sub(b), a.try_sub(b).unwrap());
1597	}
1598
1599	#[test]
1600	fn saturating_add_clamps_months_overflow() {
1601		let d = Duration::new(i32::MAX, 0, 0).unwrap().saturating_add(Duration::new(1000, 0, 0).unwrap());
1602		assert_eq!(d.get_months(), i32::MAX);
1603	}
1604
1605	#[test]
1606	fn saturating_add_clamps_days_overflow() {
1607		let d = Duration::new(0, i32::MAX, 0).unwrap().saturating_add(Duration::new(0, 1000, 0).unwrap());
1608		assert_eq!(d.get_days(), i32::MAX);
1609	}
1610
1611	#[test]
1612	fn saturating_sub_carries_mixed_sign() {
1613		// try_sub rejects this borrow as a mixed days/nanos sign; the saturating form must
1614		// instead carry it into one same-sign value.
1615		let d = Duration::new(0, 1, 0).unwrap().saturating_sub(Duration::new(0, 0, 1).unwrap());
1616		assert_eq!(d.get_days(), 0);
1617		assert_eq!(d.get_nanos(), 86_399_999_999_999);
1618	}
1619
1620	#[test]
1621	fn saturating_mul_scales_and_clamps() {
1622		assert_eq!(Duration::from_seconds(1).unwrap().saturating_mul(60), Duration::from_seconds(60).unwrap());
1623		assert_eq!(Duration::new(0, i32::MAX, 0).unwrap().saturating_mul(2).get_days(), i32::MAX);
1624	}
1625}
1626
1627#[cfg(test)]
1628mod conversion_tests {
1629	use std::str::FromStr;
1630
1631	use super::{Duration, StdDuration};
1632
1633	#[test]
1634	fn from_str_parses_human_units() {
1635		// Config values carry their unit in the literal, so the unit can never be misread.
1636		assert_eq!(Duration::from_str("10ms").unwrap().to_std(), StdDuration::from_millis(10));
1637		assert_eq!("5s".parse::<Duration>().unwrap().to_std(), StdDuration::from_secs(5));
1638		assert_eq!("2m".parse::<Duration>().unwrap().to_std(), StdDuration::from_secs(120));
1639	}
1640
1641	#[test]
1642	fn from_str_trims_whitespace() {
1643		assert_eq!("  400ms  ".parse::<Duration>().unwrap().to_std(), StdDuration::from_millis(400));
1644	}
1645
1646	#[test]
1647	fn from_str_rejects_bare_integer() {
1648		// A bare "5000" must fail loudly rather than be silently read as ms or s.
1649		assert!("5000".parse::<Duration>().is_err());
1650	}
1651
1652	#[test]
1653	fn from_str_rejects_garbage() {
1654		assert!("soon".parse::<Duration>().is_err());
1655		assert!("".parse::<Duration>().is_err());
1656	}
1657
1658	#[test]
1659	fn to_std_and_from_std_round_trip() {
1660		let std = StdDuration::from_millis(1234);
1661		assert_eq!(Duration::from_std(std).to_std(), std);
1662	}
1663
1664	#[test]
1665	fn to_std_saturates_negative_to_zero() {
1666		// Config durations are never negative; a negative one degrades to zero, not a panic.
1667		let negative = Duration::from_seconds(-5).unwrap();
1668		assert_eq!(negative.to_std(), StdDuration::ZERO);
1669	}
1670
1671	#[test]
1672	fn dividing_a_span_counts_whole_steps_and_drops_the_remainder() {
1673		// A partial slide must not count: rounding up files the row under a window start in its future.
1674		let second = Duration::from_seconds(1).unwrap();
1675
1676		assert_eq!(second.div_duration(Duration::from_milliseconds(250).unwrap()).unwrap(), 4);
1677		assert_eq!(second.div_duration(Duration::from_milliseconds(300).unwrap()).unwrap(), 3);
1678		assert_eq!(second.div_duration(Duration::from_seconds(2).unwrap()).unwrap(), 0);
1679	}
1680
1681	#[test]
1682	fn dividing_by_a_zero_span_is_refused_rather_than_panicking() {
1683		// RQL does not close every route to a zero slide, and dividing would panic the flow.
1684		let err = Duration::from_seconds(1).unwrap().div_duration(Duration::zero());
1685
1686		assert!(err.is_err(), "a zero divisor must be an error, not a division by zero");
1687	}
1688
1689	#[test]
1690	fn division_reads_the_whole_duration_not_just_its_nanosecond_field() {
1691		// Days and nanos are separate fields; reading nanos alone answers zero for a days dividend.
1692		let day = Duration::from_days(1).unwrap();
1693		let hour = Duration::from_hours(1).unwrap();
1694
1695		assert_eq!(day.div_duration(hour).unwrap(), 24);
1696		assert_eq!(day.get_nanos(), 0, "precondition: the dividend carries no nanoseconds of its own");
1697	}
1698}