simple_datetime_rs/
date_time.rs

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