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