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