1use std::{
5 fmt::{self, Display, Formatter},
6 ops::{Add, Rem, Sub},
7 str::FromStr,
8};
9
10use serde::{
11 Deserialize, Deserializer, Serialize, Serializer,
12 de::{self, Visitor},
13};
14
15use crate::{
16 clock::ClockNow,
17 error::{Error, TemporalKind, TypeError},
18 fragment::Fragment,
19 value::{date::Date, duration::Duration, temporal::parse::datetime::parse_datetime, time::Time},
20};
21
22const NANOS_PER_SECOND: u64 = 1_000_000_000;
23const NANOS_PER_MILLI: u64 = 1_000_000;
24const NANOS_PER_DAY: u64 = 86_400 * NANOS_PER_SECOND;
25
26pub static CREATED_AT_COLUMN_NAME: &str = "created_at";
27pub static UPDATED_AT_COLUMN_NAME: &str = "updated_at";
28
29#[repr(transparent)]
30#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
31pub struct DateTime {
32 nanos: u64,
33}
34
35impl DateTime {
36 pub fn new(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
37 let date = Date::new(year, month, day)?;
38 let time = Time::new(hour, min, sec, nano)?;
39
40 let days = date.to_days_since_epoch();
41 if days < 0 {
42 return None;
43 }
44
45 let nanos = (days as u64).checked_mul(NANOS_PER_DAY)?.checked_add(time.to_nanos_since_midnight())?;
46 Some(Self {
47 nanos,
48 })
49 }
50
51 pub fn from_ymd_hms(
52 year: i32,
53 month: u32,
54 day: u32,
55 hour: u32,
56 min: u32,
57 sec: u32,
58 ) -> Result<Self, Box<TypeError>> {
59 Self::new(year, month, day, hour, min, sec, 0).ok_or_else(|| {
60 Box::new(Self::overflow_err(format!(
61 "invalid datetime: {}-{:02}-{:02} {:02}:{:02}:{:02}",
62 year, month, day, hour, min, sec
63 )))
64 })
65 }
66
67 fn overflow_err(message: impl Into<String>) -> TypeError {
68 TypeError::Temporal {
69 kind: TemporalKind::DateTimeOverflow {
70 message: message.into(),
71 },
72 message: "datetime overflow".to_string(),
73 fragment: Fragment::None,
74 }
75 }
76
77 pub fn from_nanos(nanos: u64) -> Self {
78 Self {
79 nanos,
80 }
81 }
82
83 pub fn to_nanos(&self) -> u64 {
84 self.nanos
85 }
86
87 pub fn from_timestamp(timestamp: i64) -> Result<Self, Box<TypeError>> {
88 if timestamp < 0 {
89 return Err(Box::new(Self::overflow_err(format!(
90 "DateTime does not support timestamps before Unix epoch: {}",
91 timestamp
92 ))));
93 }
94 let nanos = (timestamp as u64).checked_mul(NANOS_PER_SECOND).ok_or_else(|| {
95 Box::new(Self::overflow_err(format!("timestamp {} overflows DateTime range", timestamp)))
96 })?;
97 Ok(Self {
98 nanos,
99 })
100 }
101
102 pub fn from_timestamp_millis(millis: u64) -> Result<Self, Box<TypeError>> {
103 let nanos = millis.checked_mul(NANOS_PER_MILLI).ok_or_else(|| {
104 Box::new(Self::overflow_err(format!("timestamp_millis {} overflows DateTime range", millis)))
105 })?;
106 Ok(Self {
107 nanos,
108 })
109 }
110
111 pub fn from_timestamp_nanos(nanos: u128) -> Result<Self, Box<TypeError>> {
112 let nanos = u64::try_from(nanos).map_err(|_| {
113 Box::new(Self::overflow_err(format!("timestamp_nanos {} overflows u64 DateTime range", nanos)))
114 })?;
115 Ok(Self {
116 nanos,
117 })
118 }
119
120 pub fn timestamp(&self) -> i64 {
121 (self.nanos / NANOS_PER_SECOND) as i64
122 }
123
124 pub fn timestamp_millis(&self) -> i64 {
125 (self.nanos / NANOS_PER_MILLI) as i64
126 }
127
128 pub fn timestamp_nanos(&self) -> Result<i64, Box<TypeError>> {
129 i64::try_from(self.nanos).map_err(|_| Box::new(Self::overflow_err("DateTime nanos exceeds i64::MAX")))
130 }
131
132 pub fn try_date(&self) -> Result<Date, Box<TypeError>> {
133 let days_u64 = self.nanos / NANOS_PER_DAY;
134 let days = i32::try_from(days_u64)
135 .map_err(|_| Box::new(Self::overflow_err("DateTime nanos too large for date extraction")))?;
136 Date::from_days_since_epoch(days)
137 .ok_or_else(|| Box::new(Self::overflow_err("DateTime days out of range for Date")))
138 }
139
140 pub fn date(&self) -> Date {
141 self.try_date().expect("DateTime nanos too large for date extraction")
142 }
143
144 pub fn time(&self) -> Time {
145 let nanos_in_day = self.nanos % NANOS_PER_DAY;
146 Time::from_nanos_since_midnight(nanos_in_day).unwrap()
147 }
148
149 pub fn to_nanos_since_epoch_u128(&self) -> u128 {
150 self.nanos as u128
151 }
152
153 pub fn year(&self) -> i32 {
154 self.date().year()
155 }
156
157 pub fn month(&self) -> u32 {
158 self.date().month()
159 }
160
161 pub fn day(&self) -> u32 {
162 self.date().day()
163 }
164
165 pub fn hour(&self) -> u32 {
166 self.time().hour()
167 }
168
169 pub fn minute(&self) -> u32 {
170 self.time().minute()
171 }
172
173 pub fn second(&self) -> u32 {
174 self.time().second()
175 }
176
177 pub fn nanosecond(&self) -> u32 {
178 self.time().nanosecond()
179 }
180
181 pub fn add_duration(&self, dur: &Duration) -> Result<Self, Box<TypeError>> {
182 let date = self.date();
183 let time = self.time();
184 let mut year = date.year();
185 let mut month = date.month() as i32;
186 let mut day = date.day();
187
188 let total_months = month + dur.get_months();
189 year += (total_months - 1).div_euclid(12);
190 month = (total_months - 1).rem_euclid(12) + 1;
191
192 let max_day = Date::days_in_month(year, month as u32);
193 if day > max_day {
194 day = max_day;
195 }
196
197 let base_date = Date::new(year, month as u32, day).ok_or_else(|| {
198 Box::new(Self::overflow_err(format!(
199 "invalid date after adding duration: {}-{:02}-{:02}",
200 year, month, day
201 )))
202 })?;
203 let base_days = base_date.to_days_since_epoch() as i64 + dur.get_days() as i64;
204 let time_nanos = time.to_nanos_since_midnight() as i64 + dur.get_nanos();
205
206 let total_nanos = base_days as i128 * 86_400_000_000_000i128 + time_nanos as i128;
207
208 if total_nanos < 0 {
209 return Err(Box::new(Self::overflow_err("result is before Unix epoch")));
210 }
211
212 let nanos = u64::try_from(total_nanos)
213 .map_err(|_| Box::new(Self::overflow_err("result exceeds DateTime range")))?;
214 Ok(Self {
215 nanos,
216 })
217 }
218}
219
220impl DateTime {
221 pub fn saturating_add(self, rhs: Duration) -> DateTime {
222 let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
223 i64::MIN
224 } else {
225 i64::MAX
226 });
227 let nanos = (self.to_nanos() as i128 + total as i128).clamp(0, u64::MAX as i128);
228 DateTime::from_nanos(nanos as u64)
229 }
230
231 pub fn saturating_sub(self, rhs: Duration) -> DateTime {
232 let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
233 i64::MIN
234 } else {
235 i64::MAX
236 });
237 let nanos = (self.to_nanos() as i128 - total as i128).clamp(0, u64::MAX as i128);
238 DateTime::from_nanos(nanos as u64)
239 }
240
241 pub fn checked_sub(self, rhs: Duration) -> Option<DateTime> {
242 let total = rhs.as_nanos().ok()?;
243 let nanos = self.to_nanos() as i128 - total as i128;
244 if nanos < 0 {
245 None
246 } else {
247 Some(DateTime::from_nanos(nanos as u64))
248 }
249 }
250
251 pub fn saturating_duration_since(self, earlier: DateTime) -> Duration {
252 let diff = (self.to_nanos() as i128 - earlier.to_nanos() as i128)
253 .clamp(i64::MIN as i128, i64::MAX as i128) as i64;
254 Duration::from_nanoseconds(diff).unwrap_or_else(|_| Duration::zero())
255 }
256}
257
258impl Add<Duration> for DateTime {
259 type Output = DateTime;
260
261 #[inline]
262 fn add(self, rhs: Duration) -> DateTime {
263 let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
264 let nanos = self.to_nanos() as i128 + total as i128;
265 DateTime::from_nanos(u64::try_from(nanos).expect("datetime addition out of range"))
266 }
267}
268
269impl Sub<Duration> for DateTime {
270 type Output = DateTime;
271
272 #[inline]
273 fn sub(self, rhs: Duration) -> DateTime {
274 let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
275 let nanos = self.to_nanos() as i128 - total as i128;
276 DateTime::from_nanos(u64::try_from(nanos).expect("datetime subtraction out of range"))
277 }
278}
279
280impl Sub<DateTime> for DateTime {
281 type Output = Duration;
282
283 #[inline]
284 fn sub(self, rhs: DateTime) -> Duration {
285 let diff = self.to_nanos() as i128 - rhs.to_nanos() as i128;
286 Duration::from_nanoseconds(i64::try_from(diff).expect("datetime difference exceeds i64 nanoseconds"))
287 .expect("datetime difference out of duration range")
288 }
289}
290
291impl Rem<Duration> for DateTime {
292 type Output = Duration;
293
294 #[inline]
295 fn rem(self, rhs: Duration) -> Duration {
296 let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
297 let total = u64::try_from(total).expect("duration must be positive for windowing");
298 Duration::from_nanoseconds((self.to_nanos() % total) as i64)
299 .expect("datetime remainder out of duration range")
300 }
301}
302
303impl Display for DateTime {
304 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
305 let date = self.date();
306 let time = self.time();
307
308 write!(f, "{}T{}Z", date, time)
309 }
310}
311
312impl Serialize for DateTime {
313 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
314 where
315 S: Serializer,
316 {
317 serializer.serialize_u64(self.nanos)
318 }
319}
320
321struct DateTimeVisitor;
322
323impl<'de> Visitor<'de> for DateTimeVisitor {
324 type Value = DateTime;
325
326 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
327 formatter.write_str("a datetime as nanoseconds since the Unix epoch (u64)")
328 }
329
330 fn visit_u64<E>(self, value: u64) -> Result<DateTime, E>
331 where
332 E: de::Error,
333 {
334 Ok(DateTime::from_nanos(value))
335 }
336}
337
338impl<'de> Deserialize<'de> for DateTime {
339 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
340 where
341 D: Deserializer<'de>,
342 {
343 deserializer.deserialize_u64(DateTimeVisitor)
344 }
345}
346
347impl DateTime {
348 pub fn now<C: ClockNow>(clock: &C) -> Self {
349 Self::from_nanos(clock.now_nanos())
350 }
351}
352
353impl FromStr for DateTime {
354 type Err = Error;
355
356 fn from_str(s: &str) -> Result<Self, Self::Err> {
357 parse_datetime(Fragment::internal(s.trim()))
358 }
359}
360
361#[cfg(test)]
362pub mod tests {
363 use std::fmt::Debug;
364
365 use postcard::{from_bytes, to_allocvec};
366 use serde_json::{from_str, to_string};
367
368 use crate::{
369 error::{TemporalKind, TypeError},
370 value::{datetime::DateTime, duration::Duration},
371 };
372
373 #[test]
374 fn test_datetime_display_standard_format() {
375 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
376 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
377
378 let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
379 assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
380
381 let datetime = DateTime::new(1999, 12, 31, 23, 59, 59, 999999999).unwrap();
382 assert_eq!(format!("{}", datetime), "1999-12-31T23:59:59.999999999Z");
383 }
384
385 #[test]
386 fn test_datetime_display_millisecond_precision() {
387 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123000000).unwrap();
388 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123000000Z");
389
390 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 001000000).unwrap();
391 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.001000000Z");
392
393 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999000000).unwrap();
394 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999000000Z");
395 }
396
397 #[test]
398 fn test_datetime_display_microsecond_precision() {
399 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456000).unwrap();
400 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456000Z");
401
402 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000001000).unwrap();
403 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000001000Z");
404
405 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999000).unwrap();
406 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999000Z");
407 }
408
409 #[test]
410 fn test_datetime_display_nanosecond_precision() {
411 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
412 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
413
414 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000000001).unwrap();
415 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000001Z");
416
417 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999999).unwrap();
418 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999999Z");
419 }
420
421 #[test]
422 fn test_datetime_display_zero_fractional_seconds() {
423 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 0).unwrap();
424 assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000000Z");
425
426 let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
427 assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
428 }
429
430 #[test]
431 fn test_datetime_display_edge_times() {
432 let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
434 assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
435
436 let datetime = DateTime::new(2024, 3, 15, 23, 59, 59, 999999999).unwrap();
438 assert_eq!(format!("{}", datetime), "2024-03-15T23:59:59.999999999Z");
439
440 let datetime = DateTime::new(2024, 3, 15, 12, 0, 0, 0).unwrap();
442 assert_eq!(format!("{}", datetime), "2024-03-15T12:00:00.000000000Z");
443 }
444
445 #[test]
446 fn test_datetime_display_unix_epoch() {
447 let datetime = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
448 assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
449
450 let datetime = DateTime::new(1970, 1, 1, 0, 0, 1, 0).unwrap();
451 assert_eq!(format!("{}", datetime), "1970-01-01T00:00:01.000000000Z");
452 }
453
454 #[test]
455 fn test_datetime_display_leap_year() {
456 let datetime = DateTime::new(2024, 2, 29, 12, 30, 45, 123456789).unwrap();
457 assert_eq!(format!("{}", datetime), "2024-02-29T12:30:45.123456789Z");
458
459 let datetime = DateTime::new(2000, 2, 29, 0, 0, 0, 0).unwrap();
460 assert_eq!(format!("{}", datetime), "2000-02-29T00:00:00.000000000Z");
461 }
462
463 #[test]
464 fn test_datetime_display_boundary_dates() {
465 let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
467 assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
468
469 let datetime = DateTime::new(2100, 1, 1, 0, 0, 0, 0).unwrap();
470 assert_eq!(format!("{}", datetime), "2100-01-01T00:00:00.000000000Z");
471
472 let datetime = DateTime::new(2554, 1, 1, 0, 0, 0, 0).unwrap();
474 assert_eq!(format!("{}", datetime), "2554-01-01T00:00:00.000000000Z");
475
476 assert!(DateTime::new(9999, 12, 31, 23, 59, 59, 999999999).is_none());
478 }
479
480 #[test]
481 fn test_datetime_rejects_pre_epoch() {
482 assert!(DateTime::new(1, 1, 1, 0, 0, 0, 0).is_none());
484
485 assert!(DateTime::new(1900, 1, 1, 0, 0, 0, 0).is_none());
487
488 assert!(DateTime::new(1969, 12, 31, 23, 59, 59, 999999999).is_none());
490
491 assert!(DateTime::from_timestamp(-1).is_err());
493 }
494
495 #[test]
496 fn test_datetime_display_default() {
497 let datetime = DateTime::default();
498 assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
499 }
500
501 #[test]
502 fn test_datetime_display_all_hours() {
503 for hour in 0..24 {
504 let datetime = DateTime::new(2024, 3, 15, hour, 30, 45, 123456789).unwrap();
505 let expected = format!("2024-03-15T{:02}:30:45.123456789Z", hour);
506 assert_eq!(format!("{}", datetime), expected);
507 }
508 }
509
510 #[test]
511 fn test_datetime_display_all_minutes() {
512 for minute in 0..60 {
513 let datetime = DateTime::new(2024, 3, 15, 14, minute, 45, 123456789).unwrap();
514 let expected = format!("2024-03-15T14:{:02}:45.123456789Z", minute);
515 assert_eq!(format!("{}", datetime), expected);
516 }
517 }
518
519 #[test]
520 fn test_datetime_display_all_seconds() {
521 for second in 0..60 {
522 let datetime = DateTime::new(2024, 3, 15, 14, 30, second, 123456789).unwrap();
523 let expected = format!("2024-03-15T14:30:{:02}.123456789Z", second);
524 assert_eq!(format!("{}", datetime), expected);
525 }
526 }
527
528 #[test]
529 fn test_datetime_display_from_timestamp() {
530 let datetime = DateTime::from_timestamp(0).unwrap();
531 assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
532
533 let datetime = DateTime::from_timestamp(1234567890).unwrap();
534 assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.000000000Z");
535 }
536
537 #[test]
538 fn test_datetime_display_from_timestamp_millis() {
539 let datetime = DateTime::from_timestamp_millis(1234567890123).unwrap();
540 assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.123000000Z");
541
542 let datetime = DateTime::from_timestamp_millis(0).unwrap();
543 assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
544 }
545
546 #[test]
547 fn test_datetime_from_nanos_roundtrip() {
548 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
549 let nanos = datetime.to_nanos();
550 let recovered = DateTime::from_nanos(nanos);
551 assert_eq!(datetime, recovered);
552 }
553
554 #[test]
555 fn test_datetime_roundtrip() {
556 let test_cases = [
557 (1970, 1, 1, 0, 0, 0, 0u32),
558 (2024, 3, 15, 14, 30, 45, 123456789),
559 (2000, 2, 29, 23, 59, 59, 999999999),
560 ];
561
562 for (y, m, d, h, min, s, n) in test_cases {
563 let datetime = DateTime::new(y, m, d, h, min, s, n).unwrap();
564 let nanos = datetime.to_nanos();
565 let recovered = DateTime::from_nanos(nanos);
566
567 assert_eq!(datetime.year(), recovered.year());
568 assert_eq!(datetime.month(), recovered.month());
569 assert_eq!(datetime.day(), recovered.day());
570 assert_eq!(datetime.hour(), recovered.hour());
571 assert_eq!(datetime.minute(), recovered.minute());
572 assert_eq!(datetime.second(), recovered.second());
573 assert_eq!(datetime.nanosecond(), recovered.nanosecond());
574 }
575 }
576
577 #[test]
578 fn test_datetime_components() {
579 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
580
581 assert_eq!(datetime.year(), 2024);
582 assert_eq!(datetime.month(), 3);
583 assert_eq!(datetime.day(), 15);
584 assert_eq!(datetime.hour(), 14);
585 assert_eq!(datetime.minute(), 30);
586 assert_eq!(datetime.second(), 45);
587 assert_eq!(datetime.nanosecond(), 123456789);
588 }
589
590 #[test]
591 fn test_serde_roundtrip() {
592 let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
593 let json = to_string(&datetime).unwrap();
594 assert_eq!(json, datetime.to_nanos().to_string());
596
597 let recovered: DateTime = from_str(&json).unwrap();
598 assert_eq!(datetime, recovered);
599 }
600
601 #[test]
602 fn test_serde_postcard_roundtrip_preserves_all_components() {
603 for (y, mo, d, h, mi, s, n) in [
606 (1970u32 as i32, 1u32, 1u32, 0u32, 0u32, 0u32, 0u32),
607 (2024, 3, 15, 14, 30, 45, 123456789),
608 (1999, 12, 31, 23, 59, 59, 999999999),
609 (2024, 3, 15, 14, 30, 45, 1),
610 ] {
611 let dt = DateTime::new(y, mo, d, h, mi, s, n).unwrap();
612 let bytes = to_allocvec(&dt).unwrap();
613 let recovered: DateTime = from_bytes(&bytes).unwrap();
614 assert_eq!(dt, recovered);
615 assert_eq!(recovered.year(), y);
616 assert_eq!(recovered.month(), mo);
617 assert_eq!(recovered.day(), d);
618 assert_eq!(recovered.hour(), h);
619 assert_eq!(recovered.minute(), mi);
620 assert_eq!(recovered.second(), s);
621 assert_eq!(recovered.nanosecond(), n);
622 }
623 }
624
625 fn assert_datetime_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
626 let err = result.expect_err("expected DateTimeOverflow error");
627 match *err {
628 TypeError::Temporal {
629 kind: TemporalKind::DateTimeOverflow {
630 ..
631 },
632 ..
633 } => {}
634 other => panic!("expected DateTimeOverflow, got: {:?}", other),
635 }
636 }
637
638 #[test]
639 fn test_from_timestamp_nanos_overflow() {
640 let huge: u128 = u64::MAX as u128 + 1;
641 assert_datetime_overflow(DateTime::from_timestamp_nanos(huge));
642 }
643
644 #[test]
645 fn test_from_timestamp_nanos_max_u64_ok() {
646 let dt = DateTime::from_timestamp_nanos(u64::MAX as u128).unwrap();
647 assert_eq!(dt.to_nanos(), u64::MAX);
648 }
649
650 #[test]
651 fn test_from_timestamp_large_value_overflow() {
652 assert_datetime_overflow(DateTime::from_timestamp(i64::MAX));
653 }
654
655 #[test]
656 fn test_from_timestamp_negative_overflow() {
657 assert_datetime_overflow(DateTime::from_timestamp(-1));
658 }
659
660 #[test]
661 fn test_from_timestamp_millis_overflow() {
662 assert_datetime_overflow(DateTime::from_timestamp_millis(u64::MAX));
663 }
664
665 #[test]
666 fn test_from_timestamp_millis_boundary_ok() {
667 let dt = DateTime::from_timestamp_millis(1_700_000_000_000).unwrap();
668 assert!(dt.to_nanos() > 0);
669 }
670
671 #[test]
672 fn test_timestamp_nanos_large_value_returns_err() {
673 let dt = DateTime::from_nanos(i64::MAX as u64 + 1);
674 assert_datetime_overflow(dt.timestamp_nanos());
675 }
676
677 #[test]
678 fn test_timestamp_nanos_within_range_ok() {
679 let dt = DateTime::from_nanos(i64::MAX as u64);
680 assert_eq!(dt.timestamp_nanos().unwrap(), i64::MAX);
681 }
682
683 #[test]
684 fn test_try_date_max_nanos_ok() {
685 let dt = DateTime::from_nanos(u64::MAX);
687 let date = dt.try_date().unwrap();
688 assert!(date.year() > 2500);
689 }
690
691 #[test]
692 fn test_add_duration_overflow() {
693 let dt = DateTime::from_nanos(u64::MAX - 1);
694 let dur = Duration::from_days(1).unwrap();
695 assert_datetime_overflow(dt.add_duration(&dur));
696 }
697
698 #[test]
699 fn test_add_duration_before_epoch() {
700 let dt = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
701 let dur = Duration::from_seconds(-1).unwrap();
702 assert_datetime_overflow(dt.add_duration(&dur));
703 }
704
705 #[test]
706 fn test_add_duration_negative_nanos_borrows_from_days() {
707 let dt = DateTime::new(2024, 3, 15, 0, 0, 30, 0).unwrap();
708 let dur = Duration::from_seconds(-60).unwrap();
709 let result = dt.add_duration(&dur).unwrap();
710 assert_eq!(result.year(), 2024);
711 assert_eq!(result.month(), 3);
712 assert_eq!(result.day(), 14);
713 assert_eq!(result.hour(), 23);
714 assert_eq!(result.minute(), 59);
715 assert_eq!(result.second(), 30);
716 }
717
718 #[test]
719 fn test_add_duration_nanos_overflow_into_next_day() {
720 let dt = DateTime::new(2024, 3, 15, 23, 59, 30, 0).unwrap();
721 let dur = Duration::from_seconds(60).unwrap();
722 let result = dt.add_duration(&dur).unwrap();
723 assert_eq!(result.year(), 2024);
724 assert_eq!(result.month(), 3);
725 assert_eq!(result.day(), 16);
726 assert_eq!(result.hour(), 0);
727 assert_eq!(result.minute(), 0);
728 assert_eq!(result.second(), 30);
729 }
730
731 #[test]
732 fn add_and_sub_duration_operators() {
733 let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
734 let minute = Duration::from_seconds(60).unwrap();
735 assert_eq!(dt + minute, DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 25).unwrap());
736 assert_eq!(dt - minute, DateTime::from_ymd_hms(2024, 1, 15, 10, 29, 25).unwrap());
737 }
738
739 #[test]
740 fn sub_datetime_yields_duration() {
741 let a = DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 0).unwrap();
742 let b = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap();
743 assert_eq!(a - b, Duration::from_seconds(60).unwrap());
744 }
745
746 #[test]
747 fn rem_duration_aligns_to_window_boundary() {
748 let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
749 let minute = Duration::from_seconds(60).unwrap();
750 assert_eq!(dt % minute, Duration::from_seconds(25).unwrap());
752 assert_eq!(dt - (dt % minute), DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap());
754
755 let second = Duration::from_seconds(1).unwrap();
757 assert_eq!(dt % second, Duration::from_seconds(0).unwrap());
758 }
759
760 #[test]
761 fn saturating_sub_below_epoch_clamps_to_epoch() {
762 let epoch = DateTime::from_nanos(0);
765 assert_eq!(epoch.saturating_sub(Duration::from_seconds(1).unwrap()), epoch);
766
767 let early = DateTime::from_timestamp(5).unwrap();
768 assert_eq!(early.saturating_sub(Duration::from_seconds(10_000).unwrap()), epoch);
769 }
770
771 #[test]
772 fn checked_sub_returns_none_when_window_has_not_elapsed() {
773 let now = DateTime::from_timestamp_millis(1_000).unwrap();
778 assert_eq!(now.checked_sub(Duration::from_seconds(3).unwrap()), None);
779 assert_eq!(DateTime::from_nanos(0).checked_sub(Duration::from_seconds(1).unwrap()), None);
780 }
781
782 #[test]
783 fn checked_sub_matches_subtraction_when_in_range() {
784 let now = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
785 let minute = Duration::from_seconds(60).unwrap();
786 assert_eq!(now.checked_sub(minute), Some(now - minute));
787 }
788
789 #[test]
790 fn saturating_add_above_max_clamps_to_max() {
791 let near_max = DateTime::from_nanos(u64::MAX - 1);
793 assert_eq!(near_max.saturating_add(Duration::from_days(1).unwrap()), DateTime::from_nanos(u64::MAX));
794 }
795
796 #[test]
797 fn saturating_add_sub_match_operators_in_range() {
798 let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
800 let minute = Duration::from_seconds(60).unwrap();
801 assert_eq!(dt.saturating_add(minute), dt + minute);
802 assert_eq!(dt.saturating_sub(minute), dt - minute);
803 }
804
805 #[test]
806 fn saturating_duration_since_normal_and_clamped() {
807 let a = DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 0).unwrap();
808 let b = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap();
809 assert_eq!(a.saturating_duration_since(b), Duration::from_seconds(60).unwrap());
810 assert_eq!(b.saturating_duration_since(a), Duration::from_seconds(-60).unwrap());
812 let clamped = DateTime::from_nanos(u64::MAX).saturating_duration_since(DateTime::from_nanos(0));
814 assert_eq!(clamped.as_nanos().unwrap(), i64::MAX);
815 }
816}
817
818#[cfg(test)]
819mod now_tests {
820 use super::DateTime;
821 use crate::clock::testing::TestClock;
822
823 #[test]
824 fn now_reads_the_clock() {
825 let clock = TestClock::from_millis(1500);
827 assert_eq!(DateTime::now(&clock), DateTime::from_nanos(1_500_000_000));
828 }
829
830 #[test]
831 fn from_str_round_trips_display() {
832 let dt = DateTime::from_nanos(1_700_000_000_000_000_000);
833 let parsed: DateTime = dt.to_string().parse().unwrap();
834 assert_eq!(parsed, dt);
835 }
836
837 #[test]
838 fn from_str_rejects_garbage() {
839 assert!("not a datetime".parse::<DateTime>().is_err());
840 }
841}