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
14#[cfg(feature = "time")]
15use time::PrimitiveDateTime;
16
17const MIN_YEAR: u16 = 1970;
19
20const MAX_UNIX_DURATION: Duration = Duration::from_secs(253_402_300_799);
25
26#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
33pub struct DateTime {
34 year: u16,
38
39 month: u8,
41
42 day: u8,
44
45 hour: u8,
47
48 minutes: u8,
50
51 seconds: u8,
53
54 unix_duration: Duration,
56}
57
58impl DateTime {
59 pub const INFINITY: DateTime = DateTime {
62 year: 9999,
63 month: 12,
64 day: 31,
65 hour: 23,
66 minutes: 59,
67 seconds: 59,
68 unix_duration: MAX_UNIX_DURATION,
69 };
70
71 #[allow(clippy::integer_arithmetic)]
74 pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result<Self> {
75 if year < MIN_YEAR
77 || !(1..=12).contains(&month)
78 || !(1..=31).contains(&day)
79 || !(0..=23).contains(&hour)
80 || !(0..=59).contains(&minutes)
81 || !(0..=59).contains(&seconds)
82 {
83 return Err(ErrorKind::DateTime.into());
84 }
85
86 let leap_years =
87 ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
88
89 let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
90
91 let (mut ydays, mdays): (u16, u8) = match month {
92 1 => (0, 31),
93 2 if is_leap_year => (31, 29),
94 2 => (31, 28),
95 3 => (59, 31),
96 4 => (90, 30),
97 5 => (120, 31),
98 6 => (151, 30),
99 7 => (181, 31),
100 8 => (212, 31),
101 9 => (243, 30),
102 10 => (273, 31),
103 11 => (304, 30),
104 12 => (334, 31),
105 _ => return Err(ErrorKind::DateTime.into()),
106 };
107
108 if day > mdays || day == 0 {
109 return Err(ErrorKind::DateTime.into());
110 }
111
112 ydays += u16::from(day) - 1;
113
114 if is_leap_year && month > 2 {
115 ydays += 1;
116 }
117
118 let days = u64::from(year - 1970) * 365 + u64::from(leap_years) + u64::from(ydays);
119 let time = u64::from(seconds) + (u64::from(minutes) * 60) + (u64::from(hour) * 3600);
120 let unix_duration = Duration::from_secs(time + days * 86400);
121
122 if unix_duration > MAX_UNIX_DURATION {
123 return Err(ErrorKind::DateTime.into());
124 }
125
126 Ok(Self {
127 year,
128 month,
129 day,
130 hour,
131 minutes,
132 seconds,
133 unix_duration,
134 })
135 }
136
137 #[allow(clippy::integer_arithmetic)]
142 pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
143 if unix_duration > MAX_UNIX_DURATION {
144 return Err(ErrorKind::DateTime.into());
145 }
146
147 let secs_since_epoch = unix_duration.as_secs();
148
149 const LEAPOCH: i64 = 11017;
151 const DAYS_PER_400Y: i64 = 365 * 400 + 97;
152 const DAYS_PER_100Y: i64 = 365 * 100 + 24;
153 const DAYS_PER_4Y: i64 = 365 * 4 + 1;
154
155 let days = i64::try_from(secs_since_epoch / 86400)? - LEAPOCH;
156 let secs_of_day = secs_since_epoch % 86400;
157
158 let mut qc_cycles = days / DAYS_PER_400Y;
159 let mut remdays = days % DAYS_PER_400Y;
160
161 if remdays < 0 {
162 remdays += DAYS_PER_400Y;
163 qc_cycles -= 1;
164 }
165
166 let mut c_cycles = remdays / DAYS_PER_100Y;
167 if c_cycles == 4 {
168 c_cycles -= 1;
169 }
170 remdays -= c_cycles * DAYS_PER_100Y;
171
172 let mut q_cycles = remdays / DAYS_PER_4Y;
173 if q_cycles == 25 {
174 q_cycles -= 1;
175 }
176 remdays -= q_cycles * DAYS_PER_4Y;
177
178 let mut remyears = remdays / 365;
179 if remyears == 4 {
180 remyears -= 1;
181 }
182 remdays -= remyears * 365;
183
184 let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
185
186 let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
187 let mut mon = 0;
188 for mon_len in months.iter() {
189 mon += 1;
190 if remdays < *mon_len {
191 break;
192 }
193 remdays -= *mon_len;
194 }
195 let mday = remdays + 1;
196 let mon = if mon + 2 > 12 {
197 year += 1;
198 mon - 10
199 } else {
200 mon + 2
201 };
202
203 let second = secs_of_day % 60;
204 let mins_of_day = secs_of_day / 60;
205 let minute = mins_of_day % 60;
206 let hour = mins_of_day / 60;
207
208 Self::new(
209 year.try_into()?,
210 mon,
211 mday.try_into()?,
212 hour.try_into()?,
213 minute.try_into()?,
214 second.try_into()?,
215 )
216 }
217
218 pub fn year(&self) -> u16 {
220 self.year
221 }
222
223 pub fn month(&self) -> u8 {
225 self.month
226 }
227
228 pub fn day(&self) -> u8 {
230 self.day
231 }
232
233 pub fn hour(&self) -> u8 {
235 self.hour
236 }
237
238 pub fn minutes(&self) -> u8 {
240 self.minutes
241 }
242
243 pub fn seconds(&self) -> u8 {
245 self.seconds
246 }
247
248 pub fn unix_duration(&self) -> Duration {
250 self.unix_duration
251 }
252
253 #[cfg(feature = "std")]
255 pub fn from_system_time(time: SystemTime) -> Result<Self> {
256 time.duration_since(UNIX_EPOCH)
257 .map_err(|_| ErrorKind::DateTime.into())
258 .and_then(Self::from_unix_duration)
259 }
260
261 #[cfg(feature = "std")]
263 pub fn to_system_time(&self) -> SystemTime {
264 UNIX_EPOCH + self.unix_duration()
265 }
266}
267
268impl FromStr for DateTime {
269 type Err = Error;
270
271 fn from_str(s: &str) -> Result<Self> {
272 match *s.as_bytes() {
273 [year1, year2, year3, year4, b'-', month1, month2, b'-', day1, day2, b'T', hour1, hour2, b':', min1, min2, b':', sec1, sec2, b'Z'] =>
274 {
275 let tag = Tag::GeneralizedTime;
276 let year = decode_year(&[year1, year2, year3, year4])?;
277 let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
278 let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
279 let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
280 let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
281 let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
282 Self::new(year, month, day, hour, minutes, seconds)
283 }
284 _ => Err(ErrorKind::DateTime.into()),
285 }
286 }
287}
288
289impl fmt::Display for DateTime {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 write!(
292 f,
293 "{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
294 self.year, self.month, self.day, self.hour, self.minutes, self.seconds
295 )
296 }
297}
298
299#[cfg(feature = "std")]
300impl From<DateTime> for SystemTime {
301 fn from(time: DateTime) -> SystemTime {
302 time.to_system_time()
303 }
304}
305
306#[cfg(feature = "std")]
307impl From<&DateTime> for SystemTime {
308 fn from(time: &DateTime) -> SystemTime {
309 time.to_system_time()
310 }
311}
312
313#[cfg(feature = "std")]
314impl TryFrom<SystemTime> for DateTime {
315 type Error = Error;
316
317 fn try_from(time: SystemTime) -> Result<DateTime> {
318 DateTime::from_system_time(time)
319 }
320}
321
322#[cfg(feature = "std")]
323impl TryFrom<&SystemTime> for DateTime {
324 type Error = Error;
325
326 fn try_from(time: &SystemTime) -> Result<DateTime> {
327 DateTime::from_system_time(*time)
328 }
329}
330
331#[cfg(feature = "time")]
332impl TryFrom<DateTime> for PrimitiveDateTime {
333 type Error = Error;
334
335 fn try_from(time: DateTime) -> Result<PrimitiveDateTime> {
336 let month = time.month().try_into()?;
337 let date = time::Date::from_calendar_date(i32::from(time.year()), month, time.day())?;
338 let time = time::Time::from_hms(time.hour(), time.minutes(), time.seconds())?;
339
340 Ok(PrimitiveDateTime::new(date, time))
341 }
342}
343
344#[cfg(feature = "time")]
345impl TryFrom<PrimitiveDateTime> for DateTime {
346 type Error = Error;
347
348 fn try_from(time: PrimitiveDateTime) -> Result<DateTime> {
349 DateTime::new(
350 time.year().try_into().map_err(|_| ErrorKind::DateTime)?,
351 time.month().into(),
352 time.day(),
353 time.hour(),
354 time.minute(),
355 time.second(),
356 )
357 }
358}
359
360#[cfg(feature = "arbitrary")]
363impl<'a> arbitrary::Arbitrary<'a> for DateTime {
364 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
365 Self::from_unix_duration(Duration::new(
366 u.int_in_range(0..=MAX_UNIX_DURATION.as_secs().saturating_sub(1))?,
367 u.int_in_range(0..=999_999_999)?,
368 ))
369 .map_err(|_| arbitrary::Error::IncorrectFormat)
370 }
371
372 fn size_hint(depth: usize) -> (usize, Option<usize>) {
373 arbitrary::size_hint::and(u64::size_hint(depth), u32::size_hint(depth))
374 }
375}
376
377#[allow(clippy::integer_arithmetic)]
380pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
381 if hi.is_ascii_digit() && lo.is_ascii_digit() {
382 Ok((hi - b'0') * 10 + (lo - b'0'))
383 } else {
384 Err(tag.value_error())
385 }
386}
387
388pub(crate) fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
390where
391 W: Writer + ?Sized,
392{
393 let hi_val = value / 10;
394
395 if hi_val >= 10 {
396 return Err(tag.value_error());
397 }
398
399 writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
400 writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
401}
402
403#[allow(clippy::integer_arithmetic)]
406fn decode_year(year: &[u8; 4]) -> Result<u16> {
407 let tag = Tag::GeneralizedTime;
408 let hi = decode_decimal(tag, year[0], year[1]).map_err(|_| ErrorKind::DateTime)?;
409 let lo = decode_decimal(tag, year[2], year[3]).map_err(|_| ErrorKind::DateTime)?;
410 Ok(u16::from(hi) * 100 + u16::from(lo))
411}
412
413#[cfg(test)]
414mod tests {
415 use super::DateTime;
416
417 fn is_date_valid(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> bool {
419 DateTime::new(year, month, day, hour, minute, second).is_ok()
420 }
421
422 #[test]
423 fn feb_leap_year_handling() {
424 assert!(is_date_valid(2000, 2, 29, 0, 0, 0));
425 assert!(!is_date_valid(2001, 2, 29, 0, 0, 0));
426 assert!(!is_date_valid(2100, 2, 29, 0, 0, 0));
427 }
428
429 #[test]
430 fn from_str() {
431 let datetime = "2001-01-02T12:13:14Z".parse::<DateTime>().unwrap();
432 assert_eq!(datetime.year(), 2001);
433 assert_eq!(datetime.month(), 1);
434 assert_eq!(datetime.day(), 2);
435 assert_eq!(datetime.hour(), 12);
436 assert_eq!(datetime.minutes(), 13);
437 assert_eq!(datetime.seconds(), 14);
438 }
439
440 #[cfg(feature = "alloc")]
441 #[test]
442 fn display() {
443 use alloc::string::ToString;
444 let datetime = DateTime::new(2001, 01, 02, 12, 13, 14).unwrap();
445 assert_eq!(&datetime.to_string(), "2001-01-02T12:13:14Z");
446 }
447}