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