Skip to main content

zip/
datetime.rs

1//! Code related to `DateTime` in zip files
2
3use crate::result::DateTimeRangeError;
4use core::fmt;
5
6/// Representation of a moment in time.
7///
8/// Zip files use an old format from DOS to store timestamps,
9/// with its own set of peculiarities.
10/// For example, it has a resolution of 2 seconds!
11///
12/// A [`DateTime`] can be stored directly in a zipfile with
13/// [`FileOptions::last_modified_time`](crate::read::ZipFile::last_modified), or
14/// read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified).
15///
16/// # Warning
17///
18/// Because there is no timezone associated with the [`DateTime`], they should ideally only
19/// be used for user-facing descriptions.
20///
21/// Modern zip files store more precise timestamps; see [`crate::extra_fields::ExtendedTimestamp`]
22/// for details.
23#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
24pub struct DateTime {
25    datepart: u16,
26    timepart: u16,
27}
28
29impl fmt::Debug for DateTime {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        if *self == Self::default() {
32            return f.write_str("DateTime::default()");
33        }
34        f.write_fmt(format_args!(
35            "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?",
36            self.year(),
37            self.month(),
38            self.day(),
39            self.hour(),
40            self.minute(),
41            self.second()
42        ))
43    }
44}
45
46#[cfg(feature = "_arbitrary")]
47impl arbitrary::Arbitrary<'_> for DateTime {
48    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
49        // DOS time format stores seconds divided by 2 in a 5-bit field (0..=29),
50        // so the maximum representable second value is 58.
51        const MAX_DOS_SECONDS: u16 = 58;
52
53        let year: u16 = u.int_in_range(1980..=2107)?;
54        let month: u16 = u.int_in_range(1..=12)?;
55        let day: u16 = u.int_in_range(1..=31)?;
56        let datepart = day | (month << 5) | ((year - 1980) << 9);
57        let hour: u16 = u.int_in_range(0..=23)?;
58        let minute: u16 = u.int_in_range(0..=59)?;
59        let second: u16 = u.int_in_range(0..=MAX_DOS_SECONDS)?;
60        let timepart = (second >> 1) | (minute << 5) | (hour << 11);
61        Ok(DateTime { datepart, timepart })
62    }
63}
64
65#[cfg(feature = "chrono")]
66impl TryFrom<chrono::NaiveDateTime> for DateTime {
67    type Error = DateTimeRangeError;
68
69    fn try_from(value: chrono::NaiveDateTime) -> Result<Self, Self::Error> {
70        use chrono::{Datelike, Timelike};
71
72        DateTime::from_date_and_time(
73            value.year().try_into()?,
74            value.month().try_into()?,
75            value.day().try_into()?,
76            value.hour().try_into()?,
77            value.minute().try_into()?,
78            value.second().try_into()?,
79        )
80    }
81}
82
83#[cfg(feature = "chrono")]
84impl TryFrom<DateTime> for chrono::NaiveDateTime {
85    type Error = DateTimeRangeError;
86
87    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
88        let date = chrono::NaiveDate::from_ymd_opt(
89            value.year().into(),
90            value.month().into(),
91            value.day().into(),
92        )
93        .ok_or(DateTimeRangeError)?;
94        let time = chrono::NaiveTime::from_hms_opt(
95            value.hour().into(),
96            value.minute().into(),
97            value.second().into(),
98        )
99        .ok_or(DateTimeRangeError)?;
100        Ok(chrono::NaiveDateTime::new(date, time))
101    }
102}
103
104#[cfg(feature = "jiff-02")]
105impl TryFrom<jiff::civil::DateTime> for DateTime {
106    type Error = DateTimeRangeError;
107
108    fn try_from(value: jiff::civil::DateTime) -> Result<Self, Self::Error> {
109        Self::from_date_and_time(
110            value.year().try_into()?,
111            value.month() as u8,
112            value.day() as u8,
113            value.hour() as u8,
114            value.minute() as u8,
115            value.second() as u8,
116        )
117    }
118}
119
120#[cfg(feature = "jiff-02")]
121impl TryFrom<DateTime> for jiff::civil::DateTime {
122    type Error = jiff::Error;
123
124    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
125        Self::new(
126            value.year() as i16,
127            value.month() as i8,
128            value.day() as i8,
129            value.hour() as i8,
130            value.minute() as i8,
131            value.second() as i8,
132            0,
133        )
134    }
135}
136
137impl TryFrom<(u16, u16)> for DateTime {
138    type Error = DateTimeRangeError;
139
140    #[inline]
141    fn try_from(values: (u16, u16)) -> Result<Self, Self::Error> {
142        Self::try_from_msdos(values.0, values.1)
143    }
144}
145
146impl From<DateTime> for (u16, u16) {
147    #[inline]
148    fn from(dt: DateTime) -> Self {
149        (dt.datepart(), dt.timepart())
150    }
151}
152
153impl Default for DateTime {
154    /// Constructs an 'default' datetime of 1980-01-01 00:00:00
155    fn default() -> DateTime {
156        DateTime::DEFAULT
157    }
158}
159
160impl fmt::Display for DateTime {
161    #[inline]
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(
164            f,
165            "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
166            self.year(),
167            self.month(),
168            self.day(),
169            self.hour(),
170            self.minute(),
171            self.second()
172        )
173    }
174}
175
176impl DateTime {
177    /// Constructs a default datetime of 1980-01-01 00:00:00.
178    pub const DEFAULT: Self = DateTime {
179        datepart: 0b0000_0000_0010_0001,
180        timepart: 0,
181    };
182
183    /// Returns the current time if possible, otherwise the default of 1980-01-01.
184    #[cfg(feature = "time")]
185    #[must_use]
186    pub fn default_for_write() -> Self {
187        let now = time::OffsetDateTime::now_utc();
188        time::PrimitiveDateTime::new(now.date(), now.time())
189            .try_into()
190            .unwrap_or_else(|_| DateTime::default())
191    }
192
193    /// Returns the current time if possible, otherwise the default of 1980-01-01.
194    #[cfg(not(feature = "time"))]
195    #[must_use]
196    pub fn default_for_write() -> Self {
197        DateTime::default()
198    }
199
200    #[cfg(feature = "chrono")]
201    /// Generate a `SystemTime` from a `DateTime`.
202    pub(crate) fn datetime_to_systemtime(&self) -> Option<std::time::SystemTime> {
203        if let Some(chrono_datetime) = self.generate_chrono_datetime() {
204            let time = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
205                chrono_datetime,
206                chrono::Utc,
207            );
208            return Some(time.into());
209        }
210        None
211    }
212
213    #[cfg(feature = "chrono")]
214    /// Generate a `NaiveDateTime` from a `DateTime`.
215    fn generate_chrono_datetime(&self) -> Option<chrono::NaiveDateTime> {
216        if let Some(chrono_date) = chrono::NaiveDate::from_ymd_opt(
217            self.year().into(),
218            self.month().into(),
219            self.day().into(),
220        ) && let Some(chrono_datetime) = chrono_date.and_hms_opt(
221            self.hour().into(),
222            self.minute().into(),
223            self.second().into(),
224        ) {
225            return Some(chrono_datetime);
226        }
227        None
228    }
229
230    /// Converts an msdos (u16, u16) pair to a `DateTime` object
231    ///
232    /// # Safety
233    /// The caller must ensure the date and time are valid.
234    #[must_use]
235    pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime {
236        DateTime { datepart, timepart }
237    }
238
239    pub(crate) fn is_leap_year(year: u16) -> bool {
240        year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
241    }
242
243    /// Converts an msdos (u16, u16) pair to a `DateTime` object if it represents a valid date and
244    /// time.
245    pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result<DateTime, DateTimeRangeError> {
246        let seconds = (timepart & 0b0000_0000_0001_1111) << 1;
247        let minutes = (timepart & 0b0000_0111_1110_0000) >> 5;
248        let hours = (timepart & 0b1111_1000_0000_0000) >> 11;
249        let days = datepart & 0b0000_0000_0001_1111;
250        let months = (datepart & 0b0000_0001_1110_0000) >> 5;
251        let years = (datepart & 0b1111_1110_0000_0000) >> 9;
252        Self::from_date_and_time(
253            years.checked_add(1980).ok_or(DateTimeRangeError)?,
254            months.try_into()?,
255            days.try_into()?,
256            hours.try_into()?,
257            minutes.try_into()?,
258            seconds.try_into()?,
259        )
260    }
261
262    /// Constructs a `DateTime` from a specific date and time
263    ///
264    /// The bounds are:
265    /// * year: [1980, 2107]
266    /// * month: [1, 12]
267    /// * day: [1, 28..=31]
268    /// * hour: [0, 23]
269    /// * minute: [0, 59]
270    /// * second: [0, 60] (rounded down to even and to [0, 58] due to ZIP format limitation)
271    pub fn from_date_and_time(
272        year: u16,
273        month: u8,
274        day: u8,
275        hour: u8,
276        minute: u8,
277        second: u8,
278    ) -> Result<DateTime, DateTimeRangeError> {
279        if (1980..=2107).contains(&year)
280            && (1..=12).contains(&month)
281            && (1..=31).contains(&day)
282            && hour <= 23
283            && minute <= 59
284            && second <= 60
285        {
286            // DOS/ZIP timestamp stores seconds/2 in 5 bits and cannot represent 59 or 60 seconds (incl. leap seconds)
287            let second = second.min(58);
288            let max_day = match month {
289                1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
290                4 | 6 | 9 | 11 => 30,
291                2 if Self::is_leap_year(year) => 29,
292                2 => 28,
293                _ => unreachable!(),
294            };
295            if day > max_day {
296                return Err(DateTimeRangeError);
297            }
298            let datepart = u16::from(day) | (u16::from(month) << 5) | ((year - 1980) << 9);
299            let timepart =
300                (u16::from(second) >> 1) | (u16::from(minute) << 5) | (u16::from(hour) << 11);
301            Ok(DateTime { datepart, timepart })
302        } else {
303            Err(DateTimeRangeError)
304        }
305    }
306
307    /// Indicates whether this date and time can be written to a zip archive.
308    #[must_use]
309    pub fn is_valid(&self) -> bool {
310        Self::try_from_msdos(self.datepart, self.timepart).is_ok()
311    }
312
313    /// Gets the time portion of this datetime in the msdos representation
314    #[must_use]
315    pub const fn timepart(&self) -> u16 {
316        self.timepart
317    }
318
319    /// Gets the date portion of this datetime in the msdos representation
320    #[must_use]
321    pub const fn datepart(&self) -> u16 {
322        self.datepart
323    }
324
325    /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
326    #[must_use]
327    pub const fn year(&self) -> u16 {
328        (self.datepart >> 9) + 1980
329    }
330
331    /// Get the month, where 1 = january and 12 = december
332    ///
333    /// # Warning
334    ///
335    /// When read from a zip file, this may not be a reasonable value
336    #[must_use]
337    pub const fn month(&self) -> u8 {
338        ((self.datepart & 0b0000_0001_1110_0000) >> 5) as u8
339    }
340
341    /// Get the day
342    ///
343    /// # Warning
344    ///
345    /// When read from a zip file, this may not be a reasonable value
346    #[must_use]
347    pub const fn day(&self) -> u8 {
348        (self.datepart & 0b0000_0000_0001_1111) as u8
349    }
350
351    /// Get the hour
352    ///
353    /// # Warning
354    ///
355    /// When read from a zip file, this may not be a reasonable value
356    #[must_use]
357    pub const fn hour(&self) -> u8 {
358        (self.timepart >> 11) as u8
359    }
360
361    /// Get the minute
362    ///
363    /// # Warning
364    ///
365    /// When read from a zip file, this may not be a reasonable value
366    #[must_use]
367    pub const fn minute(&self) -> u8 {
368        ((self.timepart & 0b0000_0111_1110_0000) >> 5) as u8
369    }
370
371    /// Get the second
372    ///
373    /// # Warning
374    ///
375    /// When read from a zip file, this may not be a reasonable value
376    #[must_use]
377    pub const fn second(&self) -> u8 {
378        ((self.timepart & 0b0000_0000_0001_1111) << 1) as u8
379    }
380}
381
382#[cfg(feature = "time")]
383impl TryFrom<time::PrimitiveDateTime> for DateTime {
384    type Error = DateTimeRangeError;
385
386    fn try_from(dt: time::PrimitiveDateTime) -> Result<Self, Self::Error> {
387        Self::from_date_and_time(
388            dt.year().try_into()?,
389            dt.month().into(),
390            dt.day(),
391            dt.hour(),
392            dt.minute(),
393            dt.second(),
394        )
395    }
396}
397
398#[cfg(feature = "time")]
399impl TryFrom<DateTime> for time::PrimitiveDateTime {
400    type Error = time::error::ComponentRange;
401
402    fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
403        use time::{Date, Month, Time};
404        let date =
405            Date::from_calendar_date(i32::from(dt.year()), Month::try_from(dt.month())?, dt.day())?;
406        let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
407        Ok(time::PrimitiveDateTime::new(date, time))
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    #[test]
414    #[allow(clippy::unusual_byte_groupings)]
415    fn datetime_default() {
416        use super::DateTime;
417        let dt = DateTime::default();
418        assert_eq!(dt.timepart(), 0);
419        assert_eq!(dt.datepart(), 0b0000000_0001_00001);
420    }
421
422    #[test]
423    #[allow(clippy::unusual_byte_groupings)]
424    fn datetime_max() {
425        use super::DateTime;
426        let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
427        assert_eq!(dt.timepart(), 0b10111_111011_11101);
428        assert_eq!(dt.datepart(), 0b1111111_1100_11111);
429    }
430
431    #[test]
432    fn datetime_equality() {
433        use super::DateTime;
434
435        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
436        assert_eq!(
437            dt,
438            DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
439        );
440        assert_ne!(dt, DateTime::default());
441    }
442
443    #[test]
444    fn datetime_order() {
445        use std::cmp::Ordering;
446
447        use super::DateTime;
448
449        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
450        assert_eq!(
451            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
452            Ordering::Equal
453        );
454        // year
455        assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
456        assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
457        // month
458        assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
459        assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
460        // day
461        assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
462        assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
463        // hour
464        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
465        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
466        // minute
467        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
468        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
469        // second
470        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
471        assert_eq!(
472            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
473            Ordering::Equal
474        );
475        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
476        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
477    }
478
479    #[test]
480    fn datetime_display() {
481        use super::DateTime;
482
483        assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
484        assert_eq!(
485            format!(
486                "{}",
487                DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
488            ),
489            "2018-11-17 10:38:30"
490        );
491        assert_eq!(
492            format!(
493                "{}",
494                DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
495            ),
496            "2107-12-31 23:59:58"
497        );
498    }
499
500    #[test]
501    fn datetime_bounds() {
502        use super::DateTime;
503
504        assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
505        assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
506        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
507        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
508
509        assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
510        assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
511        assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
512        assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
513        assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
514        assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
515        assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
516        assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
517
518        assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
519        assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
520        assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
521        assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
522        assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
523        assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
524        assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
525        assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
526        assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
527        assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
528        assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
529        assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
530        assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
531        assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
532        assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
533        assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
534        assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());
535
536        // leap year: divisible by 4
537        assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
538        // leap year: divisible by 100 and by 400
539        assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
540        // common year: divisible by 100 but not by 400
541        assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
542    }
543
544    #[cfg(feature = "time")]
545    #[test]
546    fn datetime_try_from_primitive_datetime() {
547        use time::macros::datetime;
548
549        use super::DateTime;
550
551        // 2018-11-17 10:38:30
552        let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
553        assert_eq!(dt.year(), 2018);
554        assert_eq!(dt.month(), 11);
555        assert_eq!(dt.day(), 17);
556        assert_eq!(dt.hour(), 10);
557        assert_eq!(dt.minute(), 38);
558        assert_eq!(dt.second(), 30);
559    }
560
561    #[cfg(feature = "time")]
562    #[test]
563    fn datetime_try_from_bounds() {
564        use super::DateTime;
565        use time::macros::datetime;
566
567        // 1979-12-31 23:59:59
568        assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());
569
570        // 1980-01-01 00:00:00
571        assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());
572
573        // 2107-12-31 23:59:59
574        assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());
575
576        // 2108-01-01 00:00:00
577        assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
578    }
579
580    #[cfg(feature = "time")]
581    #[test]
582    fn primitive_datetime_try_from_datetime() {
583        use time::PrimitiveDateTime;
584        use time::macros::datetime;
585
586        use super::DateTime;
587
588        // 2018-11-17 10:38:30
589        let dt =
590            PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
591        assert_eq!(dt, datetime!(2018-11-17 10:38:30));
592    }
593
594    #[cfg(feature = "time")]
595    #[test]
596    fn primitive_datetime_try_from_bounds() {
597        use super::DateTime;
598        use time::PrimitiveDateTime;
599
600        // 1980-00-00 00:00:00
601        assert!(
602            PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) })
603                .is_err()
604        );
605
606        // 2107-15-31 31:63:62
607        assert!(
608            PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) })
609                .is_err()
610        );
611    }
612
613    #[cfg(feature = "jiff-02")]
614    #[test]
615    fn datetime_try_from_civil_datetime() {
616        use jiff::civil;
617
618        use super::DateTime;
619
620        // 2018-11-17 10:38:30
621        let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap();
622        assert_eq!(dt.year(), 2018);
623        assert_eq!(dt.month(), 11);
624        assert_eq!(dt.day(), 17);
625        assert_eq!(dt.hour(), 10);
626        assert_eq!(dt.minute(), 38);
627        assert_eq!(dt.second(), 30);
628    }
629
630    #[cfg(feature = "jiff-02")]
631    #[test]
632    fn datetime_try_from_civil_datetime_bounds() {
633        use jiff::civil;
634
635        use super::DateTime;
636
637        // 1979-12-31 23:59:59
638        assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());
639
640        // 1980-01-01 00:00:00
641        assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());
642
643        // 2107-12-31 23:59:59
644        assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());
645
646        // 2108-01-01 00:00:00
647        assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
648    }
649
650    #[cfg(feature = "jiff-02")]
651    #[test]
652    fn civil_datetime_try_from_datetime() {
653        use jiff::civil;
654
655        use super::DateTime;
656
657        // 2018-11-17 10:38:30 UTC
658        let dt =
659            civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
660        assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
661    }
662
663    #[cfg(feature = "jiff-02")]
664    #[test]
665    fn civil_datetime_try_from_datetime_bounds() {
666        use jiff::civil;
667
668        use super::DateTime;
669
670        // 1980-00-00 00:00:00
671        assert!(
672            civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) })
673                .is_err()
674        );
675
676        // 2107-15-31 31:63:62
677        assert!(
678            civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) })
679                .is_err()
680        );
681    }
682
683    #[test]
684    fn time_conversion() {
685        use super::DateTime;
686        let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
687        assert_eq!(dt.year(), 2018);
688        assert_eq!(dt.month(), 11);
689        assert_eq!(dt.day(), 17);
690        assert_eq!(dt.hour(), 10);
691        assert_eq!(dt.minute(), 38);
692        assert_eq!(dt.second(), 30);
693
694        let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
695        assert_eq!(dt.year(), 2018);
696        assert_eq!(dt.month(), 11);
697        assert_eq!(dt.day(), 17);
698        assert_eq!(dt.hour(), 10);
699        assert_eq!(dt.minute(), 38);
700        assert_eq!(dt.second(), 30);
701
702        assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
703    }
704
705    #[test]
706    fn time_out_of_bounds() {
707        use super::DateTime;
708        let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
709        assert_eq!(dt.year(), 2107);
710        assert_eq!(dt.month(), 15);
711        assert_eq!(dt.day(), 31);
712        assert_eq!(dt.hour(), 31);
713        assert_eq!(dt.minute(), 63);
714        assert_eq!(dt.second(), 62);
715
716        let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
717        assert_eq!(dt.year(), 1980);
718        assert_eq!(dt.month(), 0);
719        assert_eq!(dt.day(), 0);
720        assert_eq!(dt.hour(), 0);
721        assert_eq!(dt.minute(), 0);
722        assert_eq!(dt.second(), 0);
723    }
724
725    #[cfg(feature = "time")]
726    #[test]
727    fn time_at_january() {
728        use super::DateTime;
729        use time::{OffsetDateTime, PrimitiveDateTime};
730
731        // 2020-01-01 00:00:00
732        let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
733
734        assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
735    }
736
737    #[test]
738    fn test_is_leap_year() {
739        use crate::DateTime;
740        assert!(DateTime::is_leap_year(2000));
741        assert!(!DateTime::is_leap_year(2026));
742        assert!(!DateTime::is_leap_year(2027));
743        assert!(DateTime::is_leap_year(2028));
744        assert!(DateTime::is_leap_year(1600));
745        assert!(DateTime::is_leap_year(2400));
746        assert!(!DateTime::is_leap_year(1900));
747        assert!(!DateTime::is_leap_year(2100));
748    }
749}