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