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)]
12pub struct DateTime {
13    pub date: Date,
14    pub time: Time,
15    pub shift_minutes: isize,
16}
17
18impl DateTime {
19    pub fn new(date: Date, time: Time, shift_minutes: isize) -> Self {
20        DateTime {
21            date,
22            time,
23            shift_minutes,
24        }
25    }
26
27    pub fn to_iso_8061(&self) -> String {
28        format!("{}T{}{}", self.date, self.time, self.shift_string())
29    }
30
31    pub fn shift_string(&self) -> String {
32        if self.shift_minutes == 0 {
33            return "Z".to_string();
34        }
35        let hours = (self.shift_minutes.abs() / 60) as u64;
36        let minutes = (self.shift_minutes.abs() % 60) as u64;
37        if self.shift_minutes.is_positive() {
38            format!("+{:02}:{:02}", hours, minutes)
39        } else {
40            format!("-{:02}:{:02}", hours, minutes)
41        }
42    }
43
44    pub fn from_seconds_since_unix_epoch(seconds: u64) -> Self {
45        let (date, seconds) = Date::from_seconds_since_unix_epoch(seconds);
46        let time = Time::from_seconds(seconds);
47        DateTime::new(date, time, 0)
48    }
49
50    pub fn to_seconds_from_unix_epoch(&self) -> u64 {
51        self.date.to_seconds_from_unix_epoch(false) + self.time.to_seconds()
52    }
53
54    pub fn to_seconds_from_unix_epoch_gmt(&self) -> u64 {
55        (self.to_seconds_from_unix_epoch() as i128
56            - self.shift_minutes as i128 * SECONDS_IN_MINUTE as i128) as u64
57    }
58
59    pub fn now() -> Self {
60        Self::from_seconds_since_unix_epoch(crossplatform_util::now_seconds())
61    }
62
63    pub fn now_seconds() -> u64 {
64        crossplatform_util::now_seconds()
65    }
66
67    pub fn now_milliseconds() -> u128 {
68        crossplatform_util::now_milliseconds()
69    }
70
71    pub fn set_shift(&mut self, minutes: isize) {
72        if minutes > self.shift_minutes {
73            *self = self.add_seconds((minutes - self.shift_minutes) as u64 * SECONDS_IN_MINUTE)
74        } else {
75            *self = self.sub_seconds((self.shift_minutes - minutes) as u64 * SECONDS_IN_MINUTE)
76        }
77        self.shift_minutes = minutes;
78    }
79
80    pub fn add_seconds(&self, seconds: u64) -> Self {
81        let total_seconds = self.time.to_seconds() + seconds;
82        Self::new(
83            self.date.add_days(total_seconds / SECONDS_IN_DAY),
84            Time::from_seconds(total_seconds % SECONDS_IN_DAY),
85            self.shift_minutes,
86        )
87    }
88
89    pub fn add_time(&self, time: Time) -> Self {
90        Self::new(self.date, self.time + time, self.shift_minutes).normalize()
91    }
92
93    pub fn sub_seconds(&mut self, seconds: u64) -> Self {
94        let mut days = seconds / SECONDS_IN_DAY;
95        let seconds = seconds % SECONDS_IN_DAY;
96        let time_seconds = self.time.to_seconds();
97        let seconds = if time_seconds < seconds {
98            days += 1;
99            SECONDS_IN_DAY - seconds + time_seconds
100        } else {
101            time_seconds - seconds
102        };
103        Self::new(
104            self.date.sub_days(days),
105            Time::from_seconds(seconds),
106            self.shift_minutes,
107        )
108    }
109
110    fn shift_from_str(shift_str: &str) -> Result<isize, DateError> {
111        if shift_str.len() == 0 || &shift_str[0..1] == "Z" {
112            return Ok(0);
113        }
114
115        let mut split = (&shift_str[1..]).split(":");
116        let err = || DateErrorKind::WrongTimeShiftStringFormat;
117        let hour: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
118        let minute: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
119        let mut minutes: isize = (hour * MINUTES_IN_HOUR + minute) as isize;
120        if &shift_str[0..1] == "-" {
121            minutes = 0 - minutes;
122        }
123        Ok(minutes)
124    }
125
126    pub fn normalize(&self) -> DateTime {
127        let date = self.date.normalize();
128        let mut time = self.time.normalize();
129        let days = time.hour / 24;
130        time.hour %= 24;
131        Self::new(date.add_days(days), time, self.shift_minutes)
132    }
133}
134
135impl fmt::Display for DateTime {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "{} {}", self.date, self.time)
138    }
139}
140
141impl fmt::Debug for DateTime {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        fmt::Display::fmt(self, f)
144    }
145}
146
147impl Ord for DateTime {
148    fn cmp(&self, other: &Self) -> Ordering {
149        match self.date.cmp(&other.date) {
150            Ordering::Equal if self.shift_minutes == other.shift_minutes => {
151                self.time.cmp(&other.time)
152            }
153            Ordering::Equal => {
154                let lhs = self.time.to_minutes() as i64 - self.shift_minutes as i64;
155                let rhs = other.time.to_minutes() as i64 - other.shift_minutes as i64;
156                match lhs.cmp(&rhs) {
157                    Ordering::Equal => (self.time.second + self.time.microsecond)
158                        .cmp(&(other.time.second + other.time.microsecond)),
159                    ordering => ordering,
160                }
161            }
162            ordering => ordering,
163        }
164    }
165}
166
167impl FromStr for DateTime {
168    type Err = DateError;
169
170    fn from_str(date_time_str: &str) -> Result<Self, Self::Err> {
171        let bytes = date_time_str.as_bytes();
172        let len = bytes.len();
173
174        if len < 11 || bytes[10] != b'T' {
175            return Err(DateErrorKind::WrongDateTimeStringFormat.into());
176        }
177
178        let date: Date = std::str::from_utf8(&bytes[0..10])
179            .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
180            .parse()?;
181
182        if len <= 19 {
183            return Err(DateErrorKind::WrongDateTimeStringFormat.into());
184        }
185
186        for i in 19..len {
187            match bytes[i] {
188                b'Z' | b'+' | b'-' => {
189                    return Ok(DateTime::new(
190                        date,
191                        std::str::from_utf8(&bytes[11..i])
192                            .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
193                            .parse()?,
194                        DateTime::shift_from_str(
195                            std::str::from_utf8(&bytes[i..])
196                                .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?,
197                        )?,
198                    ));
199                }
200                _ => {}
201            }
202        }
203
204        Err(DateErrorKind::WrongDateTimeStringFormat.into())
205    }
206}
207
208impl PartialOrd for DateTime {
209    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
210        Some(self.cmp(other))
211    }
212}
213
214impl PartialEq for DateTime {
215    fn eq(&self, other: &Self) -> bool {
216        self.cmp(other) == Ordering::Equal
217    }
218}
219
220impl Eq for DateTime {}
221
222impl std::ops::Sub for DateTime {
223    type Output = Time;
224
225    fn sub(self, rhs: Self) -> Self::Output {
226        self.time + Time::from_hours((self.date - rhs.date) * HOURS_IN_DAY) - rhs.time
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::constants::MINUTES_IN_HOUR;
234
235    #[test]
236    fn test_from_seconds_since_unix_epoch() {
237        let date_time = DateTime::new(Date::new(2021, 4, 13), Time::new(20, 55, 50), 0);
238        assert_eq!(
239            DateTime::from_seconds_since_unix_epoch(1618347350),
240            date_time
241        );
242        assert_eq!(date_time.to_seconds_from_unix_epoch(), 1618347350);
243    }
244
245    #[test]
246    fn test_date_time_cmp() {
247        let mut lhs = DateTime::new(Date::new(2019, 12, 31), Time::new(12, 0, 0), 0);
248        let mut rhs = lhs;
249        assert_eq!(lhs, rhs);
250        rhs.time.hour += 1;
251        assert!(lhs < rhs);
252        lhs.shift_minutes = -60;
253        assert_eq!(lhs, rhs);
254    }
255
256    #[test]
257    fn test_date_time_to_string() {
258        let date_time = DateTime::new(
259            Date::new(2021, 7, 28),
260            Time::new(10, 0, 0),
261            -4 * MINUTES_IN_HOUR as isize,
262        );
263        assert_eq!(date_time.to_iso_8061(), "2021-07-28T10:00:00-04:00");
264        assert_eq!(date_time.to_string(), "2021-07-28 10:00:00");
265    }
266
267    #[test]
268    fn test_shift_from_str() -> Result<(), DateError> {
269        assert_eq!(DateTime::shift_from_str("+4:30")?, 270);
270        assert_eq!(DateTime::shift_from_str("-4:30")?, -270);
271        assert_eq!(DateTime::shift_from_str("Z")?, 0);
272        Ok(())
273    }
274
275    #[test]
276    fn test_date_time_from_str() -> Result<(), DateError> {
277        assert_eq!(
278            "2021-07-28T10:00:00-4:00".parse::<DateTime>()?,
279            DateTime::new(
280                Date::new(2021, 7, 28),
281                Time::new(10, 0, 0),
282                -4 * MINUTES_IN_HOUR as isize
283            )
284        );
285
286        assert_eq!(
287            "2021-07-28T10:00:00+02:00".parse::<DateTime>()?,
288            DateTime::new(
289                Date::new(2021, 7, 28),
290                Time::new(10, 0, 0),
291                2 * MINUTES_IN_HOUR as isize
292            )
293        );
294
295        assert_eq!(
296            "2021-07-28T10:00:00Z".parse::<DateTime>()?,
297            DateTime::new(Date::new(2021, 7, 28), Time::new(10, 0, 0), 0)
298        );
299        assert_eq!(
300            "2020-01-09T21:10:05.779325Z".parse::<DateTime>()?,
301            DateTime::new(
302                Date::new(2020, 1, 9),
303                Time::new_with_microseconds(21, 10, 5, 779325),
304                0
305            )
306        );
307
308        Ok(())
309    }
310
311    #[test]
312    fn test_to_seconds_since_unix_epoch_gmt() {
313        let date_time = DateTime::new(
314            Date::new(2023, 1, 13),
315            Time::new(8, 40, 42),
316            -5 * MINUTES_IN_HOUR as isize,
317        );
318        assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
319        let date_time = DateTime::new(
320            Date::new(2023, 1, 13),
321            Time::new(14, 40, 42),
322            1 * MINUTES_IN_HOUR as isize,
323        );
324        assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
325    }
326
327    #[test]
328    fn test_date_time_normalize() {
329        let date_time = DateTime::new(
330            Date::new(2023, 1, 13),
331            Time::new(24, 0, 42),
332            -5 * MINUTES_IN_HOUR as isize,
333        );
334        let date_time2 = DateTime::new(
335            Date::new(2023, 1, 14),
336            Time::new(0, 0, 42),
337            -5 * MINUTES_IN_HOUR as isize,
338        );
339        assert_eq!(date_time.normalize(), date_time2);
340    }
341
342    #[test]
343    fn test_date_time_from_str_invalid() {
344        assert!("invalid".parse::<DateTime>().is_err());
345        assert!("2020-01-01".parse::<DateTime>().is_err());
346        assert!("2020-01-01T".parse::<DateTime>().is_err());
347        assert!("2020-01-01T12:00:00".parse::<DateTime>().is_err());
348    }
349
350    #[test]
351    fn test_shift_string_formatting() {
352        let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
353        assert_eq!(dt_utc.shift_string(), "Z");
354
355        let dt_plus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 120);
356        assert_eq!(dt_plus.shift_string(), "+02:00");
357
358        let dt_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), -300);
359        assert_eq!(dt_minus.shift_string(), "-05:00");
360
361        let dt_plus_30 = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 30);
362        assert_eq!(dt_plus_30.shift_string(), "+00:30");
363    }
364
365    #[test]
366    fn test_iso_8601_formatting() {
367        let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 0);
368        assert_eq!(dt.to_iso_8061(), "2020-01-01T12:30:45Z");
369
370        let dt_tz = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 120);
371        assert_eq!(dt_tz.to_iso_8061(), "2020-01-01T12:30:45+02:00");
372
373        let dt_tz_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), -300);
374        assert_eq!(dt_tz_minus.to_iso_8061(), "2020-01-01T12:30:45-05:00");
375    }
376
377    #[test]
378    fn test_date_time_arithmetic() {
379        let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
380
381        let dt_plus_sec = dt.add_seconds(3600); // Add 1 hour
382        assert_eq!(dt_plus_sec.time.hour, 13);
383        assert_eq!(dt_plus_sec.date, Date::new(2020, 1, 1));
384
385        let dt_plus_day = dt.add_seconds(86400); // Add 1 day
386        assert_eq!(dt_plus_day.date, Date::new(2020, 1, 2));
387        assert_eq!(dt_plus_day.time, Time::new(12, 0, 0));
388
389        let time_to_add = Time::new(2, 30, 0);
390        let dt_plus_time = dt.add_time(time_to_add);
391        assert_eq!(dt_plus_time.time, Time::new(14, 30, 0));
392    }
393
394    #[test]
395    fn test_date_time_subtraction() {
396        let dt1 = DateTime::new(Date::new(2020, 1, 2), Time::new(12, 0, 0), 0);
397        let dt2 = DateTime::new(Date::new(2020, 1, 1), Time::new(10, 0, 0), 0);
398
399        let diff = dt1 - dt2;
400        assert_eq!(diff, Time::new(26, 0, 0)); // 1 day + 2 hours
401    }
402
403    #[test]
404    fn test_timezone_shift_operations() {
405        let mut dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
406
407        dt.set_shift(120); // UTC+2
408        assert_eq!(dt.shift_minutes, 120);
409        assert_eq!(dt.time, Time::new(14, 0, 0)); // Time should adjust
410
411        dt.set_shift(-300); // UTC-5
412        assert_eq!(dt.shift_minutes, -300);
413        assert_eq!(dt.time, Time::new(7, 0, 0)); // Time should adjust
414    }
415
416    #[test]
417    fn test_unix_epoch_conversions() {
418        let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
419        assert_eq!(dt.to_seconds_from_unix_epoch(), 0);
420
421        let dt_from_epoch = DateTime::from_seconds_since_unix_epoch(0);
422        assert_eq!(dt_from_epoch.date, Date::new(1970, 1, 1));
423        assert_eq!(dt_from_epoch.time, Time::new(0, 0, 0));
424
425        let dt_tz = DateTime::new(Date::new(1970, 1, 1), Time::new(12, 0, 0), 0);
426        let gmt_seconds = dt_tz.to_seconds_from_unix_epoch_gmt();
427        assert_eq!(gmt_seconds, 43200); // 12 hours in seconds
428    }
429
430    #[test]
431    fn test_date_time_comparison_with_timezone() {
432        let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
433        let dt_est = DateTime::new(Date::new(2020, 1, 1), Time::new(7, 0, 0), -300);
434        assert_eq!(dt_utc, dt_est);
435        let dt_different = DateTime::new(Date::new(2020, 1, 1), Time::new(8, 0, 0), -300);
436        assert_ne!(dt_utc, dt_different);
437    }
438
439    #[test]
440    fn test_edge_cases() {
441        let dt_leap = DateTime::new(Date::new(2020, 2, 29), Time::new(12, 0, 0), 0);
442        assert!(dt_leap.date.valid());
443        let dt_year_end = DateTime::new(Date::new(2020, 12, 31), Time::new(23, 59, 59), 0);
444        let dt_next_year = dt_year_end.add_seconds(1);
445        assert_eq!(dt_next_year.date, Date::new(2021, 1, 1));
446        assert_eq!(dt_next_year.time, Time::new(0, 0, 0));
447    }
448}