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