simple_datetime_rs/
date_time.rs

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// Unix epoch serialization (default when serde is enabled without serde-struct)
20// Serializes as a single u64 representing seconds since unix epoch in UTC
21#[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        // Serialize as seconds since unix epoch (UTC)
28        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        // Deserialize as UTC seconds, shift_minutes is presumed 0 (UTC/GMT)
40        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); // Add 1 hour
410        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); // Add 1 day
414        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)); // 1 day + 2 hours
429    }
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); // UTC+2
436        assert_eq!(dt.shift_minutes, 120);
437        assert_eq!(dt.time, Time::new(14, 0, 0)); // Time should adjust
438
439        dt.set_shift(-300); // UTC-5
440        assert_eq!(dt.shift_minutes, -300);
441        assert_eq!(dt.time, Time::new(7, 0, 0)); // Time should adjust
442    }
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); // 12 hours in seconds
456    }
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            // DateTime with timezone should serialize as UTC
507            let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), -300); // UTC-5
508            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            // Deserialized should be in UTC (shift_minutes = 0)
513            let deserialized: DateTime = serde_json::from_str(&json).unwrap();
514            assert_eq!(deserialized.shift_minutes, 0);
515            // The UTC time should match
516            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                    // Unix epoch format: deserialized is always UTC
554                    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                    // Struct format: preserves all fields
564                    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            // Test that UTC times are preserved correctly
573            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}