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); 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); 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)); }
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); assert_eq!(dt.shift_minutes, 120);
351 assert_eq!(dt.time, Time::new(14, 0, 0)); dt.set_shift(-300); assert_eq!(dt.shift_minutes, -300);
355 assert_eq!(dt.time, Time::new(7, 0, 0)); }
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); }
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}