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