Skip to main content

reifydb_type/value/
datetime.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt::{self, Display, Formatter};
5
6use serde::{
7	Deserialize, Deserializer, Serialize, Serializer,
8	de::{self, Visitor},
9};
10
11use crate::{
12	error::{TemporalKind, TypeError},
13	fragment::Fragment,
14	value::{date::Date, duration::Duration, time::Time},
15};
16
17const NANOS_PER_SECOND: u64 = 1_000_000_000;
18const NANOS_PER_MILLI: u64 = 1_000_000;
19const NANOS_PER_DAY: u64 = 86_400 * NANOS_PER_SECOND;
20
21pub static CREATED_AT_COLUMN_NAME: &str = "created_at";
22pub static UPDATED_AT_COLUMN_NAME: &str = "updated_at";
23
24#[repr(transparent)]
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
26pub struct DateTime {
27	nanos: u64,
28}
29
30impl DateTime {
31	pub fn new(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
32		let date = Date::new(year, month, day)?;
33		let time = Time::new(hour, min, sec, nano)?;
34
35		let days = date.to_days_since_epoch();
36		if days < 0 {
37			return None;
38		}
39
40		let nanos = (days as u64).checked_mul(NANOS_PER_DAY)?.checked_add(time.to_nanos_since_midnight())?;
41		Some(Self {
42			nanos,
43		})
44	}
45
46	pub fn from_ymd_hms(
47		year: i32,
48		month: u32,
49		day: u32,
50		hour: u32,
51		min: u32,
52		sec: u32,
53	) -> Result<Self, Box<TypeError>> {
54		Self::new(year, month, day, hour, min, sec, 0).ok_or_else(|| {
55			Box::new(Self::overflow_err(format!(
56				"invalid datetime: {}-{:02}-{:02} {:02}:{:02}:{:02}",
57				year, month, day, hour, min, sec
58			)))
59		})
60	}
61
62	fn overflow_err(message: impl Into<String>) -> TypeError {
63		TypeError::Temporal {
64			kind: TemporalKind::DateTimeOverflow {
65				message: message.into(),
66			},
67			message: "datetime overflow".to_string(),
68			fragment: Fragment::None,
69		}
70	}
71
72	pub fn from_nanos(nanos: u64) -> Self {
73		Self {
74			nanos,
75		}
76	}
77
78	pub fn to_nanos(&self) -> u64 {
79		self.nanos
80	}
81
82	pub fn from_timestamp(timestamp: i64) -> Result<Self, Box<TypeError>> {
83		if timestamp < 0 {
84			return Err(Box::new(Self::overflow_err(format!(
85				"DateTime does not support timestamps before Unix epoch: {}",
86				timestamp
87			))));
88		}
89		let nanos = (timestamp as u64).checked_mul(NANOS_PER_SECOND).ok_or_else(|| {
90			Box::new(Self::overflow_err(format!("timestamp {} overflows DateTime range", timestamp)))
91		})?;
92		Ok(Self {
93			nanos,
94		})
95	}
96
97	pub fn from_timestamp_millis(millis: u64) -> Result<Self, Box<TypeError>> {
98		let nanos = millis.checked_mul(NANOS_PER_MILLI).ok_or_else(|| {
99			Box::new(Self::overflow_err(format!("timestamp_millis {} overflows DateTime range", millis)))
100		})?;
101		Ok(Self {
102			nanos,
103		})
104	}
105
106	pub fn from_timestamp_nanos(nanos: u128) -> Result<Self, Box<TypeError>> {
107		let nanos = u64::try_from(nanos).map_err(|_| {
108			Box::new(Self::overflow_err(format!("timestamp_nanos {} overflows u64 DateTime range", nanos)))
109		})?;
110		Ok(Self {
111			nanos,
112		})
113	}
114
115	pub fn timestamp(&self) -> i64 {
116		(self.nanos / NANOS_PER_SECOND) as i64
117	}
118
119	pub fn timestamp_millis(&self) -> i64 {
120		(self.nanos / NANOS_PER_MILLI) as i64
121	}
122
123	pub fn timestamp_nanos(&self) -> Result<i64, Box<TypeError>> {
124		i64::try_from(self.nanos).map_err(|_| Box::new(Self::overflow_err("DateTime nanos exceeds i64::MAX")))
125	}
126
127	pub fn try_date(&self) -> Result<Date, Box<TypeError>> {
128		let days_u64 = self.nanos / NANOS_PER_DAY;
129		let days = i32::try_from(days_u64)
130			.map_err(|_| Box::new(Self::overflow_err("DateTime nanos too large for date extraction")))?;
131		Date::from_days_since_epoch(days)
132			.ok_or_else(|| Box::new(Self::overflow_err("DateTime days out of range for Date")))
133	}
134
135	pub fn date(&self) -> Date {
136		self.try_date().expect("DateTime nanos too large for date extraction")
137	}
138
139	pub fn time(&self) -> Time {
140		let nanos_in_day = self.nanos % NANOS_PER_DAY;
141		Time::from_nanos_since_midnight(nanos_in_day).unwrap()
142	}
143
144	pub fn to_nanos_since_epoch_u128(&self) -> u128 {
145		self.nanos as u128
146	}
147
148	pub fn year(&self) -> i32 {
149		self.date().year()
150	}
151
152	pub fn month(&self) -> u32 {
153		self.date().month()
154	}
155
156	pub fn day(&self) -> u32 {
157		self.date().day()
158	}
159
160	pub fn hour(&self) -> u32 {
161		self.time().hour()
162	}
163
164	pub fn minute(&self) -> u32 {
165		self.time().minute()
166	}
167
168	pub fn second(&self) -> u32 {
169		self.time().second()
170	}
171
172	pub fn nanosecond(&self) -> u32 {
173		self.time().nanosecond()
174	}
175
176	pub fn add_duration(&self, dur: &Duration) -> Result<Self, Box<TypeError>> {
177		let date = self.date();
178		let time = self.time();
179		let mut year = date.year();
180		let mut month = date.month() as i32;
181		let mut day = date.day();
182
183		let total_months = month + dur.get_months();
184		year += (total_months - 1).div_euclid(12);
185		month = (total_months - 1).rem_euclid(12) + 1;
186
187		let max_day = Date::days_in_month(year, month as u32);
188		if day > max_day {
189			day = max_day;
190		}
191
192		let base_date = Date::new(year, month as u32, day).ok_or_else(|| {
193			Box::new(Self::overflow_err(format!(
194				"invalid date after adding duration: {}-{:02}-{:02}",
195				year, month, day
196			)))
197		})?;
198		let base_days = base_date.to_days_since_epoch() as i64 + dur.get_days() as i64;
199		let time_nanos = time.to_nanos_since_midnight() as i64 + dur.get_nanos();
200
201		let total_nanos = base_days as i128 * 86_400_000_000_000i128 + time_nanos as i128;
202
203		if total_nanos < 0 {
204			return Err(Box::new(Self::overflow_err("result is before Unix epoch")));
205		}
206
207		let nanos = u64::try_from(total_nanos)
208			.map_err(|_| Box::new(Self::overflow_err("result exceeds DateTime range")))?;
209		Ok(Self {
210			nanos,
211		})
212	}
213}
214
215impl Display for DateTime {
216	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
217		let date = self.date();
218		let time = self.time();
219
220		write!(f, "{}T{}Z", date, time)
221	}
222}
223
224impl Serialize for DateTime {
225	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
226	where
227		S: Serializer,
228	{
229		serializer.serialize_str(&self.to_string())
230	}
231}
232
233struct DateTimeVisitor;
234
235impl<'de> Visitor<'de> for DateTimeVisitor {
236	type Value = DateTime;
237
238	fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
239		formatter.write_str("a datetime in ISO 8601 format (YYYY-MM-DDTHH:MM:SS[.nnnnnnnnn]Z)")
240	}
241
242	fn visit_str<E>(self, value: &str) -> Result<DateTime, E>
243	where
244		E: de::Error,
245	{
246		let value = value.strip_suffix('Z').unwrap_or(value);
247
248		let parts: Vec<&str> = value.split('T').collect();
249		if parts.len() != 2 {
250			return Err(E::custom(format!("invalid datetime format: {}", value)));
251		}
252
253		let date_parts: Vec<&str> = parts[0].split('-').collect();
254		if date_parts.len() != 3 {
255			return Err(E::custom(format!("invalid date format: {}", parts[0])));
256		}
257
258		let (year_str, month_str, day_str) = (date_parts[0], date_parts[1], date_parts[2]);
259
260		let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
261		if year < 1970 {
262			return Err(E::custom(format!("DateTime does not support pre-epoch years: {}", year)));
263		}
264		let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
265		let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
266
267		let (time_part, nano_part) = if let Some(dot_pos) = parts[1].find('.') {
268			(&parts[1][..dot_pos], Some(&parts[1][dot_pos + 1..]))
269		} else {
270			(parts[1], None)
271		};
272
273		let time_parts: Vec<&str> = time_part.split(':').collect();
274		if time_parts.len() != 3 {
275			return Err(E::custom(format!("invalid time format: {}", parts[1])));
276		}
277
278		let hour = time_parts[0]
279			.parse::<u32>()
280			.map_err(|_| E::custom(format!("invalid hour: {}", time_parts[0])))?;
281		let minute = time_parts[1]
282			.parse::<u32>()
283			.map_err(|_| E::custom(format!("invalid minute: {}", time_parts[1])))?;
284		let second = time_parts[2]
285			.parse::<u32>()
286			.map_err(|_| E::custom(format!("invalid second: {}", time_parts[2])))?;
287
288		let nano = if let Some(nano_str) = nano_part {
289			let padded = if nano_str.len() < 9 {
290				format!("{:0<9}", nano_str)
291			} else {
292				nano_str[..9].to_string()
293			};
294			padded.parse::<u32>().map_err(|_| E::custom(format!("invalid nanoseconds: {}", nano_str)))?
295		} else {
296			0
297		};
298
299		DateTime::new(year, month, day, hour, minute, second, nano).ok_or_else(|| {
300			E::custom(format!(
301				"invalid datetime: {}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}Z",
302				year, month, day, hour, minute, second, nano
303			))
304		})
305	}
306}
307
308impl<'de> Deserialize<'de> for DateTime {
309	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
310	where
311		D: Deserializer<'de>,
312	{
313		deserializer.deserialize_str(DateTimeVisitor)
314	}
315}
316
317#[cfg(test)]
318pub mod tests {
319	use std::fmt::Debug;
320
321	use serde_json::{from_str, to_string};
322
323	use crate::{
324		error::{TemporalKind, TypeError},
325		value::{datetime::DateTime, duration::Duration},
326	};
327
328	#[test]
329	fn test_datetime_display_standard_format() {
330		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
331		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
332
333		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
334		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
335
336		let datetime = DateTime::new(1999, 12, 31, 23, 59, 59, 999999999).unwrap();
337		assert_eq!(format!("{}", datetime), "1999-12-31T23:59:59.999999999Z");
338	}
339
340	#[test]
341	fn test_datetime_display_millisecond_precision() {
342		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123000000).unwrap();
343		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123000000Z");
344
345		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 001000000).unwrap();
346		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.001000000Z");
347
348		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999000000).unwrap();
349		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999000000Z");
350	}
351
352	#[test]
353	fn test_datetime_display_microsecond_precision() {
354		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456000).unwrap();
355		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456000Z");
356
357		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000001000).unwrap();
358		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000001000Z");
359
360		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999000).unwrap();
361		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999000Z");
362	}
363
364	#[test]
365	fn test_datetime_display_nanosecond_precision() {
366		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
367		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
368
369		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000000001).unwrap();
370		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000001Z");
371
372		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999999).unwrap();
373		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999999Z");
374	}
375
376	#[test]
377	fn test_datetime_display_zero_fractional_seconds() {
378		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 0).unwrap();
379		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000000Z");
380
381		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
382		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
383	}
384
385	#[test]
386	fn test_datetime_display_edge_times() {
387		// Midnight
388		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
389		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
390
391		// Almost midnight next day
392		let datetime = DateTime::new(2024, 3, 15, 23, 59, 59, 999999999).unwrap();
393		assert_eq!(format!("{}", datetime), "2024-03-15T23:59:59.999999999Z");
394
395		// Noon
396		let datetime = DateTime::new(2024, 3, 15, 12, 0, 0, 0).unwrap();
397		assert_eq!(format!("{}", datetime), "2024-03-15T12:00:00.000000000Z");
398	}
399
400	#[test]
401	fn test_datetime_display_unix_epoch() {
402		let datetime = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
403		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
404
405		let datetime = DateTime::new(1970, 1, 1, 0, 0, 1, 0).unwrap();
406		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:01.000000000Z");
407	}
408
409	#[test]
410	fn test_datetime_display_leap_year() {
411		let datetime = DateTime::new(2024, 2, 29, 12, 30, 45, 123456789).unwrap();
412		assert_eq!(format!("{}", datetime), "2024-02-29T12:30:45.123456789Z");
413
414		let datetime = DateTime::new(2000, 2, 29, 0, 0, 0, 0).unwrap();
415		assert_eq!(format!("{}", datetime), "2000-02-29T00:00:00.000000000Z");
416	}
417
418	#[test]
419	fn test_datetime_display_boundary_dates() {
420		// Century boundaries
421		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
422		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
423
424		let datetime = DateTime::new(2100, 1, 1, 0, 0, 0, 0).unwrap();
425		assert_eq!(format!("{}", datetime), "2100-01-01T00:00:00.000000000Z");
426
427		// Max representable date (~year 2554 with u64 nanos)
428		let datetime = DateTime::new(2554, 1, 1, 0, 0, 0, 0).unwrap();
429		assert_eq!(format!("{}", datetime), "2554-01-01T00:00:00.000000000Z");
430
431		// Year 9999 exceeds u64 nanos range
432		assert!(DateTime::new(9999, 12, 31, 23, 59, 59, 999999999).is_none());
433	}
434
435	#[test]
436	fn test_datetime_rejects_pre_epoch() {
437		// Year 1 is before epoch
438		assert!(DateTime::new(1, 1, 1, 0, 0, 0, 0).is_none());
439
440		// 1900 is before epoch
441		assert!(DateTime::new(1900, 1, 1, 0, 0, 0, 0).is_none());
442
443		// 1969 is before epoch
444		assert!(DateTime::new(1969, 12, 31, 23, 59, 59, 999999999).is_none());
445
446		// Negative timestamp
447		assert!(DateTime::from_timestamp(-1).is_err());
448	}
449
450	#[test]
451	fn test_datetime_display_default() {
452		let datetime = DateTime::default();
453		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
454	}
455
456	#[test]
457	fn test_datetime_display_all_hours() {
458		for hour in 0..24 {
459			let datetime = DateTime::new(2024, 3, 15, hour, 30, 45, 123456789).unwrap();
460			let expected = format!("2024-03-15T{:02}:30:45.123456789Z", hour);
461			assert_eq!(format!("{}", datetime), expected);
462		}
463	}
464
465	#[test]
466	fn test_datetime_display_all_minutes() {
467		for minute in 0..60 {
468			let datetime = DateTime::new(2024, 3, 15, 14, minute, 45, 123456789).unwrap();
469			let expected = format!("2024-03-15T14:{:02}:45.123456789Z", minute);
470			assert_eq!(format!("{}", datetime), expected);
471		}
472	}
473
474	#[test]
475	fn test_datetime_display_all_seconds() {
476		for second in 0..60 {
477			let datetime = DateTime::new(2024, 3, 15, 14, 30, second, 123456789).unwrap();
478			let expected = format!("2024-03-15T14:30:{:02}.123456789Z", second);
479			assert_eq!(format!("{}", datetime), expected);
480		}
481	}
482
483	#[test]
484	fn test_datetime_display_from_timestamp() {
485		let datetime = DateTime::from_timestamp(0).unwrap();
486		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
487
488		let datetime = DateTime::from_timestamp(1234567890).unwrap();
489		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.000000000Z");
490	}
491
492	#[test]
493	fn test_datetime_display_from_timestamp_millis() {
494		let datetime = DateTime::from_timestamp_millis(1234567890123).unwrap();
495		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.123000000Z");
496
497		let datetime = DateTime::from_timestamp_millis(0).unwrap();
498		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
499	}
500
501	#[test]
502	fn test_datetime_from_nanos_roundtrip() {
503		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
504		let nanos = datetime.to_nanos();
505		let recovered = DateTime::from_nanos(nanos);
506		assert_eq!(datetime, recovered);
507	}
508
509	#[test]
510	fn test_datetime_roundtrip() {
511		let test_cases = [
512			(1970, 1, 1, 0, 0, 0, 0u32),
513			(2024, 3, 15, 14, 30, 45, 123456789),
514			(2000, 2, 29, 23, 59, 59, 999999999),
515		];
516
517		for (y, m, d, h, min, s, n) in test_cases {
518			let datetime = DateTime::new(y, m, d, h, min, s, n).unwrap();
519			let nanos = datetime.to_nanos();
520			let recovered = DateTime::from_nanos(nanos);
521
522			assert_eq!(datetime.year(), recovered.year());
523			assert_eq!(datetime.month(), recovered.month());
524			assert_eq!(datetime.day(), recovered.day());
525			assert_eq!(datetime.hour(), recovered.hour());
526			assert_eq!(datetime.minute(), recovered.minute());
527			assert_eq!(datetime.second(), recovered.second());
528			assert_eq!(datetime.nanosecond(), recovered.nanosecond());
529		}
530	}
531
532	#[test]
533	fn test_datetime_components() {
534		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
535
536		assert_eq!(datetime.year(), 2024);
537		assert_eq!(datetime.month(), 3);
538		assert_eq!(datetime.day(), 15);
539		assert_eq!(datetime.hour(), 14);
540		assert_eq!(datetime.minute(), 30);
541		assert_eq!(datetime.second(), 45);
542		assert_eq!(datetime.nanosecond(), 123456789);
543	}
544
545	#[test]
546	fn test_serde_roundtrip() {
547		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
548		let json = to_string(&datetime).unwrap();
549		assert_eq!(json, "\"2024-03-15T14:30:45.123456789Z\"");
550
551		let recovered: DateTime = from_str(&json).unwrap();
552		assert_eq!(datetime, recovered);
553	}
554
555	fn assert_datetime_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
556		let err = result.expect_err("expected DateTimeOverflow error");
557		match *err {
558			TypeError::Temporal {
559				kind: TemporalKind::DateTimeOverflow {
560					..
561				},
562				..
563			} => {}
564			other => panic!("expected DateTimeOverflow, got: {:?}", other),
565		}
566	}
567
568	#[test]
569	fn test_from_timestamp_nanos_overflow() {
570		let huge: u128 = u64::MAX as u128 + 1;
571		assert_datetime_overflow(DateTime::from_timestamp_nanos(huge));
572	}
573
574	#[test]
575	fn test_from_timestamp_nanos_max_u64_ok() {
576		let dt = DateTime::from_timestamp_nanos(u64::MAX as u128).unwrap();
577		assert_eq!(dt.to_nanos(), u64::MAX);
578	}
579
580	#[test]
581	fn test_from_timestamp_large_value_overflow() {
582		assert_datetime_overflow(DateTime::from_timestamp(i64::MAX));
583	}
584
585	#[test]
586	fn test_from_timestamp_negative_overflow() {
587		assert_datetime_overflow(DateTime::from_timestamp(-1));
588	}
589
590	#[test]
591	fn test_from_timestamp_millis_overflow() {
592		assert_datetime_overflow(DateTime::from_timestamp_millis(u64::MAX));
593	}
594
595	#[test]
596	fn test_from_timestamp_millis_boundary_ok() {
597		let dt = DateTime::from_timestamp_millis(1_700_000_000_000).unwrap();
598		assert!(dt.to_nanos() > 0);
599	}
600
601	#[test]
602	fn test_timestamp_nanos_large_value_returns_err() {
603		let dt = DateTime::from_nanos(i64::MAX as u64 + 1);
604		assert_datetime_overflow(dt.timestamp_nanos());
605	}
606
607	#[test]
608	fn test_timestamp_nanos_within_range_ok() {
609		let dt = DateTime::from_nanos(i64::MAX as u64);
610		assert_eq!(dt.timestamp_nanos().unwrap(), i64::MAX);
611	}
612
613	#[test]
614	fn test_try_date_max_nanos_ok() {
615		// u64::MAX nanos / NANOS_PER_DAY = 213_503 which fits in i32
616		let dt = DateTime::from_nanos(u64::MAX);
617		let date = dt.try_date().unwrap();
618		assert!(date.year() > 2500);
619	}
620
621	#[test]
622	fn test_add_duration_overflow() {
623		let dt = DateTime::from_nanos(u64::MAX - 1);
624		let dur = Duration::from_days(1).unwrap();
625		assert_datetime_overflow(dt.add_duration(&dur));
626	}
627
628	#[test]
629	fn test_add_duration_before_epoch() {
630		let dt = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
631		let dur = Duration::from_seconds(-1).unwrap();
632		assert_datetime_overflow(dt.add_duration(&dur));
633	}
634
635	#[test]
636	fn test_add_duration_negative_nanos_borrows_from_days() {
637		let dt = DateTime::new(2024, 3, 15, 0, 0, 30, 0).unwrap();
638		let dur = Duration::from_seconds(-60).unwrap();
639		let result = dt.add_duration(&dur).unwrap();
640		assert_eq!(result.year(), 2024);
641		assert_eq!(result.month(), 3);
642		assert_eq!(result.day(), 14);
643		assert_eq!(result.hour(), 23);
644		assert_eq!(result.minute(), 59);
645		assert_eq!(result.second(), 30);
646	}
647
648	#[test]
649	fn test_add_duration_nanos_overflow_into_next_day() {
650		let dt = DateTime::new(2024, 3, 15, 23, 59, 30, 0).unwrap();
651		let dur = Duration::from_seconds(60).unwrap();
652		let result = dt.add_duration(&dur).unwrap();
653		assert_eq!(result.year(), 2024);
654		assert_eq!(result.month(), 3);
655		assert_eq!(result.day(), 16);
656		assert_eq!(result.hour(), 0);
657		assert_eq!(result.minute(), 0);
658		assert_eq!(result.second(), 30);
659	}
660}