reifydb_type/value/
datetime.rs

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