Skip to main content

reifydb_type/value/
duration.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 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/// A duration value representing a duration between two points in time.
18///
19/// All non-zero components must share the same sign. Nanos are normalized
20/// so that `|nanos| < NANOS_PER_DAY`, with excess rolling into `days`.
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Duration {
23	months: i32, // Store years*12 + months
24	days: i32,   // Separate days
25	nanos: i64,  // All time components as nanoseconds
26}
27
28const NANOS_PER_DAY: i64 = 86_400_000_000_000;
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		// Days and nanos must share the same sign (they are commensurable).
69		// Months may differ in sign from days/nanos (months are variable-length).
70		if (days > 0 && nanos < 0) || (days < 0 && nanos > 0) {
71			return Err(Box::new(Self::mixed_sign_err(days, nanos)));
72		}
73
74		Ok(Self {
75			months,
76			days,
77			nanos,
78		})
79	}
80
81	pub fn new(months: i32, days: i32, nanos: i64) -> Result<Self, Box<TypeError>> {
82		Self::normalized(months, days, nanos)
83	}
84
85	pub fn from_seconds(seconds: i64) -> Result<Self, Box<TypeError>> {
86		Self::normalized(0, 0, seconds * 1_000_000_000)
87	}
88
89	pub fn from_milliseconds(milliseconds: i64) -> Result<Self, Box<TypeError>> {
90		Self::normalized(0, 0, milliseconds * 1_000_000)
91	}
92
93	pub fn from_microseconds(microseconds: i64) -> Result<Self, Box<TypeError>> {
94		Self::normalized(0, 0, microseconds * 1_000)
95	}
96
97	pub fn from_nanoseconds(nanoseconds: i64) -> Result<Self, Box<TypeError>> {
98		Self::normalized(0, 0, nanoseconds)
99	}
100
101	pub fn from_minutes(minutes: i64) -> Result<Self, Box<TypeError>> {
102		Self::normalized(0, 0, minutes * 60 * 1_000_000_000)
103	}
104
105	pub fn from_hours(hours: i64) -> Result<Self, Box<TypeError>> {
106		Self::normalized(0, 0, hours * 60 * 60 * 1_000_000_000)
107	}
108
109	pub fn from_days(days: i64) -> Result<Self, Box<TypeError>> {
110		let days =
111			i32::try_from(days).map_err(|_| Box::new(Self::overflow_err("days value out of i32 range")))?;
112		Self::normalized(0, days, 0)
113	}
114
115	pub fn from_weeks(weeks: i64) -> Result<Self, Box<TypeError>> {
116		let days = weeks.checked_mul(7).ok_or_else(|| Box::new(Self::overflow_err("weeks overflow")))?;
117		let days =
118			i32::try_from(days).map_err(|_| Box::new(Self::overflow_err("days value out of i32 range")))?;
119		Self::normalized(0, days, 0)
120	}
121
122	pub fn from_months(months: i64) -> Result<Self, Box<TypeError>> {
123		let months = i32::try_from(months)
124			.map_err(|_| Box::new(Self::overflow_err("months value out of i32 range")))?;
125		Self::normalized(months, 0, 0)
126	}
127
128	pub fn from_years(years: i64) -> Result<Self, Box<TypeError>> {
129		let months = years.checked_mul(12).ok_or_else(|| Box::new(Self::overflow_err("years overflow")))?;
130		let months = i32::try_from(months)
131			.map_err(|_| Box::new(Self::overflow_err("months value out of i32 range")))?;
132		Self::normalized(months, 0, 0)
133	}
134
135	pub fn zero() -> Self {
136		Self {
137			months: 0,
138			days: 0,
139			nanos: 0,
140		}
141	}
142
143	pub fn seconds(&self) -> i64 {
144		self.nanos / 1_000_000_000
145	}
146
147	pub fn milliseconds(&self) -> i64 {
148		self.nanos / 1_000_000
149	}
150
151	pub fn microseconds(&self) -> i64 {
152		self.nanos / 1_000
153	}
154
155	pub fn nanoseconds(&self) -> i64 {
156		self.nanos
157	}
158
159	pub fn get_months(&self) -> i32 {
160		self.months
161	}
162
163	pub fn get_days(&self) -> i32 {
164		self.days
165	}
166
167	pub fn get_nanos(&self) -> i64 {
168		self.nanos
169	}
170
171	pub fn as_nanos(&self) -> i64 {
172		self.nanos
173	}
174
175	pub fn is_positive(&self) -> bool {
176		self.months >= 0
177			&& self.days >= 0 && self.nanos >= 0
178			&& (self.months > 0 || self.days > 0 || self.nanos > 0)
179	}
180
181	pub fn is_negative(&self) -> bool {
182		self.months <= 0
183			&& self.days <= 0 && self.nanos <= 0
184			&& (self.months < 0 || self.days < 0 || self.nanos < 0)
185	}
186
187	pub fn abs(&self) -> Self {
188		Self {
189			months: self.months.abs(),
190			days: self.days.abs(),
191			nanos: self.nanos.abs(),
192		}
193	}
194
195	pub fn negate(&self) -> Self {
196		Self {
197			months: -self.months,
198			days: -self.days,
199			nanos: -self.nanos,
200		}
201	}
202
203	/// Format as ISO 8601 duration string: `P[n]Y[n]M[n]DT[n]H[n]M[n.n]S`
204	pub fn to_iso_string(&self) -> String {
205		if self.months == 0 && self.days == 0 && self.nanos == 0 {
206			return "PT0S".to_string();
207		}
208
209		let mut result = String::from("P");
210
211		let years = self.months / 12;
212		let months = self.months % 12;
213
214		if years != 0 {
215			write!(result, "{}Y", years).unwrap();
216		}
217		if months != 0 {
218			write!(result, "{}M", months).unwrap();
219		}
220
221		let total_seconds = self.nanos / 1_000_000_000;
222		let remaining_nanos = self.nanos % 1_000_000_000;
223
224		let extra_days = total_seconds / 86400;
225		let remaining_seconds = total_seconds % 86400;
226
227		let display_days = self.days + extra_days as i32;
228		let hours = remaining_seconds / 3600;
229		let minutes = (remaining_seconds % 3600) / 60;
230		let seconds = remaining_seconds % 60;
231
232		if display_days != 0 {
233			write!(result, "{}D", display_days).unwrap();
234		}
235
236		if hours != 0 || minutes != 0 || seconds != 0 || remaining_nanos != 0 {
237			result.push('T');
238
239			if hours != 0 {
240				write!(result, "{}H", hours).unwrap();
241			}
242			if minutes != 0 {
243				write!(result, "{}M", minutes).unwrap();
244			}
245			if seconds != 0 || remaining_nanos != 0 {
246				if remaining_nanos != 0 {
247					let fractional = remaining_nanos as f64 / 1_000_000_000.0;
248					let total_seconds_f = seconds as f64 + fractional;
249					let formatted_str = format!("{:.9}", total_seconds_f);
250					let formatted = formatted_str.trim_end_matches('0').trim_end_matches('.');
251					write!(result, "{}S", formatted).unwrap();
252				} else {
253					write!(result, "{}S", seconds).unwrap();
254				}
255			}
256		}
257
258		result
259	}
260}
261
262impl PartialOrd for Duration {
263	fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
264		Some(self.cmp(other))
265	}
266}
267
268impl Ord for Duration {
269	fn cmp(&self, other: &Self) -> cmp::Ordering {
270		// Compare months first
271		match self.months.cmp(&other.months) {
272			cmp::Ordering::Equal => {
273				// Then days
274				match self.days.cmp(&other.days) {
275					cmp::Ordering::Equal => {
276						// Finally nanos
277						self.nanos.cmp(&other.nanos)
278					}
279					other_order => other_order,
280				}
281			}
282			other_order => other_order,
283		}
284	}
285}
286
287impl Duration {
288	pub fn try_add(self, rhs: Self) -> Result<Self, Box<TypeError>> {
289		let months = self
290			.months
291			.checked_add(rhs.months)
292			.ok_or_else(|| Box::new(Self::overflow_err("months overflow in add")))?;
293		let days = self
294			.days
295			.checked_add(rhs.days)
296			.ok_or_else(|| Box::new(Self::overflow_err("days overflow in add")))?;
297		let nanos = self
298			.nanos
299			.checked_add(rhs.nanos)
300			.ok_or_else(|| Box::new(Self::overflow_err("nanos overflow in add")))?;
301		Self::normalized(months, days, nanos)
302	}
303
304	pub fn try_sub(self, rhs: Self) -> Result<Self, Box<TypeError>> {
305		let months = self
306			.months
307			.checked_sub(rhs.months)
308			.ok_or_else(|| Box::new(Self::overflow_err("months overflow in sub")))?;
309		let days = self
310			.days
311			.checked_sub(rhs.days)
312			.ok_or_else(|| Box::new(Self::overflow_err("days overflow in sub")))?;
313		let nanos = self
314			.nanos
315			.checked_sub(rhs.nanos)
316			.ok_or_else(|| Box::new(Self::overflow_err("nanos overflow in sub")))?;
317		Self::normalized(months, days, nanos)
318	}
319
320	pub fn try_mul(self, rhs: i64) -> Result<Self, Box<TypeError>> {
321		let rhs_i32 = i32::try_from(rhs)
322			.map_err(|_| Box::new(Self::overflow_err("multiplier out of i32 range for months/days")))?;
323		let months = self
324			.months
325			.checked_mul(rhs_i32)
326			.ok_or_else(|| Box::new(Self::overflow_err("months overflow in mul")))?;
327		let days = self
328			.days
329			.checked_mul(rhs_i32)
330			.ok_or_else(|| Box::new(Self::overflow_err("days overflow in mul")))?;
331		let nanos = self
332			.nanos
333			.checked_mul(rhs)
334			.ok_or_else(|| Box::new(Self::overflow_err("nanos overflow in mul")))?;
335		Self::normalized(months, days, nanos)
336	}
337}
338
339impl ops::Add for Duration {
340	type Output = Self;
341	fn add(self, rhs: Self) -> Self {
342		self.try_add(rhs).expect("duration add overflow")
343	}
344}
345
346impl ops::Sub for Duration {
347	type Output = Self;
348	fn sub(self, rhs: Self) -> Self {
349		self.try_sub(rhs).expect("duration sub overflow")
350	}
351}
352
353impl ops::Mul<i64> for Duration {
354	type Output = Self;
355	fn mul(self, rhs: i64) -> Self {
356		self.try_mul(rhs).expect("duration mul overflow")
357	}
358}
359
360impl Display for Duration {
361	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
362		if self.months == 0 && self.days == 0 && self.nanos == 0 {
363			return write!(f, "0s");
364		}
365
366		let years = self.months / 12;
367		let months = self.months % 12;
368
369		let total_seconds = self.nanos / 1_000_000_000;
370		let remaining_nanos = self.nanos % 1_000_000_000;
371
372		let extra_days = total_seconds / 86400;
373		let remaining_seconds = total_seconds % 86400;
374
375		let display_days = self.days + extra_days as i32;
376		let hours = remaining_seconds / 3600;
377		let minutes = (remaining_seconds % 3600) / 60;
378		let seconds = remaining_seconds % 60;
379
380		let abs_remaining = remaining_nanos.abs();
381		let ms = abs_remaining / 1_000_000;
382		let us = (abs_remaining % 1_000_000) / 1_000;
383		let ns = abs_remaining % 1_000;
384
385		if years != 0 {
386			write!(f, "{}y", years)?;
387		}
388		if months != 0 {
389			write!(f, "{}mo", months)?;
390		}
391		if display_days != 0 {
392			write!(f, "{}d", display_days)?;
393		}
394		if hours != 0 {
395			write!(f, "{}h", hours)?;
396		}
397		if minutes != 0 {
398			write!(f, "{}m", minutes)?;
399		}
400		if seconds != 0 {
401			write!(f, "{}s", seconds)?;
402		}
403
404		if ms != 0 || us != 0 || ns != 0 {
405			if remaining_nanos < 0
406				&& seconds == 0 && hours == 0
407				&& minutes == 0 && display_days == 0
408				&& years == 0 && months == 0
409			{
410				write!(f, "-")?;
411			}
412			if ms != 0 {
413				write!(f, "{}ms", ms)?;
414			}
415			if us != 0 {
416				write!(f, "{}us", us)?;
417			}
418			if ns != 0 {
419				write!(f, "{}ns", ns)?;
420			}
421		}
422
423		Ok(())
424	}
425}
426
427#[cfg(test)]
428pub mod tests {
429	use super::*;
430	use crate::error::TemporalKind;
431
432	fn assert_overflow(result: Result<Duration, Box<TypeError>>) {
433		let err = result.expect_err("expected DurationOverflow error");
434		match *err {
435			TypeError::Temporal {
436				kind: TemporalKind::DurationOverflow {
437					..
438				},
439				..
440			} => {}
441			other => panic!("expected DurationOverflow, got: {:?}", other),
442		}
443	}
444
445	fn assert_mixed_sign(result: Result<Duration, Box<TypeError>>, expected_days: i32, expected_nanos: i64) {
446		let err = result.expect_err("expected DurationMixedSign error");
447		match *err {
448			TypeError::Temporal {
449				kind: TemporalKind::DurationMixedSign {
450					days,
451					nanos,
452				},
453				..
454			} => {
455				assert_eq!(days, expected_days, "days mismatch");
456				assert_eq!(nanos, expected_nanos, "nanos mismatch");
457			}
458			other => panic!("expected DurationMixedSign, got: {:?}", other),
459		}
460	}
461
462	#[test]
463	fn test_duration_iso_string_zero() {
464		assert_eq!(Duration::zero().to_iso_string(), "PT0S");
465		assert_eq!(Duration::from_seconds(0).unwrap().to_iso_string(), "PT0S");
466		assert_eq!(Duration::from_nanoseconds(0).unwrap().to_iso_string(), "PT0S");
467		assert_eq!(Duration::default().to_iso_string(), "PT0S");
468	}
469
470	#[test]
471	fn test_duration_iso_string_seconds() {
472		assert_eq!(Duration::from_seconds(1).unwrap().to_iso_string(), "PT1S");
473		assert_eq!(Duration::from_seconds(30).unwrap().to_iso_string(), "PT30S");
474		assert_eq!(Duration::from_seconds(59).unwrap().to_iso_string(), "PT59S");
475	}
476
477	#[test]
478	fn test_duration_iso_string_minutes() {
479		assert_eq!(Duration::from_minutes(1).unwrap().to_iso_string(), "PT1M");
480		assert_eq!(Duration::from_minutes(30).unwrap().to_iso_string(), "PT30M");
481		assert_eq!(Duration::from_minutes(59).unwrap().to_iso_string(), "PT59M");
482	}
483
484	#[test]
485	fn test_duration_iso_string_hours() {
486		assert_eq!(Duration::from_hours(1).unwrap().to_iso_string(), "PT1H");
487		assert_eq!(Duration::from_hours(12).unwrap().to_iso_string(), "PT12H");
488		assert_eq!(Duration::from_hours(23).unwrap().to_iso_string(), "PT23H");
489	}
490
491	#[test]
492	fn test_duration_iso_string_days() {
493		assert_eq!(Duration::from_days(1).unwrap().to_iso_string(), "P1D");
494		assert_eq!(Duration::from_days(7).unwrap().to_iso_string(), "P7D");
495		assert_eq!(Duration::from_days(365).unwrap().to_iso_string(), "P365D");
496	}
497
498	#[test]
499	fn test_duration_iso_string_weeks() {
500		assert_eq!(Duration::from_weeks(1).unwrap().to_iso_string(), "P7D");
501		assert_eq!(Duration::from_weeks(2).unwrap().to_iso_string(), "P14D");
502		assert_eq!(Duration::from_weeks(52).unwrap().to_iso_string(), "P364D");
503	}
504
505	#[test]
506	fn test_duration_iso_string_combined_time() {
507		let d = Duration::new(0, 0, (1 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap();
508		assert_eq!(d.to_iso_string(), "PT1H30M");
509
510		let d = Duration::new(0, 0, (5 * 60 + 45) * 1_000_000_000).unwrap();
511		assert_eq!(d.to_iso_string(), "PT5M45S");
512
513		let d = Duration::new(0, 0, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap();
514		assert_eq!(d.to_iso_string(), "PT2H30M45S");
515	}
516
517	#[test]
518	fn test_duration_iso_string_combined_date_time() {
519		assert_eq!(Duration::new(0, 1, 2 * 60 * 60 * 1_000_000_000).unwrap().to_iso_string(), "P1DT2H");
520		assert_eq!(Duration::new(0, 1, 30 * 60 * 1_000_000_000).unwrap().to_iso_string(), "P1DT30M");
521		assert_eq!(
522			Duration::new(0, 1, (2 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap().to_iso_string(),
523			"P1DT2H30M"
524		);
525		assert_eq!(
526			Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap().to_iso_string(),
527			"P1DT2H30M45S"
528		);
529	}
530
531	#[test]
532	fn test_duration_iso_string_milliseconds() {
533		assert_eq!(Duration::from_milliseconds(123).unwrap().to_iso_string(), "PT0.123S");
534		assert_eq!(Duration::from_milliseconds(1).unwrap().to_iso_string(), "PT0.001S");
535		assert_eq!(Duration::from_milliseconds(999).unwrap().to_iso_string(), "PT0.999S");
536		assert_eq!(Duration::from_milliseconds(1500).unwrap().to_iso_string(), "PT1.5S");
537	}
538
539	#[test]
540	fn test_duration_iso_string_microseconds() {
541		assert_eq!(Duration::from_microseconds(123456).unwrap().to_iso_string(), "PT0.123456S");
542		assert_eq!(Duration::from_microseconds(1).unwrap().to_iso_string(), "PT0.000001S");
543		assert_eq!(Duration::from_microseconds(999999).unwrap().to_iso_string(), "PT0.999999S");
544		assert_eq!(Duration::from_microseconds(1500000).unwrap().to_iso_string(), "PT1.5S");
545	}
546
547	#[test]
548	fn test_duration_iso_string_nanoseconds() {
549		assert_eq!(Duration::from_nanoseconds(123456789).unwrap().to_iso_string(), "PT0.123456789S");
550		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
551		assert_eq!(Duration::from_nanoseconds(999999999).unwrap().to_iso_string(), "PT0.999999999S");
552		assert_eq!(Duration::from_nanoseconds(1500000000).unwrap().to_iso_string(), "PT1.5S");
553	}
554
555	#[test]
556	fn test_duration_iso_string_fractional_seconds() {
557		let d = Duration::new(0, 0, 1 * 1_000_000_000 + 500 * 1_000_000).unwrap();
558		assert_eq!(d.to_iso_string(), "PT1.5S");
559
560		let d = Duration::new(0, 0, 2 * 1_000_000_000 + 123456 * 1_000).unwrap();
561		assert_eq!(d.to_iso_string(), "PT2.123456S");
562
563		let d = Duration::new(0, 0, 3 * 1_000_000_000 + 123456789).unwrap();
564		assert_eq!(d.to_iso_string(), "PT3.123456789S");
565	}
566
567	#[test]
568	fn test_duration_iso_string_complex() {
569		let d = Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000 + 123 * 1_000_000).unwrap();
570		assert_eq!(d.to_iso_string(), "P1DT2H30M45.123S");
571
572		let d = Duration::new(0, 7, (12 * 60 * 60 + 45 * 60 + 30) * 1_000_000_000 + 456789 * 1_000).unwrap();
573		assert_eq!(d.to_iso_string(), "P7DT12H45M30.456789S");
574	}
575
576	#[test]
577	fn test_duration_iso_string_trailing_zeros() {
578		assert_eq!(Duration::from_nanoseconds(100000000).unwrap().to_iso_string(), "PT0.1S");
579		assert_eq!(Duration::from_nanoseconds(120000000).unwrap().to_iso_string(), "PT0.12S");
580		assert_eq!(Duration::from_nanoseconds(123000000).unwrap().to_iso_string(), "PT0.123S");
581		assert_eq!(Duration::from_nanoseconds(123400000).unwrap().to_iso_string(), "PT0.1234S");
582		assert_eq!(Duration::from_nanoseconds(123450000).unwrap().to_iso_string(), "PT0.12345S");
583		assert_eq!(Duration::from_nanoseconds(123456000).unwrap().to_iso_string(), "PT0.123456S");
584		assert_eq!(Duration::from_nanoseconds(123456700).unwrap().to_iso_string(), "PT0.1234567S");
585		assert_eq!(Duration::from_nanoseconds(123456780).unwrap().to_iso_string(), "PT0.12345678S");
586		assert_eq!(Duration::from_nanoseconds(123456789).unwrap().to_iso_string(), "PT0.123456789S");
587	}
588
589	#[test]
590	fn test_duration_iso_string_negative() {
591		assert_eq!(Duration::from_seconds(-30).unwrap().to_iso_string(), "PT-30S");
592		assert_eq!(Duration::from_minutes(-5).unwrap().to_iso_string(), "PT-5M");
593		assert_eq!(Duration::from_hours(-2).unwrap().to_iso_string(), "PT-2H");
594		assert_eq!(Duration::from_days(-1).unwrap().to_iso_string(), "P-1D");
595	}
596
597	#[test]
598	fn test_duration_iso_string_large() {
599		assert_eq!(Duration::from_days(1000).unwrap().to_iso_string(), "P1000D");
600		assert_eq!(Duration::from_hours(25).unwrap().to_iso_string(), "P1DT1H");
601		assert_eq!(Duration::from_minutes(1500).unwrap().to_iso_string(), "P1DT1H");
602		assert_eq!(Duration::from_seconds(90000).unwrap().to_iso_string(), "P1DT1H");
603	}
604
605	#[test]
606	fn test_duration_iso_string_edge_cases() {
607		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
608		assert_eq!(Duration::from_nanoseconds(999999999).unwrap().to_iso_string(), "PT0.999999999S");
609		assert_eq!(Duration::from_nanoseconds(1000000000).unwrap().to_iso_string(), "PT1S");
610		assert_eq!(Duration::from_nanoseconds(60 * 1000000000).unwrap().to_iso_string(), "PT1M");
611		assert_eq!(Duration::from_nanoseconds(3600 * 1000000000).unwrap().to_iso_string(), "PT1H");
612		assert_eq!(Duration::from_nanoseconds(86400 * 1000000000).unwrap().to_iso_string(), "P1D");
613	}
614
615	#[test]
616	fn test_duration_iso_string_precision() {
617		assert_eq!(Duration::from_nanoseconds(100).unwrap().to_iso_string(), "PT0.0000001S");
618		assert_eq!(Duration::from_nanoseconds(10).unwrap().to_iso_string(), "PT0.00000001S");
619		assert_eq!(Duration::from_nanoseconds(1).unwrap().to_iso_string(), "PT0.000000001S");
620	}
621
622	#[test]
623	fn test_duration_display_zero() {
624		assert_eq!(format!("{}", Duration::zero()), "0s");
625		assert_eq!(format!("{}", Duration::from_seconds(0).unwrap()), "0s");
626		assert_eq!(format!("{}", Duration::from_nanoseconds(0).unwrap()), "0s");
627		assert_eq!(format!("{}", Duration::default()), "0s");
628	}
629
630	#[test]
631	fn test_duration_display_seconds_only() {
632		assert_eq!(format!("{}", Duration::from_seconds(1).unwrap()), "1s");
633		assert_eq!(format!("{}", Duration::from_seconds(30).unwrap()), "30s");
634		assert_eq!(format!("{}", Duration::from_seconds(59).unwrap()), "59s");
635	}
636
637	#[test]
638	fn test_duration_display_minutes_only() {
639		assert_eq!(format!("{}", Duration::from_minutes(1).unwrap()), "1m");
640		assert_eq!(format!("{}", Duration::from_minutes(30).unwrap()), "30m");
641		assert_eq!(format!("{}", Duration::from_minutes(59).unwrap()), "59m");
642	}
643
644	#[test]
645	fn test_duration_display_hours_only() {
646		assert_eq!(format!("{}", Duration::from_hours(1).unwrap()), "1h");
647		assert_eq!(format!("{}", Duration::from_hours(12).unwrap()), "12h");
648		assert_eq!(format!("{}", Duration::from_hours(23).unwrap()), "23h");
649	}
650
651	#[test]
652	fn test_duration_display_days_only() {
653		assert_eq!(format!("{}", Duration::from_days(1).unwrap()), "1d");
654		assert_eq!(format!("{}", Duration::from_days(7).unwrap()), "7d");
655		assert_eq!(format!("{}", Duration::from_days(365).unwrap()), "365d");
656	}
657
658	#[test]
659	fn test_duration_display_weeks_only() {
660		assert_eq!(format!("{}", Duration::from_weeks(1).unwrap()), "7d");
661		assert_eq!(format!("{}", Duration::from_weeks(2).unwrap()), "14d");
662		assert_eq!(format!("{}", Duration::from_weeks(52).unwrap()), "364d");
663	}
664
665	#[test]
666	fn test_duration_display_months_only() {
667		assert_eq!(format!("{}", Duration::from_months(1).unwrap()), "1mo");
668		assert_eq!(format!("{}", Duration::from_months(6).unwrap()), "6mo");
669		assert_eq!(format!("{}", Duration::from_months(11).unwrap()), "11mo");
670	}
671
672	#[test]
673	fn test_duration_display_years_only() {
674		assert_eq!(format!("{}", Duration::from_years(1).unwrap()), "1y");
675		assert_eq!(format!("{}", Duration::from_years(10).unwrap()), "10y");
676		assert_eq!(format!("{}", Duration::from_years(100).unwrap()), "100y");
677	}
678
679	#[test]
680	fn test_duration_display_combined_time() {
681		let d = Duration::new(0, 0, (1 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap();
682		assert_eq!(format!("{}", d), "1h30m");
683
684		let d = Duration::new(0, 0, (5 * 60 + 45) * 1_000_000_000).unwrap();
685		assert_eq!(format!("{}", d), "5m45s");
686
687		let d = Duration::new(0, 0, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap();
688		assert_eq!(format!("{}", d), "2h30m45s");
689	}
690
691	#[test]
692	fn test_duration_display_combined_date_time() {
693		assert_eq!(format!("{}", Duration::new(0, 1, 2 * 60 * 60 * 1_000_000_000).unwrap()), "1d2h");
694		assert_eq!(format!("{}", Duration::new(0, 1, 30 * 60 * 1_000_000_000).unwrap()), "1d30m");
695		assert_eq!(
696			format!("{}", Duration::new(0, 1, (2 * 60 * 60 + 30 * 60) * 1_000_000_000).unwrap()),
697			"1d2h30m"
698		);
699		assert_eq!(
700			format!("{}", Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000).unwrap()),
701			"1d2h30m45s"
702		);
703	}
704
705	#[test]
706	fn test_duration_display_years_months() {
707		assert_eq!(format!("{}", Duration::new(13, 0, 0).unwrap()), "1y1mo");
708		assert_eq!(format!("{}", Duration::new(27, 0, 0).unwrap()), "2y3mo");
709	}
710
711	#[test]
712	fn test_duration_display_full_components() {
713		let nanos = (4 * 60 * 60 + 5 * 60 + 6) * 1_000_000_000i64;
714		assert_eq!(format!("{}", Duration::new(14, 3, nanos).unwrap()), "1y2mo3d4h5m6s");
715	}
716
717	#[test]
718	fn test_duration_display_milliseconds() {
719		assert_eq!(format!("{}", Duration::from_milliseconds(123).unwrap()), "123ms");
720		assert_eq!(format!("{}", Duration::from_milliseconds(1).unwrap()), "1ms");
721		assert_eq!(format!("{}", Duration::from_milliseconds(999).unwrap()), "999ms");
722		assert_eq!(format!("{}", Duration::from_milliseconds(1500).unwrap()), "1s500ms");
723	}
724
725	#[test]
726	fn test_duration_display_microseconds() {
727		assert_eq!(format!("{}", Duration::from_microseconds(123456).unwrap()), "123ms456us");
728		assert_eq!(format!("{}", Duration::from_microseconds(1).unwrap()), "1us");
729		assert_eq!(format!("{}", Duration::from_microseconds(999999).unwrap()), "999ms999us");
730		assert_eq!(format!("{}", Duration::from_microseconds(1500000).unwrap()), "1s500ms");
731	}
732
733	#[test]
734	fn test_duration_display_nanoseconds() {
735		assert_eq!(format!("{}", Duration::from_nanoseconds(123456789).unwrap()), "123ms456us789ns");
736		assert_eq!(format!("{}", Duration::from_nanoseconds(1).unwrap()), "1ns");
737		assert_eq!(format!("{}", Duration::from_nanoseconds(999999999).unwrap()), "999ms999us999ns");
738		assert_eq!(format!("{}", Duration::from_nanoseconds(1500000000).unwrap()), "1s500ms");
739	}
740
741	#[test]
742	fn test_duration_display_sub_second_decomposition() {
743		let d = Duration::new(0, 0, 1 * 1_000_000_000 + 500 * 1_000_000).unwrap();
744		assert_eq!(format!("{}", d), "1s500ms");
745
746		let d = Duration::new(0, 0, 2 * 1_000_000_000 + 123456 * 1_000).unwrap();
747		assert_eq!(format!("{}", d), "2s123ms456us");
748
749		let d = Duration::new(0, 0, 3 * 1_000_000_000 + 123456789).unwrap();
750		assert_eq!(format!("{}", d), "3s123ms456us789ns");
751	}
752
753	#[test]
754	fn test_duration_display_complex() {
755		let d = Duration::new(0, 1, (2 * 60 * 60 + 30 * 60 + 45) * 1_000_000_000 + 123 * 1_000_000).unwrap();
756		assert_eq!(format!("{}", d), "1d2h30m45s123ms");
757
758		let d = Duration::new(0, 7, (12 * 60 * 60 + 45 * 60 + 30) * 1_000_000_000 + 456789 * 1_000).unwrap();
759		assert_eq!(format!("{}", d), "7d12h45m30s456ms789us");
760	}
761
762	#[test]
763	fn test_duration_display_sub_second_only() {
764		assert_eq!(format!("{}", Duration::from_nanoseconds(100000000).unwrap()), "100ms");
765		assert_eq!(format!("{}", Duration::from_nanoseconds(120000000).unwrap()), "120ms");
766		assert_eq!(format!("{}", Duration::from_nanoseconds(123000000).unwrap()), "123ms");
767		assert_eq!(format!("{}", Duration::from_nanoseconds(100).unwrap()), "100ns");
768		assert_eq!(format!("{}", Duration::from_nanoseconds(10).unwrap()), "10ns");
769		assert_eq!(format!("{}", Duration::from_nanoseconds(1000).unwrap()), "1us");
770	}
771
772	#[test]
773	fn test_duration_display_negative() {
774		assert_eq!(format!("{}", Duration::from_seconds(-30).unwrap()), "-30s");
775		assert_eq!(format!("{}", Duration::from_minutes(-5).unwrap()), "-5m");
776		assert_eq!(format!("{}", Duration::from_hours(-2).unwrap()), "-2h");
777		assert_eq!(format!("{}", Duration::from_days(-1).unwrap()), "-1d");
778	}
779
780	#[test]
781	fn test_duration_display_negative_sub_second() {
782		assert_eq!(format!("{}", Duration::from_milliseconds(-500).unwrap()), "-500ms");
783		assert_eq!(format!("{}", Duration::from_microseconds(-100).unwrap()), "-100us");
784		assert_eq!(format!("{}", Duration::from_nanoseconds(-50).unwrap()), "-50ns");
785	}
786
787	#[test]
788	fn test_duration_display_large() {
789		assert_eq!(format!("{}", Duration::from_days(1000).unwrap()), "1000d");
790		assert_eq!(format!("{}", Duration::from_hours(25).unwrap()), "1d1h");
791		assert_eq!(format!("{}", Duration::from_minutes(1500).unwrap()), "1d1h");
792		assert_eq!(format!("{}", Duration::from_seconds(90000).unwrap()), "1d1h");
793	}
794
795	#[test]
796	fn test_duration_display_edge_cases() {
797		assert_eq!(format!("{}", Duration::from_nanoseconds(1).unwrap()), "1ns");
798		assert_eq!(format!("{}", Duration::from_nanoseconds(999999999).unwrap()), "999ms999us999ns");
799		assert_eq!(format!("{}", Duration::from_nanoseconds(1000000000).unwrap()), "1s");
800		assert_eq!(format!("{}", Duration::from_nanoseconds(60 * 1000000000).unwrap()), "1m");
801		assert_eq!(format!("{}", Duration::from_nanoseconds(3600 * 1000000000).unwrap()), "1h");
802		assert_eq!(format!("{}", Duration::from_nanoseconds(86400 * 1000000000).unwrap()), "1d");
803	}
804
805	#[test]
806	fn test_duration_display_abs_and_negate() {
807		let d = Duration::from_seconds(-30).unwrap();
808		assert_eq!(format!("{}", d.abs()), "30s");
809
810		let d = Duration::from_seconds(30).unwrap();
811		assert_eq!(format!("{}", d.negate()), "-30s");
812	}
813
814	#[test]
815	fn test_nanos_normalize_to_days() {
816		let d = Duration::new(0, 0, 86_400_000_000_000).unwrap();
817		assert_eq!(d.get_days(), 1);
818		assert_eq!(d.get_nanos(), 0);
819	}
820
821	#[test]
822	fn test_nanos_normalize_to_days_with_remainder() {
823		let d = Duration::new(0, 0, 86_400_000_000_000 + 1_000_000_000).unwrap();
824		assert_eq!(d.get_days(), 1);
825		assert_eq!(d.get_nanos(), 1_000_000_000);
826	}
827
828	#[test]
829	fn test_nanos_normalize_negative() {
830		let d = Duration::new(0, 0, -86_400_000_000_000).unwrap();
831		assert_eq!(d.get_days(), -1);
832		assert_eq!(d.get_nanos(), 0);
833	}
834
835	#[test]
836	fn test_normalized_equality() {
837		let d1 = Duration::new(0, 0, 86_400_000_000_000).unwrap();
838		let d2 = Duration::new(0, 1, 0).unwrap();
839		assert_eq!(d1, d2);
840	}
841
842	#[test]
843	fn test_normalized_ordering() {
844		let d1 = Duration::new(0, 0, 86_400_000_000_000 + 1).unwrap();
845		let d2 = Duration::new(0, 1, 0).unwrap();
846		assert!(d1 > d2);
847	}
848
849	// Months may differ in sign from days/nanos (months are variable-length).
850	// Days and nanos must share the same sign (they are commensurable).
851
852	#[test]
853	fn test_mixed_sign_months_days_allowed() {
854		let d = Duration::new(1, -15, 0).unwrap();
855		assert_eq!(d.get_months(), 1);
856		assert_eq!(d.get_days(), -15);
857	}
858
859	#[test]
860	fn test_mixed_sign_months_nanos_allowed() {
861		let d = Duration::new(-1, 0, 1_000_000_000).unwrap();
862		assert_eq!(d.get_months(), -1);
863		assert_eq!(d.get_nanos(), 1_000_000_000);
864	}
865
866	#[test]
867	fn test_mixed_sign_days_positive_nanos_negative() {
868		assert_mixed_sign(Duration::new(0, 1, -1), 1, -1);
869	}
870
871	#[test]
872	fn test_mixed_sign_days_negative_nanos_positive() {
873		assert_mixed_sign(Duration::new(0, -1, 1), -1, 1);
874	}
875
876	#[test]
877	fn test_is_positive_negative_mutually_exclusive() {
878		let durations = [
879			Duration::new(1, 0, 0).unwrap(),
880			Duration::new(0, 1, 0).unwrap(),
881			Duration::new(0, 0, 1).unwrap(),
882			Duration::new(-1, 0, 0).unwrap(),
883			Duration::new(0, -1, 0).unwrap(),
884			Duration::new(0, 0, -1).unwrap(),
885			Duration::new(1, 1, 1).unwrap(),
886			Duration::new(-1, -1, -1).unwrap(),
887			Duration::new(1, -15, 0).unwrap(), // mixed months/days
888			Duration::new(-1, 15, 0).unwrap(), // mixed months/days
889			Duration::zero(),
890		];
891		for d in durations {
892			assert!(
893				!(d.is_positive() && d.is_negative()),
894				"Duration {:?} is both positive and negative",
895				d
896			);
897		}
898	}
899
900	#[test]
901	fn test_mixed_months_days_is_neither_positive_nor_negative() {
902		let d = Duration::new(1, -15, 0).unwrap();
903		assert!(!d.is_positive());
904		assert!(!d.is_negative());
905	}
906
907	#[test]
908	fn test_from_days_overflow() {
909		assert_overflow(Duration::from_days(i32::MAX as i64 + 1));
910	}
911
912	#[test]
913	fn test_months_positive_days_negative_ok() {
914		let d = Duration::new(1, -15, 0).unwrap();
915		assert_eq!(d.get_months(), 1);
916		assert_eq!(d.get_days(), -15);
917		assert_eq!(d.get_nanos(), 0);
918	}
919
920	#[test]
921	fn test_months_negative_days_positive_ok() {
922		let d = Duration::new(-1, 15, 0).unwrap();
923		assert_eq!(d.get_months(), -1);
924		assert_eq!(d.get_days(), 15);
925	}
926
927	#[test]
928	fn test_months_positive_nanos_negative_ok() {
929		let d = Duration::new(1, 0, -1_000_000_000).unwrap();
930		assert_eq!(d.get_months(), 1);
931		assert_eq!(d.get_nanos(), -1_000_000_000);
932	}
933
934	#[test]
935	fn test_months_negative_nanos_positive_ok() {
936		let d = Duration::new(-1, 0, 1_000_000_000).unwrap();
937		assert_eq!(d.get_months(), -1);
938		assert_eq!(d.get_nanos(), 1_000_000_000);
939	}
940
941	#[test]
942	fn test_months_positive_days_negative_nanos_negative_ok() {
943		let d = Duration::new(2, -3, -1_000_000_000).unwrap();
944		assert_eq!(d.get_months(), 2);
945		assert_eq!(d.get_days(), -3);
946		assert_eq!(d.get_nanos(), -1_000_000_000);
947	}
948
949	#[test]
950	fn test_months_negative_days_positive_nanos_positive_ok() {
951		let d = Duration::new(-2, 3, 1_000_000_000).unwrap();
952		assert_eq!(d.get_months(), -2);
953		assert_eq!(d.get_days(), 3);
954		assert_eq!(d.get_nanos(), 1_000_000_000);
955	}
956
957	#[test]
958	fn test_days_positive_nanos_negative_with_months_err() {
959		assert_mixed_sign(Duration::new(5, 1, -1), 1, -1);
960	}
961
962	#[test]
963	fn test_days_negative_nanos_positive_with_months_err() {
964		assert_mixed_sign(Duration::new(-5, -1, 1), -1, 1);
965	}
966
967	#[test]
968	fn test_nanos_normalization_causes_days_nanos_mixed_sign_err() {
969		// 2 days of nanos + 1 extra, with days=-3 → after normalization days=-1, nanos=1
970		assert_mixed_sign(Duration::new(0, -3, 2 * 86_400_000_000_000 + 1), -1, 1);
971	}
972
973	#[test]
974	fn test_positive_months_negative_days_is_neither() {
975		let d = Duration::new(1, -15, 0).unwrap();
976		assert!(!d.is_positive());
977		assert!(!d.is_negative());
978	}
979
980	#[test]
981	fn test_negative_months_positive_days_is_neither() {
982		let d = Duration::new(-1, 15, 0).unwrap();
983		assert!(!d.is_positive());
984		assert!(!d.is_negative());
985	}
986
987	#[test]
988	fn test_positive_months_negative_days_negative_nanos_is_neither() {
989		let d = Duration::new(2, -3, -1_000_000_000).unwrap();
990		assert!(!d.is_positive());
991		assert!(!d.is_negative());
992	}
993
994	#[test]
995	fn test_all_positive_is_positive() {
996		let d = Duration::new(1, 2, 3).unwrap();
997		assert!(d.is_positive());
998		assert!(!d.is_negative());
999	}
1000
1001	#[test]
1002	fn test_all_negative_is_negative() {
1003		let d = Duration::new(-1, -2, -3).unwrap();
1004		assert!(!d.is_positive());
1005		assert!(d.is_negative());
1006	}
1007
1008	#[test]
1009	fn test_zero_is_neither_positive_nor_negative() {
1010		assert!(!Duration::zero().is_positive());
1011		assert!(!Duration::zero().is_negative());
1012	}
1013
1014	#[test]
1015	fn test_only_months_positive() {
1016		let d = Duration::new(1, 0, 0).unwrap();
1017		assert!(d.is_positive());
1018	}
1019
1020	#[test]
1021	fn test_only_days_negative() {
1022		let d = Duration::new(0, -1, 0).unwrap();
1023		assert!(d.is_negative());
1024	}
1025
1026	#[test]
1027	fn test_normalization_nanos_into_negative_days() {
1028		let d = Duration::new(-5, 0, -2 * 86_400_000_000_000).unwrap();
1029		assert_eq!(d.get_months(), -5);
1030		assert_eq!(d.get_days(), -2);
1031		assert_eq!(d.get_nanos(), 0);
1032	}
1033
1034	#[test]
1035	fn test_normalization_nanos_into_days_with_mixed_months() {
1036		let d = Duration::new(3, 1, 86_400_000_000_000 + 500_000_000).unwrap();
1037		assert_eq!(d.get_months(), 3);
1038		assert_eq!(d.get_days(), 2);
1039		assert_eq!(d.get_nanos(), 500_000_000);
1040	}
1041
1042	#[test]
1043	fn test_try_sub_month_minus_days() {
1044		let a = Duration::new(1, 0, 0).unwrap();
1045		let b = Duration::new(0, 15, 0).unwrap();
1046		let result = a.try_sub(b).unwrap();
1047		assert_eq!(result.get_months(), 1);
1048		assert_eq!(result.get_days(), -15);
1049	}
1050
1051	#[test]
1052	fn test_try_sub_day_minus_month() {
1053		let a = Duration::new(0, 1, 0).unwrap();
1054		let b = Duration::new(1, 0, 0).unwrap();
1055		let result = a.try_sub(b).unwrap();
1056		assert_eq!(result.get_months(), -1);
1057		assert_eq!(result.get_days(), 1);
1058	}
1059
1060	#[test]
1061	fn test_try_add_mixed_months_days() {
1062		let a = Duration::new(2, -10, 0).unwrap();
1063		let b = Duration::new(-1, -5, 0).unwrap();
1064		let result = a.try_add(b).unwrap();
1065		assert_eq!(result.get_months(), 1);
1066		assert_eq!(result.get_days(), -15);
1067	}
1068
1069	#[test]
1070	fn test_try_sub_days_nanos_mixed_sign_err() {
1071		let a = Duration::new(0, 1, 0).unwrap();
1072		let b = Duration::new(0, 0, 1).unwrap();
1073		// 1 day - 1 nano = days=1, nanos=-1 → mixed days/nanos sign error
1074		assert_mixed_sign(a.try_sub(b), 1, -1);
1075	}
1076
1077	#[test]
1078	fn test_try_mul_preserves_mixed_months() {
1079		let d = Duration::new(1, -3, 0).unwrap();
1080		let result = d.try_mul(2).unwrap();
1081		assert_eq!(result.get_months(), 2);
1082		assert_eq!(result.get_days(), -6);
1083	}
1084
1085	#[test]
1086	fn test_from_days_underflow() {
1087		assert_overflow(Duration::from_days(i32::MIN as i64 - 1));
1088	}
1089
1090	#[test]
1091	fn test_from_months_overflow() {
1092		assert_overflow(Duration::from_months(i32::MAX as i64 + 1));
1093	}
1094
1095	#[test]
1096	fn test_from_years_overflow() {
1097		assert_overflow(Duration::from_years(i32::MAX as i64 / 12 + 1));
1098	}
1099
1100	#[test]
1101	fn test_from_weeks_overflow() {
1102		assert_overflow(Duration::from_weeks(i32::MAX as i64 / 7 + 1));
1103	}
1104
1105	#[test]
1106	fn test_mul_months_truncation() {
1107		let d = Duration::from_months(1).unwrap();
1108		assert_overflow(d.try_mul(i32::MAX as i64 + 1));
1109	}
1110
1111	#[test]
1112	fn test_mul_days_truncation() {
1113		let d = Duration::from_days(1).unwrap();
1114		assert_overflow(d.try_mul(i32::MAX as i64 + 1));
1115	}
1116}