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