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