Skip to main content

reifydb_value/value/
datetime.rs

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