Skip to main content

reifydb_type/value/
datetime.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt::{Display, Formatter};
5
6use serde::{
7	Deserialize, Deserializer, Serialize, Serializer,
8	de::{self, Visitor},
9};
10
11use crate::value::{date::Date, time::Time};
12
13/// A date and time value with nanosecond precision.
14/// Always in SVTC timezone.
15///
16/// Internally stored as seconds and nanoseconds since Unix epoch.
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct DateTime {
19	// Seconds since Unix epoch (can be negative for dates before 1970)
20	seconds: i64,
21	// Nanosecond part (0 to 999_999_999)
22	nanos: u32,
23}
24
25impl Default for DateTime {
26	fn default() -> Self {
27		Self {
28			seconds: 0,
29			nanos: 0,
30		} // 1970-01-01T00:00:00.000000000Z
31	}
32}
33
34impl DateTime {
35	pub fn new(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
36		// Validate date
37		let date = Date::new(year, month, day)?;
38
39		// Validate time
40		let time = Time::new(hour, min, sec, nano)?;
41
42		// Convert date to seconds since epoch
43		let days = date.to_days_since_epoch() as i64;
44		let date_seconds = days * 86400;
45
46		// Convert time to seconds and nanos
47		let time_nanos = time.to_nanos_since_midnight();
48		let time_seconds = (time_nanos / 1_000_000_000) as i64;
49		let time_nano_part = (time_nanos % 1_000_000_000) as u32;
50
51		Some(Self {
52			seconds: date_seconds + time_seconds,
53			nanos: time_nano_part,
54		})
55	}
56
57	pub fn from_ymd_hms(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Result<Self, String> {
58		Self::new(year, month, day, hour, min, sec, 0).ok_or_else(|| {
59			format!("Invalid datetime: {}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec)
60		})
61	}
62
63	pub fn from_timestamp(timestamp: i64) -> Result<Self, String> {
64		Ok(Self {
65			seconds: timestamp,
66			nanos: 0,
67		})
68	}
69
70	pub fn from_timestamp_millis(millis: u64) -> Self {
71		let seconds = (millis / 1000) as i64;
72		let nanos = ((millis % 1000) * 1_000_000) as u32;
73		Self {
74			seconds,
75			nanos,
76		}
77	}
78
79	pub fn from_timestamp_nanos(nanos: u128) -> Self {
80		let seconds = (nanos / 1_000_000_000) as i64;
81		let nanos = (nanos % 1_000_000_000) as u32;
82
83		Self {
84			seconds,
85			nanos,
86		}
87	}
88
89	pub fn timestamp(&self) -> i64 {
90		self.seconds
91	}
92
93	pub fn timestamp_millis(&self) -> i64 {
94		self.seconds * 1000 + (self.nanos / 1_000_000) as i64
95	}
96
97	pub fn timestamp_nanos(&self) -> i64 {
98		self.seconds.saturating_mul(1_000_000_000).saturating_add(self.nanos as i64)
99	}
100
101	pub fn date(&self) -> Date {
102		// Convert seconds to days
103		let days = (self.seconds / 86400) as i32;
104		Date::from_days_since_epoch(days).unwrap()
105	}
106
107	pub fn time(&self) -> Time {
108		// Get the time portion of the day
109		let seconds_in_day = self.seconds % 86400;
110		let seconds_in_day = if seconds_in_day < 0 {
111			seconds_in_day + 86400
112		} else {
113			seconds_in_day
114		} as u64;
115
116		let nanos_in_day = seconds_in_day * 1_000_000_000 + self.nanos as u64;
117		Time::from_nanos_since_midnight(nanos_in_day).unwrap()
118	}
119
120	/// Convert to nanoseconds since Unix epoch for storage
121	pub fn to_nanos_since_epoch(&self) -> i64 {
122		self.timestamp_nanos()
123	}
124
125	/// Create from nanoseconds since Unix epoch for storage
126	pub fn from_nanos_since_epoch(nanos: i64) -> Self {
127		let seconds = nanos / 1_000_000_000;
128		let nano_part = nanos % 1_000_000_000;
129
130		// Handle negative nanoseconds
131		let (seconds, nanos) = if nanos < 0 && nano_part != 0 {
132			(seconds - 1, (1_000_000_000 - nano_part.abs()) as u32)
133		} else {
134			(seconds, nano_part.abs() as u32)
135		};
136
137		Self {
138			seconds,
139			nanos,
140		}
141	}
142
143	/// Create from separate seconds and nanoseconds
144	pub fn from_parts(seconds: i64, nanos: u32) -> Result<Self, String> {
145		if nanos >= 1_000_000_000 {
146			return Err(format!("Invalid nanoseconds: {} (must be < 1_000_000_000)", nanos));
147		}
148		Ok(Self {
149			seconds,
150			nanos,
151		})
152	}
153
154	/// Get separate seconds and nanoseconds for storage
155	pub fn to_parts(&self) -> (i64, u32) {
156		(self.seconds, self.nanos)
157	}
158
159	/// Get year component
160	pub fn year(&self) -> i32 {
161		self.date().year()
162	}
163
164	/// Get month component (1-12)
165	pub fn month(&self) -> u32 {
166		self.date().month()
167	}
168
169	/// Get day component (1-31)
170	pub fn day(&self) -> u32 {
171		self.date().day()
172	}
173
174	/// Get hour component (0-23)
175	pub fn hour(&self) -> u32 {
176		self.time().hour()
177	}
178
179	/// Get minute component (0-59)
180	pub fn minute(&self) -> u32 {
181		self.time().minute()
182	}
183
184	/// Get second component (0-59)
185	pub fn second(&self) -> u32 {
186		self.time().second()
187	}
188
189	/// Get nanosecond component (0-999_999_999)
190	pub fn nanosecond(&self) -> u32 {
191		self.time().nanosecond()
192	}
193}
194
195impl Display for DateTime {
196	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
197		let date = self.date();
198		let time = self.time();
199
200		// Format as ISO 8601: YYYY-MM-DDTHH:MM:SS.nnnnnnnnnZ
201		write!(f, "{}T{}Z", date, time)
202	}
203}
204
205// Serde implementation for ISO 8601 format
206impl Serialize for DateTime {
207	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208	where
209		S: Serializer,
210	{
211		serializer.serialize_str(&self.to_string())
212	}
213}
214
215struct DateTimeVisitor;
216
217impl<'de> Visitor<'de> for DateTimeVisitor {
218	type Value = DateTime;
219
220	fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
221		formatter.write_str("a datetime in ISO 8601 format (YYYY-MM-DDTHH:MM:SS[.nnnnnnnnn]Z)")
222	}
223
224	fn visit_str<E>(self, value: &str) -> Result<DateTime, E>
225	where
226		E: de::Error,
227	{
228		// Parse ISO 8601 datetime format:
229		// YYYY-MM-DDTHH:MM:SS[.nnnnnnnnn]Z Remove trailing Z if
230		// present
231		let value = value.strip_suffix('Z').unwrap_or(value);
232
233		// Split on T
234		let parts: Vec<&str> = value.split('T').collect();
235		if parts.len() != 2 {
236			return Err(E::custom(format!("invalid datetime format: {}", value)));
237		}
238
239		// Parse date part
240		let date_parts: Vec<&str> = parts[0].split('-').collect();
241		if date_parts.len() != 3 {
242			return Err(E::custom(format!("invalid date format: {}", parts[0])));
243		}
244
245		// Handle negative years
246		let (year_str, month_str, day_str) = if date_parts[0].is_empty() && date_parts.len() == 4 {
247			// Negative year case
248			(format!("-{}", date_parts[1]), date_parts[2], date_parts[3])
249		} else {
250			(date_parts[0].to_string(), date_parts[1], date_parts[2])
251		};
252
253		let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
254		let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
255		let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
256
257		// Parse time part
258		let (time_part, nano_part) = if let Some(dot_pos) = parts[1].find('.') {
259			(&parts[1][..dot_pos], Some(&parts[1][dot_pos + 1..]))
260		} else {
261			(parts[1], None)
262		};
263
264		let time_parts: Vec<&str> = time_part.split(':').collect();
265		if time_parts.len() != 3 {
266			return Err(E::custom(format!("invalid time format: {}", parts[1])));
267		}
268
269		let hour = time_parts[0]
270			.parse::<u32>()
271			.map_err(|_| E::custom(format!("invalid hour: {}", time_parts[0])))?;
272		let minute = time_parts[1]
273			.parse::<u32>()
274			.map_err(|_| E::custom(format!("invalid minute: {}", time_parts[1])))?;
275		let second = time_parts[2]
276			.parse::<u32>()
277			.map_err(|_| E::custom(format!("invalid second: {}", time_parts[2])))?;
278
279		let nano = if let Some(nano_str) = nano_part {
280			// Pad or truncate to 9 digits
281			let padded = if nano_str.len() < 9 {
282				format!("{:0<9}", nano_str)
283			} else {
284				nano_str[..9].to_string()
285			};
286			padded.parse::<u32>().map_err(|_| E::custom(format!("invalid nanoseconds: {}", nano_str)))?
287		} else {
288			0
289		};
290
291		DateTime::new(year, month, day, hour, minute, second, nano).ok_or_else(|| {
292			E::custom(format!(
293				"invalid datetime: {}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}Z",
294				year, month, day, hour, minute, second, nano
295			))
296		})
297	}
298}
299
300impl<'de> Deserialize<'de> for DateTime {
301	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
302	where
303		D: Deserializer<'de>,
304	{
305		deserializer.deserialize_str(DateTimeVisitor)
306	}
307}
308
309#[cfg(test)]
310pub mod tests {
311	use super::*;
312
313	#[test]
314	fn test_datetime_display_standard_format() {
315		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
316		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
317
318		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
319		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
320
321		let datetime = DateTime::new(1999, 12, 31, 23, 59, 59, 999999999).unwrap();
322		assert_eq!(format!("{}", datetime), "1999-12-31T23:59:59.999999999Z");
323	}
324
325	#[test]
326	fn test_datetime_display_millisecond_precision() {
327		// Test various millisecond values
328		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123000000).unwrap();
329		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123000000Z");
330
331		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 001000000).unwrap();
332		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.001000000Z");
333
334		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999000000).unwrap();
335		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999000000Z");
336	}
337
338	#[test]
339	fn test_datetime_display_microsecond_precision() {
340		// Test various microsecond values
341		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456000).unwrap();
342		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456000Z");
343
344		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000001000).unwrap();
345		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000001000Z");
346
347		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999000).unwrap();
348		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999000Z");
349	}
350
351	#[test]
352	fn test_datetime_display_nanosecond_precision() {
353		// Test various nanosecond values
354		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
355		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.123456789Z");
356
357		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 000000001).unwrap();
358		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000001Z");
359
360		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 999999999).unwrap();
361		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.999999999Z");
362	}
363
364	#[test]
365	fn test_datetime_display_zero_fractional_seconds() {
366		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 0).unwrap();
367		assert_eq!(format!("{}", datetime), "2024-03-15T14:30:45.000000000Z");
368
369		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
370		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
371	}
372
373	#[test]
374	fn test_datetime_display_edge_times() {
375		// Midnight
376		let datetime = DateTime::new(2024, 3, 15, 0, 0, 0, 0).unwrap();
377		assert_eq!(format!("{}", datetime), "2024-03-15T00:00:00.000000000Z");
378
379		// Almost midnight next day
380		let datetime = DateTime::new(2024, 3, 15, 23, 59, 59, 999999999).unwrap();
381		assert_eq!(format!("{}", datetime), "2024-03-15T23:59:59.999999999Z");
382
383		// Noon
384		let datetime = DateTime::new(2024, 3, 15, 12, 0, 0, 0).unwrap();
385		assert_eq!(format!("{}", datetime), "2024-03-15T12:00:00.000000000Z");
386	}
387
388	#[test]
389	fn test_datetime_display_unix_epoch() {
390		let datetime = DateTime::new(1970, 1, 1, 0, 0, 0, 0).unwrap();
391		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
392
393		let datetime = DateTime::new(1970, 1, 1, 0, 0, 1, 0).unwrap();
394		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:01.000000000Z");
395	}
396
397	#[test]
398	fn test_datetime_display_leap_year() {
399		let datetime = DateTime::new(2024, 2, 29, 12, 30, 45, 123456789).unwrap();
400		assert_eq!(format!("{}", datetime), "2024-02-29T12:30:45.123456789Z");
401
402		let datetime = DateTime::new(2000, 2, 29, 0, 0, 0, 0).unwrap();
403		assert_eq!(format!("{}", datetime), "2000-02-29T00:00:00.000000000Z");
404	}
405
406	#[test]
407	fn test_datetime_display_boundary_dates() {
408		// Very early date
409		let datetime = DateTime::new(1, 1, 1, 0, 0, 0, 0).unwrap();
410		assert_eq!(format!("{}", datetime), "0001-01-01T00:00:00.000000000Z");
411
412		// Far future date
413		let datetime = DateTime::new(9999, 12, 31, 23, 59, 59, 999999999).unwrap();
414		assert_eq!(format!("{}", datetime), "9999-12-31T23:59:59.999999999Z");
415
416		// Century boundaries
417		let datetime = DateTime::new(1900, 1, 1, 0, 0, 0, 0).unwrap();
418		assert_eq!(format!("{}", datetime), "1900-01-01T00:00:00.000000000Z");
419
420		let datetime = DateTime::new(2000, 1, 1, 0, 0, 0, 0).unwrap();
421		assert_eq!(format!("{}", datetime), "2000-01-01T00:00:00.000000000Z");
422
423		let datetime = DateTime::new(2100, 1, 1, 0, 0, 0, 0).unwrap();
424		assert_eq!(format!("{}", datetime), "2100-01-01T00:00:00.000000000Z");
425	}
426
427	#[test]
428	fn test_datetime_display_default() {
429		let datetime = DateTime::default();
430		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
431	}
432
433	#[test]
434	fn test_datetime_display_all_hours() {
435		for hour in 0..24 {
436			let datetime = DateTime::new(2024, 3, 15, hour, 30, 45, 123456789).unwrap();
437			let expected = format!("2024-03-15T{:02}:30:45.123456789Z", hour);
438			assert_eq!(format!("{}", datetime), expected);
439		}
440	}
441
442	#[test]
443	fn test_datetime_display_all_minutes() {
444		for minute in 0..60 {
445			let datetime = DateTime::new(2024, 3, 15, 14, minute, 45, 123456789).unwrap();
446			let expected = format!("2024-03-15T14:{:02}:45.123456789Z", minute);
447			assert_eq!(format!("{}", datetime), expected);
448		}
449	}
450
451	#[test]
452	fn test_datetime_display_all_seconds() {
453		for second in 0..60 {
454			let datetime = DateTime::new(2024, 3, 15, 14, 30, second, 123456789).unwrap();
455			let expected = format!("2024-03-15T14:30:{:02}.123456789Z", second);
456			assert_eq!(format!("{}", datetime), expected);
457		}
458	}
459
460	#[test]
461	fn test_datetime_display_from_timestamp() {
462		let datetime = DateTime::from_timestamp(0).unwrap();
463		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
464
465		let datetime = DateTime::from_timestamp(1234567890).unwrap();
466		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.000000000Z");
467	}
468
469	#[test]
470	fn test_datetime_display_from_timestamp_millis() {
471		let datetime = DateTime::from_timestamp_millis(1234567890123);
472		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.123000000Z");
473
474		let datetime = DateTime::from_timestamp_millis(0);
475		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
476	}
477
478	#[test]
479	fn test_datetime_display_from_parts() {
480		let datetime = DateTime::from_parts(1234567890, 123456789).unwrap();
481		assert_eq!(format!("{}", datetime), "2009-02-13T23:31:30.123456789Z");
482
483		let datetime = DateTime::from_parts(0, 0).unwrap();
484		assert_eq!(format!("{}", datetime), "1970-01-01T00:00:00.000000000Z");
485	}
486
487	#[test]
488	fn test_datetime_roundtrip() {
489		let test_cases = [
490			(1970, 1, 1, 0, 0, 0, 0),
491			(2024, 3, 15, 14, 30, 45, 123456789),
492			(2000, 2, 29, 23, 59, 59, 999999999),
493		];
494
495		for (y, m, d, h, min, s, n) in test_cases {
496			let datetime = DateTime::new(y, m, d, h, min, s, n).unwrap();
497			let nanos = datetime.to_nanos_since_epoch();
498			let recovered = DateTime::from_nanos_since_epoch(nanos);
499
500			assert_eq!(datetime.year(), recovered.year());
501			assert_eq!(datetime.month(), recovered.month());
502			assert_eq!(datetime.day(), recovered.day());
503			assert_eq!(datetime.hour(), recovered.hour());
504			assert_eq!(datetime.minute(), recovered.minute());
505			assert_eq!(datetime.second(), recovered.second());
506			assert_eq!(datetime.nanosecond(), recovered.nanosecond());
507		}
508	}
509
510	#[test]
511	fn test_datetime_components() {
512		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
513
514		assert_eq!(datetime.year(), 2024);
515		assert_eq!(datetime.month(), 3);
516		assert_eq!(datetime.day(), 15);
517		assert_eq!(datetime.hour(), 14);
518		assert_eq!(datetime.minute(), 30);
519		assert_eq!(datetime.second(), 45);
520		assert_eq!(datetime.nanosecond(), 123456789);
521	}
522
523	#[test]
524	fn test_serde_roundtrip() {
525		let datetime = DateTime::new(2024, 3, 15, 14, 30, 45, 123456789).unwrap();
526		let json = serde_json::to_string(&datetime).unwrap();
527		assert_eq!(json, "\"2024-03-15T14:30:45.123456789Z\"");
528
529		let recovered: DateTime = serde_json::from_str(&json).unwrap();
530		assert_eq!(datetime, recovered);
531	}
532}