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        // Optimized parsing without allocations
149        let bytes = date_time_str.as_bytes();
150        let len = bytes.len();
151        
152        // Find the 'T' separator
153        if len < 11 || bytes[10] != b'T' {
154            return Err(DateErrorKind::WrongDateTimeStringFormat.into());
155        }
156        
157        // Parse date part (first 10 characters)
158        let date: Date = std::str::from_utf8(&bytes[0..10])
159            .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
160            .parse()?;
161        
162        // Find timezone indicator - timezone is required
163        if len <= 19 {
164            return Err(DateErrorKind::WrongDateTimeStringFormat.into());
165        }
166        
167        // Look for Z, +, or - after the time part
168        for i in 19..len {
169            match bytes[i] {
170                b'Z' | b'+' | b'-' => return Ok(DateTime::new(
171                    date,
172                    std::str::from_utf8(&bytes[11..i])
173                        .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
174                        .parse()?,
175                    DateTime::shift_from_str(std::str::from_utf8(&bytes[i..])
176                        .map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?)?
177                )),
178                _ => {}
179            }
180        }
181        
182        // No valid timezone found
183        Err(DateErrorKind::WrongDateTimeStringFormat.into())
184    }
185}
186
187impl PartialOrd for DateTime {
188    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189        Some(self.cmp(other))
190    }
191}
192
193impl PartialEq for DateTime {
194    fn eq(&self, other: &Self) -> bool {
195        self.cmp(other) == Ordering::Equal
196    }
197}
198
199impl Eq for DateTime {}
200
201
202impl std::ops::Sub for DateTime {
203    type Output = Time;
204
205    fn sub(self, rhs: Self) -> Self::Output {
206        self.time + Time::from_hours((self.date - rhs.date) * HOURS_IN_DAY) - rhs.time
207    }
208}
209
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::constants::MINUTES_IN_HOUR;
215
216    #[test]
217    fn test_from_seconds_since_unix_epoch(){
218        let date_time = DateTime::new(Date::new(2021, 4, 13),
219                  Time::new(20, 55, 50), 0);
220        assert_eq!(DateTime::from_seconds_since_unix_epoch(1618347350), date_time);
221        assert_eq!(date_time.to_seconds_from_unix_epoch(), 1618347350);
222    }
223
224    #[test]
225    fn test_date_time_cmp() {
226        let mut lhs = DateTime::new(Date::new(2019, 12,31), Time::new(12, 0, 0), 0);
227        let mut rhs = lhs;
228        assert_eq!(lhs, rhs);
229        rhs.time.hour += 1;
230        assert!(lhs < rhs);
231        lhs.shift_minutes = -60;
232        assert_eq!(lhs, rhs);
233    }
234
235    #[test]
236    fn test_date_time_to_string() {
237        let date_time = DateTime::new(Date::new(2021, 7,28),
238                                    Time::new(10, 0, 0), -4 * MINUTES_IN_HOUR as isize);
239        assert_eq!(date_time.to_iso_8061(), "2021-07-28T10:00:00-04:00");
240        assert_eq!(date_time.to_string(), "2021-07-28 10:00:00");
241    }
242
243    #[test]
244    fn test_shift_from_str() -> Result<(), DateError>{
245        assert_eq!(DateTime::shift_from_str("+4:30")?, 270);
246        assert_eq!(DateTime::shift_from_str("-4:30")?, -270);
247        assert_eq!(DateTime::shift_from_str("Z")?, 0);
248        Ok(())
249    }
250
251    #[test]
252    fn test_date_time_from_str() -> Result<(), DateError>{
253        assert_eq!("2021-07-28T10:00:00-4:00".parse::<DateTime>()?,
254                   DateTime::new(Date::new(2021, 7,28),
255                          Time::new(10, 0, 0), -4 * MINUTES_IN_HOUR as isize));
256
257        assert_eq!("2021-07-28T10:00:00+02:00".parse::<DateTime>()?,
258                   DateTime::new(Date::new(2021, 7,28),
259                            Time::new(10, 0, 0), 2 * MINUTES_IN_HOUR as isize));
260
261        assert_eq!("2021-07-28T10:00:00Z".parse::<DateTime>()?,
262                   DateTime::new(Date::new(2021, 7,28),
263                              Time::new(10, 0, 0), 0));
264        assert_eq!("2020-01-09T21:10:05.779325Z".parse::<DateTime>()?,
265                   DateTime::new(Date::new(2020, 1,9),
266                                 Time::new_with_microseconds(21, 10, 5, 779325), 0));
267
268        Ok(())
269    }
270
271    #[test]
272    fn test_to_seconds_since_unix_epoch_gmt(){
273        let date_time = DateTime::new(Date::new(2023, 1, 13),
274              Time::new(8, 40, 42), -5 * MINUTES_IN_HOUR as isize);
275        assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
276        let date_time = DateTime::new(Date::new(2023, 1, 13),
277          Time::new(14, 40, 42), 1 * MINUTES_IN_HOUR as isize);
278        assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
279    }
280
281    #[test]
282    fn test_date_time_normalize(){
283        let date_time = DateTime::new(Date::new(2023, 1, 13),
284                                      Time::new(24, 0, 42), -5 * MINUTES_IN_HOUR as isize);
285        let date_time2 = DateTime::new(Date::new(2023, 1, 14),
286                                      Time::new(0, 0, 42), -5 * MINUTES_IN_HOUR as isize);
287        assert_eq!(date_time.normalize(), date_time2);
288    }
289
290    #[test]
291    fn test_date_time_from_str_invalid() {
292        // Invalid datetime string formats
293        assert!("invalid".parse::<DateTime>().is_err());
294        assert!("2020-01-01".parse::<DateTime>().is_err());
295        assert!("2020-01-01T".parse::<DateTime>().is_err());
296        assert!("2020-01-01T12:00:00".parse::<DateTime>().is_err());
297    }
298
299    #[test]
300    fn test_shift_string_formatting() {
301        let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
302        assert_eq!(dt_utc.shift_string(), "Z");
303        
304        let dt_plus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 120);
305        assert_eq!(dt_plus.shift_string(), "+02:00");
306        
307        let dt_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), -300);
308        assert_eq!(dt_minus.shift_string(), "-05:00");
309        
310        let dt_plus_30 = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 30);
311        assert_eq!(dt_plus_30.shift_string(), "+00:30");
312    }
313
314    #[test]
315    fn test_iso_8601_formatting() {
316        let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 0);
317        assert_eq!(dt.to_iso_8061(), "2020-01-01T12:30:45Z");
318        
319        let dt_tz = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 120);
320        assert_eq!(dt_tz.to_iso_8061(), "2020-01-01T12:30:45+02:00");
321        
322        let dt_tz_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), -300);
323        assert_eq!(dt_tz_minus.to_iso_8061(), "2020-01-01T12:30:45-05:00");
324    }
325
326    #[test]
327    fn test_date_time_arithmetic() {
328        let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
329        
330        let dt_plus_sec = dt.add_seconds(3600); // Add 1 hour
331        assert_eq!(dt_plus_sec.time.hour, 13);
332        assert_eq!(dt_plus_sec.date, Date::new(2020, 1, 1));
333        
334        let dt_plus_day = dt.add_seconds(86400); // Add 1 day
335        assert_eq!(dt_plus_day.date, Date::new(2020, 1, 2));
336        assert_eq!(dt_plus_day.time, Time::new(12, 0, 0));
337        
338        let time_to_add = Time::new(2, 30, 0);
339        let dt_plus_time = dt.add_time(time_to_add);
340        assert_eq!(dt_plus_time.time, Time::new(14, 30, 0));
341    }
342
343    #[test]
344    fn test_date_time_subtraction() {
345        let dt1 = DateTime::new(Date::new(2020, 1, 2), Time::new(12, 0, 0), 0);
346        let dt2 = DateTime::new(Date::new(2020, 1, 1), Time::new(10, 0, 0), 0);
347        
348        let diff = dt1 - dt2;
349        assert_eq!(diff, Time::new(26, 0, 0)); // 1 day + 2 hours
350    }
351
352    #[test]
353    fn test_timezone_shift_operations() {
354        let mut dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
355        
356        dt.set_shift(120); // UTC+2
357        assert_eq!(dt.shift_minutes, 120);
358        assert_eq!(dt.time, Time::new(14, 0, 0)); // Time should adjust
359        
360        dt.set_shift(-300); // UTC-5
361        assert_eq!(dt.shift_minutes, -300);
362        assert_eq!(dt.time, Time::new(7, 0, 0)); // Time should adjust
363    }
364
365    #[test]
366    fn test_unix_epoch_conversions() {
367        let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
368        assert_eq!(dt.to_seconds_from_unix_epoch(), 0);
369        
370        let dt_from_epoch = DateTime::from_seconds_since_unix_epoch(0);
371        assert_eq!(dt_from_epoch.date, Date::new(1970, 1, 1));
372        assert_eq!(dt_from_epoch.time, Time::new(0, 0, 0));
373        
374        let dt_tz = DateTime::new(Date::new(1970, 1, 1), Time::new(12, 0, 0), 0);
375        let gmt_seconds = dt_tz.to_seconds_from_unix_epoch_gmt();
376        assert_eq!(gmt_seconds, 43200); // 12 hours in seconds
377    }
378
379    #[test]
380    fn test_date_time_comparison_with_timezone() {
381        let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
382        let dt_est = DateTime::new(Date::new(2020, 1, 1), Time::new(7, 0, 0), -300);
383        assert_eq!(dt_utc, dt_est);
384        let dt_different = DateTime::new(Date::new(2020, 1, 1), Time::new(8, 0, 0), -300);
385        assert_ne!(dt_utc, dt_different);
386    }
387
388    #[test]
389    fn test_edge_cases() {
390        let dt_leap = DateTime::new(Date::new(2020, 2, 29), Time::new(12, 0, 0), 0);
391        assert!(dt_leap.date.valid());
392        let dt_year_end = DateTime::new(Date::new(2020, 12, 31), Time::new(23, 59, 59), 0);
393        let dt_next_year = dt_year_end.add_seconds(1);
394        assert_eq!(dt_next_year.date, Date::new(2021, 1, 1));
395        assert_eq!(dt_next_year.time, Time::new(0, 0, 0));
396    }
397}