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