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