Skip to main content

reifydb_value/value/
datetime.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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	error::{Error, TemporalKind, TypeError},
17	fragment::Fragment,
18	value::{date::Date, duration::Duration, temporal::parse::datetime::parse_datetime, time::Time},
19};
20
21const NANOS_PER_SECOND: i64 = 1_000_000_000;
22const NANOS_PER_MILLI: i64 = 1_000_000;
23const NANOS_PER_MICRO: i64 = 1_000;
24const NANOS_PER_DAY: i64 = 86_400 * NANOS_PER_SECOND;
25const ORDER_SIGN_BIT: u64 = 1 << 63;
26
27pub static CREATED_AT_COLUMN_NAME: &str = "created_at";
28pub static UPDATED_AT_COLUMN_NAME: &str = "updated_at";
29pub static TIME_COLUMN_NAME: &str = "time";
30
31#[repr(transparent)]
32#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
33pub struct DateTime {
34	nanos: i64,
35}
36
37impl DateTime {
38	pub fn new(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
39		let date = Date::new(year, month, day)?;
40		let time = Time::new(hour, min, sec, nano)?;
41
42		let days = date.to_days_since_epoch();
43		let nanos = days as i128 * NANOS_PER_DAY as i128 + time.to_nanos_since_midnight() as i128;
44		Some(Self {
45			nanos: i64::try_from(nanos).ok()?,
46		})
47	}
48
49	pub fn from_ymd_hms(
50		year: i32,
51		month: u32,
52		day: u32,
53		hour: u32,
54		min: u32,
55		sec: u32,
56	) -> Result<Self, Box<TypeError>> {
57		Self::new(year, month, day, hour, min, sec, 0).ok_or_else(|| {
58			Box::new(Self::overflow_err(format!(
59				"invalid datetime: {}-{:02}-{:02} {:02}:{:02}:{:02}",
60				year, month, day, hour, min, sec
61			)))
62		})
63	}
64
65	fn overflow_err(message: impl Into<String>) -> TypeError {
66		TypeError::Temporal {
67			kind: TemporalKind::DateTimeOverflow {
68				message: message.into(),
69			},
70			message: "datetime overflow".to_string(),
71			fragment: Fragment::None,
72		}
73	}
74
75	pub fn to_order(&self) -> u64 {
76		(self.nanos as u64) ^ ORDER_SIGN_BIT
77	}
78
79	pub fn from_order(order: u64) -> Self {
80		Self {
81			nanos: (order ^ ORDER_SIGN_BIT) as i64,
82		}
83	}
84
85	pub fn from_nanos(nanos: i64) -> Self {
86		Self {
87			nanos,
88		}
89	}
90
91	pub fn to_nanos(&self) -> i64 {
92		self.nanos
93	}
94
95	pub fn from_epoch_secs(secs: i64) -> Result<Self, Box<TypeError>> {
96		let nanos = secs.checked_mul(NANOS_PER_SECOND).ok_or_else(|| {
97			Box::new(Self::overflow_err(format!("{} seconds overflows DateTime range", secs)))
98		})?;
99		Ok(Self {
100			nanos,
101		})
102	}
103
104	pub fn from_epoch_millis(millis: i64) -> Result<Self, Box<TypeError>> {
105		let nanos = millis.checked_mul(NANOS_PER_MILLI).ok_or_else(|| {
106			Box::new(Self::overflow_err(format!("{} milliseconds overflows DateTime range", millis)))
107		})?;
108		Ok(Self {
109			nanos,
110		})
111	}
112
113	pub fn to_epoch_secs(&self) -> i64 {
114		self.nanos.div_euclid(NANOS_PER_SECOND)
115	}
116
117	pub fn to_epoch_millis(&self) -> i64 {
118		self.nanos.div_euclid(NANOS_PER_MILLI)
119	}
120
121	pub fn try_date(&self) -> Result<Date, Box<TypeError>> {
122		let days = self.nanos.div_euclid(NANOS_PER_DAY);
123		let days = i32::try_from(days)
124			.map_err(|_| Box::new(Self::overflow_err("DateTime overflows Date range")))?;
125		Date::from_days_since_epoch(days)
126			.ok_or_else(|| Box::new(Self::overflow_err("DateTime overflows Date range")))
127	}
128
129	pub fn date(&self) -> Date {
130		self.try_date().expect("DateTime overflows Date range")
131	}
132
133	pub fn time(&self) -> Time {
134		Time::from_nanos_since_midnight(self.nanos.rem_euclid(NANOS_PER_DAY) as u64).unwrap()
135	}
136
137	pub fn year(&self) -> i32 {
138		self.date().year()
139	}
140
141	pub fn month(&self) -> u32 {
142		self.date().month()
143	}
144
145	pub fn day(&self) -> u32 {
146		self.date().day()
147	}
148
149	pub fn hour(&self) -> u32 {
150		self.time().hour()
151	}
152
153	pub fn minute(&self) -> u32 {
154		self.time().minute()
155	}
156
157	pub fn second(&self) -> u32 {
158		self.time().second()
159	}
160
161	pub fn nanosecond(&self) -> u32 {
162		self.time().nanosecond()
163	}
164
165	pub fn add_duration(&self, dur: &Duration) -> Result<Self, Box<TypeError>> {
166		let date = self.date();
167		let time = self.time();
168		let mut year = date.year();
169		let mut month = date.month() as i32;
170		let mut day = date.day();
171
172		let total_months = month + dur.get_months();
173		year += (total_months - 1).div_euclid(12);
174		month = (total_months - 1).rem_euclid(12) + 1;
175
176		let max_day = Date::days_in_month(year, month as u32);
177		if day > max_day {
178			day = max_day;
179		}
180
181		let base_date = Date::new(year, month as u32, day).ok_or_else(|| {
182			Box::new(Self::overflow_err(format!(
183				"invalid datetime after adding duration: {}-{:02}-{:02}",
184				year, month, day
185			)))
186		})?;
187		let base_days = base_date.to_days_since_epoch() as i64 + dur.get_days() as i64;
188		let time_nanos = time.to_nanos_since_midnight() as i64 + dur.get_nanos();
189
190		let total_nanos = base_days as i128 * 86_400_000_000_000i128 + time_nanos as i128;
191
192		let nanos = i64::try_from(total_nanos)
193			.map_err(|_| Box::new(Self::overflow_err("the result overflows DateTime range")))?;
194		Ok(Self {
195			nanos,
196		})
197	}
198}
199
200impl DateTime {
201	pub fn saturating_add(self, rhs: Duration) -> DateTime {
202		let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
203			i64::MIN
204		} else {
205			i64::MAX
206		});
207		let nanos = (self.to_nanos() as i128 + total as i128).clamp(i64::MIN as i128, i64::MAX as i128);
208		DateTime::from_nanos(nanos as i64)
209	}
210
211	pub fn saturating_sub(self, rhs: Duration) -> DateTime {
212		let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
213			i64::MIN
214		} else {
215			i64::MAX
216		});
217		let nanos = (self.to_nanos() as i128 - total as i128).clamp(i64::MIN as i128, i64::MAX as i128);
218		DateTime::from_nanos(nanos as i64)
219	}
220
221	pub fn checked_add(self, rhs: Duration) -> Option<DateTime> {
222		let total = rhs.as_nanos().ok()?;
223		let nanos = self.to_nanos() as i128 + total as i128;
224		i64::try_from(nanos).ok().map(DateTime::from_nanos)
225	}
226
227	pub fn checked_sub(self, rhs: Duration) -> Option<DateTime> {
228		let total = rhs.as_nanos().ok()?;
229		let nanos = self.to_nanos() as i128 - total as i128;
230		i64::try_from(nanos).ok().map(DateTime::from_nanos)
231	}
232
233	pub fn saturating_duration_since(self, earlier: DateTime) -> Duration {
234		let diff = (self.to_nanos() as i128 - earlier.to_nanos() as i128)
235			.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
236		Duration::from_nanoseconds(diff).unwrap_or_else(|_| Duration::zero())
237	}
238}
239
240impl DateTime {
241	pub const ALIGNMENT: usize = 8;
242
243	pub const EPOCH: DateTime = DateTime {
244		nanos: 0,
245	};
246
247	pub const MIN: DateTime = DateTime {
248		nanos: i64::MIN,
249	};
250
251	pub const MAX: DateTime = DateTime {
252		nanos: i64::MAX,
253	};
254
255	pub fn is_epoch(&self) -> bool {
256		self.nanos == 0
257	}
258
259	pub fn from_millis(millis: i64) -> Self {
260		Self {
261			nanos: millis.saturating_mul(NANOS_PER_MILLI),
262		}
263	}
264
265	pub fn to_millis(&self) -> i64 {
266		self.nanos.div_euclid(NANOS_PER_MILLI)
267	}
268
269	pub fn to_micros(&self) -> i64 {
270		self.nanos.div_euclid(NANOS_PER_MICRO)
271	}
272
273	pub fn to_secs(&self) -> i64 {
274		self.nanos.div_euclid(NANOS_PER_SECOND)
275	}
276
277	pub fn saturating_add_millis(self, millis: u64) -> DateTime {
278		let delta = i64::try_from(millis).unwrap_or(i64::MAX).saturating_mul(NANOS_PER_MILLI);
279		DateTime::from_nanos(self.nanos.saturating_add(delta))
280	}
281
282	pub fn saturating_sub_millis(self, millis: u64) -> DateTime {
283		let delta = i64::try_from(millis).unwrap_or(i64::MAX).saturating_mul(NANOS_PER_MILLI);
284		DateTime::from_nanos(self.nanos.saturating_sub(delta))
285	}
286
287	pub fn floor_to_millis(self, millis: u64) -> DateTime {
288		let width = i64::try_from(millis).unwrap_or(i64::MAX).saturating_mul(NANOS_PER_MILLI);
289		if width == 0 {
290			return self;
291		}
292		DateTime::from_nanos(self.nanos.saturating_sub(self.nanos.rem_euclid(width)))
293	}
294}
295
296impl Add<Duration> for DateTime {
297	type Output = DateTime;
298
299	#[inline]
300	fn add(self, rhs: Duration) -> DateTime {
301		let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
302		let nanos = self.to_nanos() as i128 + total as i128;
303		DateTime::from_nanos(i64::try_from(nanos).expect("datetime addition out of range"))
304	}
305}
306
307impl Sub<Duration> for DateTime {
308	type Output = DateTime;
309
310	#[inline]
311	fn sub(self, rhs: Duration) -> DateTime {
312		let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
313		let nanos = self.to_nanos() as i128 - total as i128;
314		DateTime::from_nanos(i64::try_from(nanos).expect("datetime subtraction out of range"))
315	}
316}
317
318impl Sub<DateTime> for DateTime {
319	type Output = Duration;
320
321	#[inline]
322	fn sub(self, rhs: DateTime) -> Duration {
323		let diff = self.to_nanos() as i128 - rhs.to_nanos() as i128;
324		Duration::from_nanoseconds(i64::try_from(diff).expect("datetime difference exceeds i64 nanoseconds"))
325			.expect("datetime difference out of duration range")
326	}
327}
328
329impl Rem<Duration> for DateTime {
330	type Output = Duration;
331
332	#[inline]
333	fn rem(self, rhs: Duration) -> Duration {
334		let total = rhs.as_nanos().expect("duration exceeds i64 nanoseconds");
335		let total = u64::try_from(total).expect("duration must be positive for windowing");
336		Duration::from_nanoseconds(self.nanos.rem_euclid(total as i64))
337			.expect("datetime remainder out of duration range")
338	}
339}
340
341impl Display for DateTime {
342	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
343		let date = self.date();
344		let time = self.time();
345
346		write!(f, "{}T{}Z", date, time)
347	}
348}
349
350impl Serialize for DateTime {
351	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
352	where
353		S: Serializer,
354	{
355		serializer.serialize_i64(self.nanos)
356	}
357}
358
359struct DateTimeVisitor;
360
361impl<'de> Visitor<'de> for DateTimeVisitor {
362	type Value = DateTime;
363
364	fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
365		formatter.write_str("a datetime as signed nanoseconds since the Unix epoch (i64)")
366	}
367
368	fn visit_i64<E>(self, value: i64) -> Result<DateTime, E>
369	where
370		E: de::Error,
371	{
372		Ok(DateTime::from_nanos(value))
373	}
374
375	fn visit_u64<E>(self, value: u64) -> Result<DateTime, E>
376	where
377		E: de::Error,
378	{
379		i64::try_from(value).map(DateTime::from_nanos).map_err(|_| E::custom("datetime out of range"))
380	}
381}
382
383impl<'de> Deserialize<'de> for DateTime {
384	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385	where
386		D: Deserializer<'de>,
387	{
388		deserializer.deserialize_i64(DateTimeVisitor)
389	}
390}
391
392impl FromStr for DateTime {
393	type Err = Error;
394
395	fn from_str(s: &str) -> Result<Self, Self::Err> {
396		parse_datetime(Fragment::internal(s.trim()))
397	}
398}
399
400#[cfg(test)]
401pub mod tests {
402	use std::fmt::Debug;
403
404	use postcard::{from_bytes, to_allocvec};
405	use serde_json::{from_str, to_string};
406
407	use crate::{
408		error::{TemporalKind, TypeError},
409		value::{datetime::DateTime, duration::Duration},
410	};
411
412	#[test]
413	fn test_datetime_display_standard_format() {
414		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
415		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
416
417		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
418		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
419
420		let datetime = DateTime::new(1999, 12, 31, 23, 59, 59, 999999999).unwrap();
421		assert_eq!(format!("{}", datetime), "1999-12-31T23:59:59.999999999Z");
422	}
423
424	#[test]
425	fn test_datetime_display_millisecond_precision() {
426		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123000000).unwrap();
427		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123000000Z");
428
429		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 1000000).unwrap();
430		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.001000000Z");
431
432		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999000000).unwrap();
433		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999000000Z");
434	}
435
436	#[test]
437	fn test_datetime_display_microsecond_precision() {
438		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456000).unwrap();
439		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456000Z");
440
441		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 1000).unwrap();
442		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000001000Z");
443
444		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999000).unwrap();
445		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999000Z");
446	}
447
448	#[test]
449	fn test_datetime_display_nanosecond_precision() {
450		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
451		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
452
453		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 1).unwrap();
454		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000001Z");
455
456		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999999).unwrap();
457		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999999Z");
458	}
459
460	#[test]
461	fn test_datetime_display_zero_fractional_seconds() {
462		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 0).unwrap();
463		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000000Z");
464
465		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
466		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
467	}
468
469	#[test]
470	fn test_datetime_display_edge_times() {
471		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
472		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
473
474		let datetime = DateTime::new(2024, 3, 15, 23, 59, 59, 999999999).unwrap();
475		assert_eq!(format!("{}", datetime), "2024-03-15T23:59:59.999999999Z");
476
477		let datetime = DateTime::new(2024, 3, 15, 12, 0, 0, 0).unwrap();
478		assert_eq!(format!("{}", datetime), "2024-03-15T12:00:00.000000000Z");
479	}
480
481	#[test]
482	fn test_datetime_display_unix_epoch() {
483		let datetime = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
484		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
485
486		let datetime = DateTime::new(1970, 1, 1, 0, 0, 1, 0).unwrap();
487		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:01.000000000Z");
488	}
489
490	#[test]
491	fn test_datetime_display_leap_year() {
492		let datetime = DateTime::new(2024, 2, 29, 12, 30, 45, 123456789).unwrap();
493		assert_eq!(format!("{}", datetime), "2024-02-29T12:30:45.123456789Z");
494
495		let datetime = DateTime::new(2000, 2, 29, 0, 0, 0, 0).unwrap();
496		assert_eq!(format!("{}", datetime), "2000-02-29T00:00:00.000000000Z");
497	}
498
499	#[test]
500	fn test_datetime_display_boundary_dates() {
501		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
502		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
503
504		let datetime = DateTime::new(2100, 1, 1, 0, 0, 0, 0).unwrap();
505		assert_eq!(format!("{}", datetime), "2100-01-01T00:00:00.000000000Z");
506
507		let datetime = DateTime::new(2262, 4, 11, 23, 47, 16, 854775807).unwrap();
508		assert_eq!(format!("{}", datetime), "2262-04-11T23:47:16.854775807Z");
509		assert_eq!(datetime, DateTime::MAX);
510
511		assert!(DateTime::new(2262, 4, 11, 23, 47, 16, 854775808).is_none());
512		assert!(DateTime::new(9999, 12, 31, 23, 59, 59, 999999999).is_none());
513	}
514
515	#[test]
516	fn test_datetime_accepts_pre_epoch_down_to_min() {
517		assert_eq!(DateTime::new(1969, 12, 31, 23, 59, 59, 999999999), Some(DateTime::from_nanos(-1)));
518
519		assert!(DateTime::new(1900, 1, 1, 0, 0, 0, 0).is_some());
520
521		assert_eq!(DateTime::new(1677, 9, 21, 0, 12, 43, 145224192), Some(DateTime::MIN));
522		assert!(DateTime::new(1677, 9, 21, 0, 12, 43, 145224191).is_none());
523
524		assert_eq!(DateTime::from_epoch_secs(-1).unwrap().to_nanos(), -1_000_000_000);
525	}
526
527	#[test]
528	fn test_datetime_display_default() {
529		let datetime = DateTime::default();
530		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
531	}
532
533	#[test]
534	fn test_datetime_display_all_hours() {
535		for hour in 0..24 {
536			let datetime = DateTime::new(2024, 3, 15, hour, 30, 45, 123456789).unwrap();
537			let expected = format!("2024-03-15T{:02}:30:45.123456789Z", hour);
538			assert_eq!(format!("{}", datetime), expected);
539		}
540	}
541
542	#[test]
543	fn test_datetime_display_all_minutes() {
544		for minute in 0..60 {
545			let datetime = DateTime::new(2024, 3, 15, 14, minute, 45, 123456789).unwrap();
546			let expected = format!("2024-03-15T14:{:02}:45.123456789Z", minute);
547			assert_eq!(format!("{}", datetime), expected);
548		}
549	}
550
551	#[test]
552	fn test_datetime_display_all_seconds() {
553		for second in 0..60 {
554			let datetime = DateTime::new(2024, 3, 15, 14, 30, second, 123456789).unwrap();
555			let expected = format!("2024-03-15T14:30:{:02}.123456789Z", second);
556			assert_eq!(format!("{}", datetime), expected);
557		}
558	}
559
560	#[test]
561	fn test_datetime_display_from_epoch_secs() {
562		let datetime = DateTime::from_epoch_secs(0).unwrap();
563		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
564
565		let datetime = DateTime::from_epoch_secs(1234567890).unwrap();
566		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.000000000Z");
567	}
568
569	#[test]
570	fn test_datetime_display_from_epoch_millis() {
571		let datetime = DateTime::from_epoch_millis(1234567890123).unwrap();
572		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.123000000Z");
573
574		let datetime = DateTime::from_epoch_millis(0).unwrap();
575		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
576	}
577
578	#[test]
579	fn test_datetime_bits_roundtrip_preserves_every_component() {
580		// every key encoding and the serde impl go through to_order, so a lossy leg moves stored instants
581		// unseen
582		let cases = [
583			DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap(),
584			DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap(),
585			DateTime::new(2000, 2, 29, 23, 59, 59, 999999999).unwrap(),
586			DateTime::MAX,
587			DateTime::MIN,
588			DateTime::from_nanos(-1),
589		];
590
591		for datetime in cases {
592			let recovered = DateTime::from_order(datetime.to_order());
593
594			assert_eq!(datetime, recovered);
595			assert_eq!(datetime.nanosecond(), recovered.nanosecond(), "sub-second precision must survive");
596		}
597	}
598
599	#[test]
600	fn test_datetime_bits_are_monotonic_in_instant_order() {
601		// key encodings sort on the raw bits, so a disagreeing order would fire timers out of sequence
602		let ordered = [
603			DateTime::MIN,
604			DateTime::from_nanos(-1),
605			DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap(),
606			DateTime::new(1970, 1, 1, 0, 0, 0, 1).unwrap(),
607			DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap(),
608			DateTime::new(2024, 3, 15, 14, 30, 45, 123456790).unwrap(),
609			DateTime::MAX,
610		];
611
612		for pair in ordered.windows(2) {
613			let (lo, hi) = (pair[0], pair[1]);
614			assert!(lo < hi, "fixture must be ordered");
615			assert!(lo.to_order() < hi.to_order(), "bit order must follow instant order");
616		}
617	}
618
619	#[test]
620	fn test_datetime_from_nanos_roundtrip() {
621		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
622		let nanos = datetime.to_nanos();
623		let recovered = DateTime::from_nanos(nanos);
624		assert_eq!(datetime, recovered);
625	}
626
627	#[test]
628	fn test_datetime_roundtrip() {
629		let test_cases = [
630			(1970, 1, 1, 0, 0, 0, 0u32),
631			(2024, 3, 15, 14, 30, 45, 123456789),
632			(2000, 2, 29, 23, 59, 59, 999999999),
633		];
634
635		for (y, m, d, h, min, s, n) in test_cases {
636			let datetime = DateTime::new(y, m, d, h, min, s, n).unwrap();
637			let nanos = datetime.to_nanos();
638			let recovered = DateTime::from_nanos(nanos);
639
640			assert_eq!(datetime.year(), recovered.year());
641			assert_eq!(datetime.month(), recovered.month());
642			assert_eq!(datetime.day(), recovered.day());
643			assert_eq!(datetime.hour(), recovered.hour());
644			assert_eq!(datetime.minute(), recovered.minute());
645			assert_eq!(datetime.second(), recovered.second());
646			assert_eq!(datetime.nanosecond(), recovered.nanosecond());
647		}
648	}
649
650	#[test]
651	fn test_datetime_components() {
652		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
653
654		assert_eq!(datetime.year(), 2024);
655		assert_eq!(datetime.month(), 3);
656		assert_eq!(datetime.day(), 15);
657		assert_eq!(datetime.hour(), 14);
658		assert_eq!(datetime.minute(), 30);
659		assert_eq!(datetime.second(), 45);
660		assert_eq!(datetime.nanosecond(), 123456789);
661	}
662
663	#[test]
664	fn test_serde_roundtrip() {
665		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
666		let json = to_string(&datetime).unwrap();
667		// Wire format is the raw nanos-since-epoch integer, not an ISO-8601 string.
668		assert_eq!(json, datetime.to_nanos().to_string());
669
670		let recovered: DateTime = from_str(&json).unwrap();
671		assert_eq!(datetime, recovered);
672
673		let pre_epoch = DateTime::new(1900, 6, 15, 12, 34, 56, 789000000).unwrap();
674		let json = to_string(&pre_epoch).unwrap();
675		assert_eq!(json, pre_epoch.to_nanos().to_string());
676
677		let recovered: DateTime = from_str(&json).unwrap();
678		assert_eq!(pre_epoch, recovered);
679	}
680
681	#[test]
682	fn test_serde_postcard_roundtrip_preserves_all_components() {
683		// Postcard is the CDC wire format; sub-second nanos must survive it or consumers
684		// reconstruct the wrong instant.
685		for (y, mo, d, h, mi, s, n) in [
686			(1970u32 as i32, 1u32, 1u32, 0u32, 0u32, 0u32, 0u32),
687			(2024, 3, 15, 14, 30, 45, 123456789),
688			(1999, 12, 31, 23, 59, 59, 999999999),
689			(2024, 3, 15, 14, 30, 45, 1),
690			(1900, 6, 15, 12, 34, 56, 789000000),
691		] {
692			let dt = DateTime::new(y, mo, d, h, mi, s, n).unwrap();
693			let bytes = to_allocvec(&dt).unwrap();
694			let recovered: DateTime = from_bytes(&bytes).unwrap();
695			assert_eq!(dt, recovered);
696			assert_eq!(recovered.year(), y);
697			assert_eq!(recovered.month(), mo);
698			assert_eq!(recovered.day(), d);
699			assert_eq!(recovered.hour(), h);
700			assert_eq!(recovered.minute(), mi);
701			assert_eq!(recovered.second(), s);
702			assert_eq!(recovered.nanosecond(), n);
703		}
704	}
705
706	fn assert_datetime_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
707		let err = result.expect_err("expected DateTimeOverflow error");
708		match *err {
709			TypeError::Temporal {
710				kind: TemporalKind::DateTimeOverflow {
711					..
712				},
713				..
714			} => {}
715			other => panic!("expected DateTimeOverflow, got: {:?}", other),
716		}
717	}
718
719	#[test]
720	fn test_from_epoch_secs_large_value_overflow() {
721		assert_datetime_overflow(DateTime::from_epoch_secs(i64::MAX));
722	}
723
724	#[test]
725	fn test_from_epoch_secs_negative_overflow() {
726		assert!(DateTime::from_epoch_secs(-1).is_ok());
727		assert_datetime_overflow(DateTime::from_epoch_secs(i64::MIN));
728	}
729
730	#[test]
731	fn test_from_epoch_millis_overflow() {
732		assert_datetime_overflow(DateTime::from_epoch_millis(i64::MAX));
733	}
734
735	#[test]
736	fn test_from_epoch_millis_boundary_ok() {
737		let dt = DateTime::from_epoch_millis(1_700_000_000_000).unwrap();
738		assert!(dt.to_nanos() > 0);
739	}
740
741	#[test]
742	fn test_try_date_max_nanos_ok() {
743		let date = DateTime::MAX.try_date().unwrap();
744		assert_eq!(date.year(), 2262);
745
746		let date = DateTime::MIN.try_date().unwrap();
747		assert_eq!(date.year(), 1677);
748	}
749
750	#[test]
751	fn test_add_duration_overflow() {
752		let dt = DateTime::from_nanos(i64::MAX - 1);
753		let dur = Duration::from_days(1).unwrap();
754		assert_datetime_overflow(dt.add_duration(&dur));
755	}
756
757	#[test]
758	fn test_add_duration_before_epoch() {
759		let epoch = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
760		let dur = Duration::from_seconds(-1).unwrap();
761		assert_eq!(epoch.add_duration(&dur).unwrap(), DateTime::from_nanos(-1_000_000_000));
762
763		assert_datetime_overflow(DateTime::MIN.add_duration(&dur));
764	}
765
766	#[test]
767	fn test_add_duration_negative_nanos_borrows_from_days() {
768		let dt = DateTime::new(2024, 3, 15, 0, 0, 30, 0).unwrap();
769		let dur = Duration::from_seconds(-60).unwrap();
770		let result = dt.add_duration(&dur).unwrap();
771		assert_eq!(result.year(), 2024);
772		assert_eq!(result.month(), 3);
773		assert_eq!(result.day(), 14);
774		assert_eq!(result.hour(), 23);
775		assert_eq!(result.minute(), 59);
776		assert_eq!(result.second(), 30);
777	}
778
779	#[test]
780	fn test_add_duration_nanos_overflow_into_next_day() {
781		let dt = DateTime::new(2024, 3, 15, 23, 59, 30, 0).unwrap();
782		let dur = Duration::from_seconds(60).unwrap();
783		let result = dt.add_duration(&dur).unwrap();
784		assert_eq!(result.year(), 2024);
785		assert_eq!(result.month(), 3);
786		assert_eq!(result.day(), 16);
787		assert_eq!(result.hour(), 0);
788		assert_eq!(result.minute(), 0);
789		assert_eq!(result.second(), 30);
790	}
791
792	#[test]
793	fn add_and_sub_duration_operators() {
794		let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
795		let minute = Duration::from_seconds(60).unwrap();
796		assert_eq!(dt + minute, DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 25).unwrap());
797		assert_eq!(dt - minute, DateTime::from_ymd_hms(2024, 1, 15, 10, 29, 25).unwrap());
798	}
799
800	#[test]
801	fn sub_datetime_yields_duration() {
802		let a = DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 0).unwrap();
803		let b = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap();
804		assert_eq!(a - b, Duration::from_seconds(60).unwrap());
805	}
806
807	#[test]
808	fn rem_duration_aligns_to_window_boundary() {
809		// Window bucket starts are computed as `coord - (coord % width)`.
810		let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
811		let minute = Duration::from_seconds(60).unwrap();
812		assert_eq!(dt % minute, Duration::from_seconds(25).unwrap());
813		assert_eq!(dt - (dt % minute), DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap());
814
815		let second = Duration::from_seconds(1).unwrap();
816		assert_eq!(dt % second, Duration::from_seconds(0).unwrap());
817	}
818
819	#[test]
820	fn saturating_sub_clamps_at_min() {
821		// A cutoff below DateTime::MIN must clamp to MIN, not overflow.
822		let epoch = DateTime::from_nanos(0);
823		assert_eq!(
824			epoch.saturating_sub(Duration::from_seconds(1).unwrap()),
825			DateTime::from_nanos(-1_000_000_000)
826		);
827
828		assert_eq!(DateTime::MIN.saturating_sub(Duration::from_seconds(1).unwrap()), DateTime::MIN);
829	}
830
831	#[test]
832	fn checked_sub_gives_a_pre_epoch_cutoff_and_none_only_below_min() {
833		// now=1s, ttl=3s gives a pre-epoch cutoff, not None; only DateTime::MIN has no representable cutoff.
834		let now = DateTime::from_epoch_secs(1).unwrap();
835		assert_eq!(
836			now.checked_sub(Duration::from_seconds(3).unwrap()),
837			Some(DateTime::from_nanos(-2_000_000_000))
838		);
839		assert_eq!(DateTime::MIN.checked_sub(Duration::from_seconds(1).unwrap()), None);
840	}
841
842	#[test]
843	fn checked_sub_matches_subtraction_when_in_range() {
844		let now = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
845		let minute = Duration::from_seconds(60).unwrap();
846		assert_eq!(now.checked_sub(minute), Some(now - minute));
847	}
848
849	#[test]
850	fn saturating_add_above_max_clamps_to_max() {
851		// Overflow past the representable range clamps to the max instant.
852		let near_max = DateTime::from_nanos(i64::MAX - 1);
853		assert_eq!(near_max.saturating_add(Duration::from_days(1).unwrap()), DateTime::from_nanos(i64::MAX));
854	}
855
856	#[test]
857	fn saturating_add_sub_match_operators_in_range() {
858		// In range, the saturating ops agree with the panicking +/- operators.
859		let dt = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
860		let minute = Duration::from_seconds(60).unwrap();
861		assert_eq!(dt.saturating_add(minute), dt + minute);
862		assert_eq!(dt.saturating_sub(minute), dt - minute);
863	}
864
865	#[test]
866	fn saturating_duration_since_normal_and_clamped() {
867		// A gap wider than i64 nanoseconds must clamp rather than panic; a reversed pair is a
868		// negative duration, not a clamp.
869		let a = DateTime::from_ymd_hms(2024, 1, 15, 10, 31, 0).unwrap();
870		let b = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 0).unwrap();
871		assert_eq!(a.saturating_duration_since(b), Duration::from_seconds(60).unwrap());
872		assert_eq!(b.saturating_duration_since(a), Duration::from_seconds(-60).unwrap());
873		let clamped = DateTime::MAX.saturating_duration_since(DateTime::MIN);
874		assert_eq!(clamped.as_nanos().unwrap(), i64::MAX);
875	}
876
877	#[test]
878	fn checked_add_matches_addition_when_in_range() {
879		let now = DateTime::from_ymd_hms(2024, 1, 15, 10, 30, 25).unwrap();
880		let minute = Duration::from_seconds(60).unwrap();
881		assert_eq!(now.checked_add(minute), Some(now + minute));
882	}
883
884	#[test]
885	fn checked_add_returns_none_past_the_representable_range() {
886		// An expiry past the end of the range must be None; wrapping yields a small instant
887		// that reads as already expired.
888		let near_max = DateTime::from_nanos(i64::MAX - 1);
889		assert_eq!(near_max.checked_add(Duration::from_days(1).unwrap()), None);
890	}
891
892	#[test]
893	fn checked_sub_of_a_negative_duration_cannot_wrap_past_the_range() {
894		// Subtracting a negative duration moves forward, so checked_sub must check the upper
895		// bound too.
896		let near_max = DateTime::from_nanos(i64::MAX - 1);
897		assert_eq!(near_max.checked_sub(Duration::from_days(-1).unwrap()), None);
898	}
899
900	#[test]
901	fn floor_to_millis_reproduces_the_millis_truncated_bucket_boundary() {
902		// THE test for the nanos migration. Bucket starts are computed today by truncating the
903		// instant to millis, taking the remainder against a millis-wide window, and multiplying
904		// back up. Doing the same arithmetic in nanos must land on the identical boundary, or a
905		// row near an edge silently changes bucket and every downstream aggregate moves with it.
906		// The instant deliberately carries sub-millisecond digits, which is where the two paths
907		// would diverge if the flooring were done in the wrong order.
908		// Mutation: round instead of floor, or truncate the instant to millis first, and the two
909		// paths disagree.
910		let nanos = 1_700_000_123_456_789i64;
911		let window_ms = 1_000u64;
912
913		let ts_ms = nanos / 1_000_000;
914		let legacy_bucket_start_nanos = (ts_ms - ts_ms % window_ms as i64) * 1_000_000;
915
916		assert_eq!(
917			DateTime::from_nanos(nanos).floor_to_millis(window_ms),
918			DateTime::from_nanos(legacy_bucket_start_nanos)
919		);
920	}
921
922	#[test]
923	fn floor_to_millis_agrees_with_the_rem_duration_operator() {
924		// floor_to_millis replaces `coord - (coord % width)` at the window call sites, so the
925		// two forms must agree exactly.
926		let dt = DateTime::from_nanos(1_700_000_123_456_789);
927		let width_ms = 60_000u64;
928		let width = Duration::from_milliseconds(width_ms as i64).unwrap();
929
930		assert_eq!(dt.floor_to_millis(width_ms), dt - (dt % width));
931	}
932
933	#[test]
934	fn floor_to_millis_keeps_a_boundary_instant_where_it_is() {
935		// An instant exactly on a boundary belongs to the bucket it opens, not the one before.
936		let width_ms = 1_000u64;
937		let boundary = DateTime::from_nanos(2_000_000_000);
938
939		assert_eq!(
940			DateTime::from_nanos(1_999_999_999).floor_to_millis(width_ms),
941			DateTime::from_nanos(1_000_000_000)
942		);
943		assert_eq!(boundary.floor_to_millis(width_ms), boundary);
944		assert_eq!(DateTime::from_nanos(2_000_000_001).floor_to_millis(width_ms), boundary);
945	}
946
947	#[test]
948	fn floor_to_millis_of_a_zero_width_grid_is_the_instant_itself() {
949		// Zero width is rejected where windows are defined; this helper must still stay total
950		// rather than dividing by zero inside a flow tick, and identity cannot fabricate a bucket.
951		let dt = DateTime::from_nanos(1_700_000_123_456_789);
952		assert_eq!(dt.floor_to_millis(0), dt);
953	}
954
955	#[test]
956	fn saturating_sub_millis_clamps_at_min() {
957		// A cutoff below DateTime::MIN must clamp to MIN, not wrap.
958		assert_eq!(DateTime::EPOCH.saturating_sub_millis(30_000), DateTime::from_nanos(-30_000_000_000));
959		assert_eq!(DateTime::MIN.saturating_sub_millis(1), DateTime::MIN);
960	}
961
962	#[test]
963	fn saturating_add_millis_clamps_at_the_maximum() {
964		// Wrapping addition would turn an expiry past the range into a past instant, which
965		// reads as already expired.
966		assert_eq!(DateTime::MAX.saturating_add_millis(1), DateTime::MAX);
967		assert_eq!(DateTime::from_nanos(1_000_000).saturating_add_millis(1), DateTime::from_nanos(2_000_000));
968	}
969
970	#[test]
971	fn millis_conversions_round_trip_and_truncate_in_one_direction_only() {
972		// Widening millis to nanos is lossless, narrowing truncates, so converting sites must
973		// converge on nanos - a round trip through millis drops sub-millisecond digits for good.
974		assert_eq!(DateTime::from_millis(1_500).to_millis(), 1_500);
975		assert_eq!(DateTime::from_millis(1_500), DateTime::from_nanos(1_500_000_000));
976
977		let precise = DateTime::from_nanos(1_500_000_999);
978		assert_eq!(precise.to_millis(), 1_500);
979		assert_eq!(DateTime::from_millis(precise.to_millis()), DateTime::from_nanos(1_500_000_000));
980	}
981
982	#[test]
983	fn coarser_unit_accessors_floor() {
984		// Must match `to_nanos() / N` truncation exactly; rounding shifts a boundary instant into the next
985		// unit.
986		let dt = DateTime::from_nanos(1_700_000_123_456_789);
987
988		assert_eq!(dt.to_secs(), 1_700_000);
989		assert_eq!(dt.to_micros(), 1_700_000_123_456);
990		assert_eq!(dt.to_millis(), 1_700_000_123);
991
992		assert_eq!(dt.to_secs(), dt.to_nanos() / 1_000_000_000);
993		assert_eq!(dt.to_micros(), dt.to_nanos() / 1_000);
994
995		assert_eq!(DateTime::from_nanos(1_999_999_999).to_secs(), 1);
996		assert_eq!(DateTime::from_nanos(1_999).to_micros(), 1);
997		assert_eq!(DateTime::EPOCH.to_secs(), 0);
998		assert_eq!(DateTime::EPOCH.to_micros(), 0);
999
1000		assert_eq!(DateTime::from_nanos(-1).to_secs(), -1);
1001		assert_eq!(DateTime::from_nanos(-1).to_millis(), -1);
1002		assert_eq!(DateTime::from_nanos(-1).to_micros(), -1);
1003	}
1004
1005	#[test]
1006	fn from_millis_saturates_where_from_epoch_millis_errors() {
1007		// from_millis is the infallible form callers want; an input large enough to overflow is
1008		// roughly 584 million years, so clamping is safe and saves an unwrap at every call site.
1009		assert_eq!(DateTime::from_millis(1_500), DateTime::from_epoch_millis(1_500).unwrap());
1010		assert!(DateTime::from_epoch_millis(i64::MAX).is_err());
1011		assert_eq!(DateTime::from_millis(i64::MAX), DateTime::MAX);
1012		assert_eq!(DateTime::from_millis(i64::MIN), DateTime::MIN);
1013	}
1014
1015	#[test]
1016	fn from_epoch_secs_reads_its_argument_as_whole_seconds() {
1017		// Chain timestamps arrive in seconds; a millis-scaled constructor would be off by 1000x silently.
1018		assert_eq!(DateTime::from_epoch_secs(1).unwrap().to_nanos(), 1_000_000_000);
1019		assert_eq!(DateTime::from_epoch_secs(0).unwrap(), DateTime::EPOCH);
1020		assert_eq!(
1021			DateTime::from_epoch_secs(1_234_567_890).unwrap(),
1022			DateTime::from_epoch_millis(1_234_567_890_000).unwrap()
1023		);
1024	}
1025
1026	#[test]
1027	fn the_epoch_constant_is_the_zero_instant() {
1028		// Watermarks hydrate to the epoch to mean "nothing seen yet".
1029		assert_eq!(DateTime::EPOCH, DateTime::from_nanos(0));
1030		assert_eq!(DateTime::EPOCH, DateTime::default());
1031		assert!(DateTime::EPOCH.is_epoch());
1032		assert!(!DateTime::from_nanos(1).is_epoch());
1033	}
1034
1035	#[test]
1036	fn a_pre_epoch_instant_round_trips_every_component() {
1037		// A negative nanos value must decompose back to its own date/time, not wrap to a bogus post-epoch one.
1038		let datetime = DateTime::new(1900, 6, 15, 12, 34, 56, 789000001).unwrap();
1039		assert!(datetime.to_nanos() < 0);
1040		assert_eq!(datetime.year(), 1900);
1041		assert_eq!(datetime.month(), 6);
1042		assert_eq!(datetime.day(), 15);
1043		assert_eq!(datetime.hour(), 12);
1044		assert_eq!(datetime.minute(), 34);
1045		assert_eq!(datetime.second(), 56);
1046		assert_eq!(datetime.nanosecond(), 789000001);
1047	}
1048
1049	#[test]
1050	fn min_and_max_display_the_representable_range_boundaries() {
1051		// The Display impl must agree with the range DateTime::new accepts, or an error message quoting MIN/MAX
1052		// lies.
1053		assert_eq!(format!("{}", DateTime::MIN), "1677-09-21T00:12:43.145224192Z");
1054		assert_eq!(format!("{}", DateTime::MAX), "2262-04-11T23:47:16.854775807Z");
1055	}
1056
1057	#[test]
1058	fn a_pre_epoch_instant_floors_and_remainders_toward_the_earlier_boundary() {
1059		// rem_euclid/div_euclid give floor semantics; truncating toward zero would put -1ns in the wrong
1060		// second.
1061		let dt = DateTime::from_nanos(-1);
1062		assert_eq!(dt % Duration::from_seconds(1).unwrap(), Duration::from_nanoseconds(999_999_999).unwrap());
1063		assert_eq!(dt.floor_to_millis(1_000), DateTime::from_nanos(-1_000_000_000));
1064	}
1065}
1066
1067#[cfg(test)]
1068mod now_tests {
1069	use super::DateTime;
1070	use crate::clock::{ClockNow, testing::TestClock};
1071
1072	#[test]
1073	fn now_reads_the_clock() {
1074		// "now" comes from the injected clock so tests stay deterministic.
1075		let clock = TestClock::from_millis(1500);
1076		assert_eq!(clock.now(), DateTime::from_nanos(1_500_000_000));
1077	}
1078
1079	#[test]
1080	fn from_str_round_trips_display() {
1081		let dt = DateTime::from_nanos(1_700_000_000_000_000_000);
1082		let parsed: DateTime = dt.to_string().parse().unwrap();
1083		assert_eq!(parsed, dt);
1084	}
1085
1086	#[test]
1087	fn from_str_rejects_garbage() {
1088		assert!("not a datetime".parse::<DateTime>().is_err());
1089	}
1090}