Skip to main content

reifydb_value/value/
duration.rs

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