1use crate::{Error, ErrorKind, Result, Tag, Writer};
9use core::{fmt, str::FromStr, time::Duration};
10
11#[cfg(feature = "std")]
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use const_range::const_contains_u8;
15#[cfg(feature = "time")]
16use time::PrimitiveDateTime;
17
18const MIN_YEAR: u16 = 1970;
20
21const MAX_UNIX_DURATION: Duration = Duration::from_secs(253_402_300_799);
26
27#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
34pub struct DateTime {
35 year: u16,
39
40 month: u8,
42
43 day: u8,
45
46 hour: u8,
48
49 minutes: u8,
51
52 seconds: u8,
54
55 unix_duration: Duration,
57}
58
59impl DateTime {
60 pub const INFINITY: DateTime = DateTime {
63 year: 9999,
64 month: 12,
65 day: 31,
66 hour: 23,
67 minutes: 59,
68 seconds: 59,
69 unix_duration: MAX_UNIX_DURATION,
70 };
71
72 pub const fn new(
77 year: u16,
78 month: u8,
79 day: u8,
80 hour: u8,
81 minutes: u8,
82 seconds: u8,
83 ) -> Result<Self> {
84 match Self::from_ymd_hms(year, month, day, hour, minutes, seconds) {
85 Some(date) => Ok(date),
86 None => Err(Error::from_kind(ErrorKind::DateTime)),
87 }
88 }
89
90 #[allow(clippy::arithmetic_side_effects)]
95 pub(crate) const fn from_ymd_hms(
96 year: u16,
97 month: u8,
98 day: u8,
99 hour: u8,
100 minutes: u8,
101 seconds: u8,
102 ) -> Option<Self> {
103 if year < MIN_YEAR
105 || !const_contains_u8(1..=12, month)
106 || !const_contains_u8(1..=31, day)
107 || !const_contains_u8(0..=23, hour)
108 || !const_contains_u8(0..=59, minutes)
109 || !const_contains_u8(0..=59, seconds)
110 {
111 return None;
112 }
113
114 let leap_years =
115 ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
116
117 let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
118
119 let (mut ydays, mdays): (u16, u8) = match month {
120 1 => (0, 31),
121 2 if is_leap_year => (31, 29),
122 2 => (31, 28),
123 3 => (59, 31),
124 4 => (90, 30),
125 5 => (120, 31),
126 6 => (151, 30),
127 7 => (181, 31),
128 8 => (212, 31),
129 9 => (243, 30),
130 10 => (273, 31),
131 11 => (304, 30),
132 12 => (334, 31),
133 _ => return None,
134 };
135
136 if day > mdays || day == 0 {
137 return None;
138 }
139
140 ydays += day as u16 - 1;
141
142 if is_leap_year && month > 2 {
143 ydays += 1;
144 }
145
146 let days = ((year - 1970) as u64) * 365 + leap_years as u64 + ydays as u64;
147 let time = seconds as u64 + (minutes as u64 * 60) + (hour as u64 * 3600);
148 let unix_duration = Duration::from_secs(time + days * 86400);
149
150 if unix_duration.as_secs() > MAX_UNIX_DURATION.as_secs() {
151 return None;
152 }
153
154 Some(Self {
155 year,
156 month,
157 day,
158 hour,
159 minutes,
160 seconds,
161 unix_duration,
162 })
163 }
164
165 #[allow(clippy::arithmetic_side_effects)]
171 pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
172 if unix_duration > MAX_UNIX_DURATION {
173 return Err(ErrorKind::DateTime.into());
174 }
175
176 let secs_since_epoch = unix_duration.as_secs();
177
178 const LEAPOCH: i64 = 11017;
180 const DAYS_PER_400Y: i64 = 365 * 400 + 97;
181 const DAYS_PER_100Y: i64 = 365 * 100 + 24;
182 const DAYS_PER_4Y: i64 = 365 * 4 + 1;
183
184 let days = i64::try_from(secs_since_epoch / 86400)? - LEAPOCH;
185 let secs_of_day = secs_since_epoch % 86400;
186
187 let mut qc_cycles = days / DAYS_PER_400Y;
188 let mut remdays = days % DAYS_PER_400Y;
189
190 if remdays < 0 {
191 remdays += DAYS_PER_400Y;
192 qc_cycles -= 1;
193 }
194
195 let mut c_cycles = remdays / DAYS_PER_100Y;
196 if c_cycles == 4 {
197 c_cycles -= 1;
198 }
199 remdays -= c_cycles * DAYS_PER_100Y;
200
201 let mut q_cycles = remdays / DAYS_PER_4Y;
202 if q_cycles == 25 {
203 q_cycles -= 1;
204 }
205 remdays -= q_cycles * DAYS_PER_4Y;
206
207 let mut remyears = remdays / 365;
208 if remyears == 4 {
209 remyears -= 1;
210 }
211 remdays -= remyears * 365;
212
213 let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
214
215 let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
216 let mut mon = 0;
217 for mon_len in months.iter() {
218 mon += 1;
219 if remdays < *mon_len {
220 break;
221 }
222 remdays -= *mon_len;
223 }
224 let mday = remdays + 1;
225 let mon = if mon + 2 > 12 {
226 year += 1;
227 mon - 10
228 } else {
229 mon + 2
230 };
231
232 let second = secs_of_day % 60;
233 let mins_of_day = secs_of_day / 60;
234 let minute = mins_of_day % 60;
235 let hour = mins_of_day / 60;
236
237 Self::new(
238 year.try_into()?,
239 mon,
240 mday.try_into()?,
241 hour.try_into()?,
242 minute.try_into()?,
243 second.try_into()?,
244 )
245 }
246
247 #[must_use]
249 pub fn year(&self) -> u16 {
250 self.year
251 }
252
253 #[must_use]
255 pub fn month(&self) -> u8 {
256 self.month
257 }
258
259 #[must_use]
261 pub fn day(&self) -> u8 {
262 self.day
263 }
264
265 #[must_use]
267 pub fn hour(&self) -> u8 {
268 self.hour
269 }
270
271 #[must_use]
273 pub fn minutes(&self) -> u8 {
274 self.minutes
275 }
276
277 #[must_use]
279 pub fn seconds(&self) -> u8 {
280 self.seconds
281 }
282
283 #[must_use]
285 pub fn unix_duration(&self) -> Duration {
286 self.unix_duration
287 }
288
289 #[cfg(feature = "std")]
294 pub fn from_system_time(time: SystemTime) -> Result<Self> {
295 time.duration_since(UNIX_EPOCH)
296 .map_err(|_| ErrorKind::DateTime.into())
297 .and_then(Self::from_unix_duration)
298 }
299
300 #[cfg(feature = "std")]
302 #[must_use]
303 pub fn to_system_time(&self) -> SystemTime {
304 UNIX_EPOCH + self.unix_duration()
305 }
306}
307
308impl FromStr for DateTime {
309 type Err = Error;
310
311 fn from_str(s: &str) -> Result<Self> {
312 match *s.as_bytes() {
313 [
314 year1,
315 year2,
316 year3,
317 year4,
318 b'-',
319 month1,
320 month2,
321 b'-',
322 day1,
323 day2,
324 b'T',
325 hour1,
326 hour2,
327 b':',
328 min1,
329 min2,
330 b':',
331 sec1,
332 sec2,
333 b'Z',
334 ] => {
335 let tag = Tag::GeneralizedTime;
336 let year = decode_year([year1, year2, year3, year4])?;
337 let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
338 let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
339 let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
340 let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
341 let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
342 Self::new(year, month, day, hour, minutes, seconds)
343 }
344 _ => Err(ErrorKind::DateTime.into()),
345 }
346 }
347}
348
349impl fmt::Display for DateTime {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 write!(
352 f,
353 "{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
354 self.year, self.month, self.day, self.hour, self.minutes, self.seconds
355 )
356 }
357}
358
359#[cfg(feature = "std")]
360impl From<DateTime> for SystemTime {
361 fn from(time: DateTime) -> SystemTime {
362 time.to_system_time()
363 }
364}
365
366#[cfg(feature = "std")]
367impl From<&DateTime> for SystemTime {
368 fn from(time: &DateTime) -> SystemTime {
369 time.to_system_time()
370 }
371}
372
373#[cfg(feature = "std")]
374impl TryFrom<SystemTime> for DateTime {
375 type Error = Error;
376
377 fn try_from(time: SystemTime) -> Result<DateTime> {
378 DateTime::from_system_time(time)
379 }
380}
381
382#[cfg(feature = "std")]
383impl TryFrom<&SystemTime> for DateTime {
384 type Error = Error;
385
386 fn try_from(time: &SystemTime) -> Result<DateTime> {
387 DateTime::from_system_time(*time)
388 }
389}
390
391#[cfg(feature = "time")]
392impl TryFrom<DateTime> for PrimitiveDateTime {
393 type Error = Error;
394
395 fn try_from(time: DateTime) -> Result<PrimitiveDateTime> {
396 let month = time.month().try_into()?;
397 let date = time::Date::from_calendar_date(i32::from(time.year()), month, time.day())?;
398 let time = time::Time::from_hms(time.hour(), time.minutes(), time.seconds())?;
399
400 Ok(PrimitiveDateTime::new(date, time))
401 }
402}
403
404#[cfg(feature = "time")]
405impl TryFrom<PrimitiveDateTime> for DateTime {
406 type Error = Error;
407
408 fn try_from(time: PrimitiveDateTime) -> Result<DateTime> {
409 DateTime::new(
410 time.year().try_into().map_err(|_| ErrorKind::DateTime)?,
411 time.month().into(),
412 time.day(),
413 time.hour(),
414 time.minute(),
415 time.second(),
416 )
417 }
418}
419
420#[cfg(feature = "arbitrary")]
423impl<'a> arbitrary::Arbitrary<'a> for DateTime {
424 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
425 Self::from_unix_duration(Duration::new(
426 u.int_in_range(0..=MAX_UNIX_DURATION.as_secs().saturating_sub(1))?,
427 u.int_in_range(0..=999_999_999)?,
428 ))
429 .map_err(|_| arbitrary::Error::IncorrectFormat)
430 }
431
432 fn size_hint(depth: usize) -> (usize, Option<usize>) {
433 arbitrary::size_hint::and(u64::size_hint(depth), u32::size_hint(depth))
434 }
435}
436
437#[allow(clippy::arithmetic_side_effects)]
440pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
441 if hi.is_ascii_digit() && lo.is_ascii_digit() {
442 Ok((hi - b'0') * 10 + (lo - b'0'))
443 } else {
444 Err(tag.value_error().into())
445 }
446}
447
448pub(crate) fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
450where
451 W: Writer + ?Sized,
452{
453 let hi_val = value / 10;
454
455 if hi_val >= 10 {
456 return Err(tag.value_error().into());
457 }
458
459 writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
460 writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
461}
462
463#[allow(clippy::arithmetic_side_effects)]
466fn decode_year(year: [u8; 4]) -> Result<u16> {
467 let tag = Tag::GeneralizedTime;
468 let hi = decode_decimal(tag, year[0], year[1]).map_err(|_| ErrorKind::DateTime)?;
469 let lo = decode_decimal(tag, year[2], year[3]).map_err(|_| ErrorKind::DateTime)?;
470 Ok(u16::from(hi) * 100 + u16::from(lo))
471}
472
473mod const_range {
474 use core::ops::RangeInclusive;
475
476 #[inline]
478 pub const fn const_contains_u8(range: RangeInclusive<u8>, item: u8) -> bool {
479 item >= *range.start() && item <= *range.end()
480 }
481}
482
483#[cfg(test)]
484#[allow(clippy::unwrap_used)]
485mod tests {
486 use super::DateTime;
487
488 fn is_date_valid(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> bool {
490 DateTime::new(year, month, day, hour, minute, second).is_ok()
491 }
492
493 #[test]
494 fn feb_leap_year_handling() {
495 assert!(is_date_valid(2000, 2, 29, 0, 0, 0));
496 assert!(!is_date_valid(2001, 2, 29, 0, 0, 0));
497 assert!(!is_date_valid(2100, 2, 29, 0, 0, 0));
498 }
499
500 #[test]
501 fn invalid_dates() {
502 assert!(!is_date_valid(2, 3, 25, 0, 0, 0));
503
504 assert!(is_date_valid(1970, 1, 26, 0, 0, 0));
505 assert!(!is_date_valid(1969, 1, 26, 0, 0, 0));
506 assert!(!is_date_valid(1968, 1, 26, 0, 0, 0));
507 assert!(!is_date_valid(1600, 1, 26, 0, 0, 0));
508
509 assert!(is_date_valid(2039, 2, 27, 0, 0, 0));
510 assert!(!is_date_valid(2039, 2, 27, 255, 0, 0));
511 assert!(!is_date_valid(2039, 2, 27, 0, 255, 0));
512 assert!(!is_date_valid(2039, 2, 27, 0, 0, 255));
513
514 assert!(is_date_valid(2055, 12, 31, 0, 0, 0));
515 assert!(is_date_valid(2055, 12, 31, 23, 0, 0));
516 assert!(!is_date_valid(2055, 12, 31, 24, 0, 0));
517 assert!(is_date_valid(2055, 12, 31, 0, 59, 0));
518 assert!(!is_date_valid(2055, 12, 31, 0, 60, 0));
519 assert!(is_date_valid(2055, 12, 31, 0, 0, 59));
520 assert!(!is_date_valid(2055, 12, 31, 0, 0, 60));
521 }
522
523 #[test]
524 fn from_str() {
525 let datetime = "2001-01-02T12:13:14Z".parse::<DateTime>().unwrap();
526 assert_eq!(datetime.year(), 2001);
527 assert_eq!(datetime.month(), 1);
528 assert_eq!(datetime.day(), 2);
529 assert_eq!(datetime.hour(), 12);
530 assert_eq!(datetime.minutes(), 13);
531 assert_eq!(datetime.seconds(), 14);
532 }
533
534 #[cfg(feature = "alloc")]
535 #[test]
536 fn display() {
537 use alloc::string::ToString;
538 let datetime = DateTime::new(2001, 1, 2, 12, 13, 14).unwrap();
539 assert_eq!(&datetime.to_string(), "2001-01-02T12:13:14Z");
540 }
541}