1use crate::constants::{HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_DAY, SECONDS_IN_MINUTE};
2use crate::date::Date;
3use crate::date_error::DateError;
4use crate::date_error::DateErrorKind;
5use crate::time::Time;
6use crate::utils::crossplatform_util;
7use std::cmp::Ordering;
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Copy, Clone)]
12#[cfg_attr(feature = "serde-struct", derive(serde::Serialize, serde::Deserialize))]
13pub struct DateTime {
14 pub date: Date,
15 pub time: Time,
16 pub shift_minutes: isize,
17}
18
19#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
22impl serde::Serialize for DateTime {
23 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
24 where
25 S: serde::Serializer,
26 {
27 let seconds = self.to_seconds_from_unix_epoch_gmt();
29 serializer.serialize_u64(seconds)
30 }
31}
32
33#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
34impl<'de> serde::Deserialize<'de> for DateTime {
35 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36 where
37 D: serde::Deserializer<'de>,
38 {
39 let seconds = u64::deserialize(deserializer)?;
41 let dt = DateTime::from_seconds_since_unix_epoch(seconds);
42 Ok(DateTime::new(dt.date, dt.time, 0))
43 }
44}
45
46impl DateTime {
47 pub fn new(date: Date, time: Time, shift_minutes: isize) -> Self {
48 DateTime {
49 date,
50 time,
51 shift_minutes,
52 }
53 }
54
55 pub fn to_iso_8061(&self) -> String {
56 format!("{}T{}{}", self.date, self.time, self.shift_string())
57 }
58
59 pub fn shift_string(&self) -> String {
60 if self.shift_minutes == 0 {
61 return "Z".to_string();
62 }
63 let hours = (self.shift_minutes.abs() / 60) as u64;
64 let minutes = (self.shift_minutes.abs() % 60) as u64;
65 if self.shift_minutes.is_positive() {
66 format!("+{:02}:{:02}", hours, minutes)
67 } else {
68 format!("-{:02}:{:02}", hours, minutes)
69 }
70 }
71
72 pub fn from_seconds_since_unix_epoch(seconds: u64) -> Self {
73 let (date, seconds) = Date::from_seconds_since_unix_epoch(seconds);
74 let time = Time::from_seconds(seconds);
75 DateTime::new(date, time, 0)
76 }
77
78 pub fn to_seconds_from_unix_epoch(&self) -> u64 {
79 self.date.to_seconds_from_unix_epoch(false) + self.time.to_seconds()
80 }
81
82 pub fn to_seconds_from_unix_epoch_gmt(&self) -> u64 {
83 (self.to_seconds_from_unix_epoch() as i128
84 - self.shift_minutes as i128 * SECONDS_IN_MINUTE as i128) as u64
85 }
86
87 pub fn now() -> Self {
88 Self::from_seconds_since_unix_epoch(crossplatform_util::now_seconds())
89 }
90
91 pub fn now_seconds() -> u64 {
92 crossplatform_util::now_seconds()
93 }
94
95 pub fn now_milliseconds() -> u128 {
96 crossplatform_util::now_milliseconds()
97 }
98
99 pub fn set_shift(&mut self, minutes: isize) {
100 if minutes > self.shift_minutes {
101 *self = self.add_seconds((minutes - self.shift_minutes) as u64 * SECONDS_IN_MINUTE)
102 } else {
103 *self = self.sub_seconds((self.shift_minutes - minutes) as u64 * SECONDS_IN_MINUTE)
104 }
105 self.shift_minutes = minutes;
106 }
107
108 pub fn add_seconds(&self, seconds: u64) -> Self {
109 let total_seconds = self.time.to_seconds() + seconds;
110 Self::new(
111 self.date.add_days(total_seconds / SECONDS_IN_DAY),
112 Time::from_seconds(total_seconds % SECONDS_IN_DAY),
113 self.shift_minutes,
114 )
115 }
116
117 pub fn add_time(&self, time: Time) -> Self {
118 Self::new(self.date, self.time + time, self.shift_minutes).normalize()
119 }
120
121 pub fn sub_seconds(&mut self, seconds: u64) -> Self {
122 let mut days = seconds / SECONDS_IN_DAY;
123 let seconds = seconds % SECONDS_IN_DAY;
124 let time_seconds = self.time.to_seconds();
125 let seconds = if time_seconds < seconds {
126 days += 1;
127 SECONDS_IN_DAY - seconds + time_seconds
128 } else {
129 time_seconds - seconds
130 };
131 Self::new(
132 self.date.sub_days(days),
133 Time::from_seconds(seconds),
134 self.shift_minutes,
135 )
136 }
137
138 fn shift_from_str(shift_str: &str) -> Result<isize, DateError> {
139 if shift_str.len() == 0 || &shift_str[0..1] == "Z" {
140 return Ok(0);
141 }
142
143 let mut split = (&shift_str[1..]).split(":");
144 let err = || DateErrorKind::WrongTimeShiftStringFormat;
145 let hour: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
146 let minute: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
147 let mut minutes: isize = (hour * MINUTES_IN_HOUR + minute) as isize;
148 if &shift_str[0..1] == "-" {
149 minutes = 0 - minutes;
150 }
151 Ok(minutes)
152 }
153
154 pub fn normalize(&self) -> DateTime {
155 let date = self.date.normalize();
156 let mut time = self.time.normalize();
157 let days = time.hour / 24;
158 time.hour %= 24;
159 Self::new(date.add_days(days), time, self.shift_minutes)
160 }
161}
162
163impl fmt::Display for DateTime {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(f, "{} {}", self.date, self.time)
166 }
167}
168
169impl fmt::Debug for DateTime {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 fmt::Display::fmt(self, f)
172 }
173}
174
175impl Ord for DateTime {
176 fn cmp(&self, other: &Self) -> Ordering {
177 match self.date.cmp(&other.date) {
178 Ordering::Equal if self.shift_minutes == other.shift_minutes => {
179 self.time.cmp(&other.time)
180 }
181 Ordering::Equal => {
182 let lhs = self.time.to_minutes() as i64 - self.shift_minutes as i64;
183 let rhs = other.time.to_minutes() as i64 - other.shift_minutes as i64;
184 match lhs.cmp(&rhs) {
185 Ordering::Equal => (self.time.second + self.time.microsecond)
186 .cmp(&(other.time.second + other.time.microsecond)),
187 ordering => ordering,
188 }
189 }
190 ordering => ordering,
191 }
192 }
193}
194
195impl FromStr for DateTime {
196 type Err = DateError;
197
198 fn from_str(date_time_str: &str) -> Result<Self, Self::Err> {
199 let bytes = date_time_str.as_bytes();
200 let len = bytes.len();
201
202 if len < 11 || bytes[10] != b'T' {
203 return Err(DateErrorKind::WrongDateTimeStringFormat.into());
204 }
205
206 let date: Date = std::str::from_utf8(&bytes[0..10])
207 .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
208 .parse()?;
209
210 if len <= 19 {
211 return Err(DateErrorKind::WrongDateTimeStringFormat.into());
212 }
213
214 for i in 19..len {
215 match bytes[i] {
216 b'Z' | b'+' | b'-' => {
217 return Ok(DateTime::new(
218 date,
219 std::str::from_utf8(&bytes[11..i])
220 .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
221 .parse()?,
222 DateTime::shift_from_str(
223 std::str::from_utf8(&bytes[i..])
224 .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?,
225 )?,
226 ));
227 }
228 _ => {}
229 }
230 }
231
232 Err(DateErrorKind::WrongDateTimeStringFormat.into())
233 }
234}
235
236impl PartialOrd for DateTime {
237 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
238 Some(self.cmp(other))
239 }
240}
241
242impl PartialEq for DateTime {
243 fn eq(&self, other: &Self) -> bool {
244 self.cmp(other) == Ordering::Equal
245 }
246}
247
248impl Eq for DateTime {}
249
250impl std::ops::Sub for DateTime {
251 type Output = Time;
252
253 fn sub(self, rhs: Self) -> Self::Output {
254 self.time + Time::from_hours((self.date - rhs.date) * HOURS_IN_DAY) - rhs.time
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::constants::MINUTES_IN_HOUR;
262
263 #[test]
264 fn test_from_seconds_since_unix_epoch() {
265 let date_time = DateTime::new(Date::new(2021, 4, 13), Time::new(20, 55, 50), 0);
266 assert_eq!(
267 DateTime::from_seconds_since_unix_epoch(1618347350),
268 date_time
269 );
270 assert_eq!(date_time.to_seconds_from_unix_epoch(), 1618347350);
271 }
272
273 #[test]
274 fn test_date_time_cmp() {
275 let mut lhs = DateTime::new(Date::new(2019, 12, 31), Time::new(12, 0, 0), 0);
276 let mut rhs = lhs;
277 assert_eq!(lhs, rhs);
278 rhs.time.hour += 1;
279 assert!(lhs < rhs);
280 lhs.shift_minutes = -60;
281 assert_eq!(lhs, rhs);
282 }
283
284 #[test]
285 fn test_date_time_to_string() {
286 let date_time = DateTime::new(
287 Date::new(2021, 7, 28),
288 Time::new(10, 0, 0),
289 -4 * MINUTES_IN_HOUR as isize,
290 );
291 assert_eq!(date_time.to_iso_8061(), "2021-07-28T10:00:00-04:00");
292 assert_eq!(date_time.to_string(), "2021-07-28 10:00:00");
293 }
294
295 #[test]
296 fn test_shift_from_str() -> Result<(), DateError> {
297 assert_eq!(DateTime::shift_from_str("+4:30")?, 270);
298 assert_eq!(DateTime::shift_from_str("-4:30")?, -270);
299 assert_eq!(DateTime::shift_from_str("Z")?, 0);
300 Ok(())
301 }
302
303 #[test]
304 fn test_date_time_from_str() -> Result<(), DateError> {
305 assert_eq!(
306 "2021-07-28T10:00:00-4:00".parse::<DateTime>()?,
307 DateTime::new(
308 Date::new(2021, 7, 28),
309 Time::new(10, 0, 0),
310 -4 * MINUTES_IN_HOUR as isize
311 )
312 );
313
314 assert_eq!(
315 "2021-07-28T10:00:00+02:00".parse::<DateTime>()?,
316 DateTime::new(
317 Date::new(2021, 7, 28),
318 Time::new(10, 0, 0),
319 2 * MINUTES_IN_HOUR as isize
320 )
321 );
322
323 assert_eq!(
324 "2021-07-28T10:00:00Z".parse::<DateTime>()?,
325 DateTime::new(Date::new(2021, 7, 28), Time::new(10, 0, 0), 0)
326 );
327 assert_eq!(
328 "2020-01-09T21:10:05.779325Z".parse::<DateTime>()?,
329 DateTime::new(
330 Date::new(2020, 1, 9),
331 Time::new_with_microseconds(21, 10, 5, 779325),
332 0
333 )
334 );
335
336 Ok(())
337 }
338
339 #[test]
340 fn test_to_seconds_since_unix_epoch_gmt() {
341 let date_time = DateTime::new(
342 Date::new(2023, 1, 13),
343 Time::new(8, 40, 42),
344 -5 * MINUTES_IN_HOUR as isize,
345 );
346 assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
347 let date_time = DateTime::new(
348 Date::new(2023, 1, 13),
349 Time::new(14, 40, 42),
350 1 * MINUTES_IN_HOUR as isize,
351 );
352 assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
353 }
354
355 #[test]
356 fn test_date_time_normalize() {
357 let date_time = DateTime::new(
358 Date::new(2023, 1, 13),
359 Time::new(24, 0, 42),
360 -5 * MINUTES_IN_HOUR as isize,
361 );
362 let date_time2 = DateTime::new(
363 Date::new(2023, 1, 14),
364 Time::new(0, 0, 42),
365 -5 * MINUTES_IN_HOUR as isize,
366 );
367 assert_eq!(date_time.normalize(), date_time2);
368 }
369
370 #[test]
371 fn test_date_time_from_str_invalid() {
372 assert!("invalid".parse::<DateTime>().is_err());
373 assert!("2020-01-01".parse::<DateTime>().is_err());
374 assert!("2020-01-01T".parse::<DateTime>().is_err());
375 assert!("2020-01-01T12:00:00".parse::<DateTime>().is_err());
376 }
377
378 #[test]
379 fn test_shift_string_formatting() {
380 let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
381 assert_eq!(dt_utc.shift_string(), "Z");
382
383 let dt_plus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 120);
384 assert_eq!(dt_plus.shift_string(), "+02:00");
385
386 let dt_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), -300);
387 assert_eq!(dt_minus.shift_string(), "-05:00");
388
389 let dt_plus_30 = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 30);
390 assert_eq!(dt_plus_30.shift_string(), "+00:30");
391 }
392
393 #[test]
394 fn test_iso_8601_formatting() {
395 let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 0);
396 assert_eq!(dt.to_iso_8061(), "2020-01-01T12:30:45Z");
397
398 let dt_tz = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 120);
399 assert_eq!(dt_tz.to_iso_8061(), "2020-01-01T12:30:45+02:00");
400
401 let dt_tz_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), -300);
402 assert_eq!(dt_tz_minus.to_iso_8061(), "2020-01-01T12:30:45-05:00");
403 }
404
405 #[test]
406 fn test_date_time_arithmetic() {
407 let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
408
409 let dt_plus_sec = dt.add_seconds(3600); assert_eq!(dt_plus_sec.time.hour, 13);
411 assert_eq!(dt_plus_sec.date, Date::new(2020, 1, 1));
412
413 let dt_plus_day = dt.add_seconds(86400); assert_eq!(dt_plus_day.date, Date::new(2020, 1, 2));
415 assert_eq!(dt_plus_day.time, Time::new(12, 0, 0));
416
417 let time_to_add = Time::new(2, 30, 0);
418 let dt_plus_time = dt.add_time(time_to_add);
419 assert_eq!(dt_plus_time.time, Time::new(14, 30, 0));
420 }
421
422 #[test]
423 fn test_date_time_subtraction() {
424 let dt1 = DateTime::new(Date::new(2020, 1, 2), Time::new(12, 0, 0), 0);
425 let dt2 = DateTime::new(Date::new(2020, 1, 1), Time::new(10, 0, 0), 0);
426
427 let diff = dt1 - dt2;
428 assert_eq!(diff, Time::new(26, 0, 0)); }
430
431 #[test]
432 fn test_timezone_shift_operations() {
433 let mut dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
434
435 dt.set_shift(120); assert_eq!(dt.shift_minutes, 120);
437 assert_eq!(dt.time, Time::new(14, 0, 0)); dt.set_shift(-300); assert_eq!(dt.shift_minutes, -300);
441 assert_eq!(dt.time, Time::new(7, 0, 0)); }
443
444 #[test]
445 fn test_unix_epoch_conversions() {
446 let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
447 assert_eq!(dt.to_seconds_from_unix_epoch(), 0);
448
449 let dt_from_epoch = DateTime::from_seconds_since_unix_epoch(0);
450 assert_eq!(dt_from_epoch.date, Date::new(1970, 1, 1));
451 assert_eq!(dt_from_epoch.time, Time::new(0, 0, 0));
452
453 let dt_tz = DateTime::new(Date::new(1970, 1, 1), Time::new(12, 0, 0), 0);
454 let gmt_seconds = dt_tz.to_seconds_from_unix_epoch_gmt();
455 assert_eq!(gmt_seconds, 43200); }
457
458 #[test]
459 fn test_date_time_comparison_with_timezone() {
460 let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
461 let dt_est = DateTime::new(Date::new(2020, 1, 1), Time::new(7, 0, 0), -300);
462 assert_eq!(dt_utc, dt_est);
463 let dt_different = DateTime::new(Date::new(2020, 1, 1), Time::new(8, 0, 0), -300);
464 assert_ne!(dt_utc, dt_different);
465 }
466
467 #[test]
468 fn test_edge_cases() {
469 let dt_leap = DateTime::new(Date::new(2020, 2, 29), Time::new(12, 0, 0), 0);
470 assert!(dt_leap.date.valid());
471 let dt_year_end = DateTime::new(Date::new(2020, 12, 31), Time::new(23, 59, 59), 0);
472 let dt_next_year = dt_year_end.add_seconds(1);
473 assert_eq!(dt_next_year.date, Date::new(2021, 1, 1));
474 assert_eq!(dt_next_year.time, Time::new(0, 0, 0));
475 }
476
477 #[cfg(feature = "serde")]
478 mod serde_tests {
479 use super::*;
480 use serde_json;
481
482 #[test]
483 #[cfg(not(feature = "serde-struct"))]
484 fn test_serde_unix_epoch() {
485 let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
486 let json = serde_json::to_string(&dt).unwrap();
487 assert_eq!(json, "0");
488 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
489 assert_eq!(deserialized.date, dt.date);
490 assert_eq!(deserialized.time, dt.time);
491 assert_eq!(deserialized.shift_minutes, 0);
492
493 let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0);
494 let expected_seconds = dt.to_seconds_from_unix_epoch_gmt();
495 let json = serde_json::to_string(&dt).unwrap();
496 assert_eq!(json, expected_seconds.to_string());
497 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
498 assert_eq!(deserialized.date, dt.date);
499 assert_eq!(deserialized.time, dt.time);
500 assert_eq!(deserialized.shift_minutes, 0);
501 }
502
503 #[test]
504 #[cfg(not(feature = "serde-struct"))]
505 fn test_serde_unix_epoch_with_timezone() {
506 let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), -300); let expected_utc_seconds = dt.to_seconds_from_unix_epoch_gmt();
509 let json = serde_json::to_string(&dt).unwrap();
510 assert_eq!(json, expected_utc_seconds.to_string());
511
512 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
514 assert_eq!(deserialized.shift_minutes, 0);
515 assert_eq!(deserialized.to_seconds_from_unix_epoch_gmt(), expected_utc_seconds);
517 }
518
519 #[test]
520 #[cfg(feature = "serde-struct")]
521 fn test_serde_struct() {
522 let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0);
523 let json = serde_json::to_string(&dt).unwrap();
524 assert!(json.contains("\"date\""));
525 assert!(json.contains("\"time\""));
526 assert!(json.contains("\"shift_minutes\":0"));
527 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
528 assert_eq!(deserialized, dt);
529
530 let dt = DateTime::new(Date::new(2020, 2, 29), Time::new(23, 59, 59), -300);
531 let json = serde_json::to_string(&dt).unwrap();
532 assert!(json.contains("\"shift_minutes\":-300"));
533 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
534 assert_eq!(deserialized, dt);
535 }
536
537 #[test]
538 fn test_serde_roundtrip() {
539 let datetimes = vec![
540 DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0),
541 DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0),
542 DateTime::new(Date::new(2020, 2, 29), Time::new(23, 59, 59), 0),
543 DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), -300),
544 DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), 120),
545 ];
546
547 for dt in datetimes {
548 let json = serde_json::to_string(&dt).unwrap();
549 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
550
551 #[cfg(not(feature = "serde-struct"))]
552 {
553 assert_eq!(deserialized.shift_minutes, 0);
555 assert_eq!(
556 deserialized.to_seconds_from_unix_epoch_gmt(),
557 dt.to_seconds_from_unix_epoch_gmt()
558 );
559 }
560
561 #[cfg(feature = "serde-struct")]
562 {
563 assert_eq!(deserialized, dt, "Failed roundtrip for datetime: {}", dt);
565 }
566 }
567 }
568
569 #[test]
570 #[cfg(not(feature = "serde-struct"))]
571 fn test_serde_utc_preservation() {
572 let dt_utc = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), 0);
574 let json = serde_json::to_string(&dt_utc).unwrap();
575 let deserialized: DateTime = serde_json::from_str(&json).unwrap();
576 assert_eq!(deserialized, dt_utc);
577 }
578 }
579}