dos_date_time/
dos_date_time.rs

1// SPDX-FileCopyrightText: 2025 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! [MS-DOS date and time].
6//!
7//! [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
8
9mod cmp;
10mod consts;
11mod convert;
12mod fmt;
13
14use time::Month;
15
16use crate::{Date, Time, error::DateTimeRangeError};
17
18/// `DateTime` is a type that combines a [`Date`] and a [`Time`] and represents
19/// [MS-DOS date and time].
20///
21/// These are packed 16-bit unsigned integer values that specify the date and
22/// time an MS-DOS file was last written to, and are used as timestamps such as
23/// [FAT] or [ZIP] file format.
24///
25/// <div class="warning">
26///
27/// The resolution of MS-DOS date and time is 2 seconds.
28///
29/// </div>
30///
31/// See the [format specification] for [Kaitai Struct] for more details on the
32/// structure of MS-DOS date and time.
33///
34/// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
35/// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
36/// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
37/// [format specification]: https://formats.kaitai.io/dos_datetime/
38/// [Kaitai Struct]: https://kaitai.io/
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct DateTime {
41    date: Date,
42    time: Time,
43}
44
45impl DateTime {
46    /// Creates a new `DateTime` with the given [`Date`] and [`Time`].
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// # use dos_date_time::{Date, DateTime, Time};
52    /// #
53    /// assert_eq!(DateTime::new(Date::MIN, Time::MIN), DateTime::MIN);
54    /// assert_eq!(DateTime::new(Date::MAX, Time::MAX), DateTime::MAX);
55    /// ```
56    #[must_use]
57    pub const fn new(date: Date, time: Time) -> Self {
58        Self { date, time }
59    }
60
61    /// Creates a new `DateTime` with the given [`time::Date`] and
62    /// [`time::Time`].
63    ///
64    /// <div class="warning">
65    ///
66    /// The resolution of MS-DOS date and time is 2 seconds. So this method
67    /// rounds towards zero, truncating any fractional part of the exact result
68    /// of dividing seconds by 2.
69    ///
70    /// </div>
71    ///
72    /// # Errors
73    ///
74    /// Returns [`Err`] if `date` or `time` are invalid as MS-DOS date and time.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// # use dos_date_time::{
80    /// #     DateTime,
81    /// #     time::{
82    /// #         Time,
83    /// #         macros::{date, time},
84    /// #     },
85    /// # };
86    /// #
87    /// assert_eq!(
88    ///     DateTime::from_date_time(date!(1980-01-01), Time::MIDNIGHT),
89    ///     Ok(DateTime::MIN)
90    /// );
91    /// assert_eq!(
92    ///     DateTime::from_date_time(date!(2107-12-31), time!(23:59:58)),
93    ///     Ok(DateTime::MAX)
94    /// );
95    ///
96    /// // Before `1980-01-01 00:00:00`.
97    /// assert!(DateTime::from_date_time(date!(1979-12-31), time!(23:59:59)).is_err());
98    /// // After `2107-12-31 23:59:59`.
99    /// assert!(DateTime::from_date_time(date!(2108-01-01), Time::MIDNIGHT).is_err());
100    /// ```
101    pub fn from_date_time(date: time::Date, time: time::Time) -> Result<Self, DateTimeRangeError> {
102        let (date, time) = (date.try_into()?, time.into());
103        let dt = Self::new(date, time);
104        Ok(dt)
105    }
106
107    /// Returns [`true`] if `self` is valid MS-DOS date and time, and [`false`]
108    /// otherwise.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// # use dos_date_time::{Date, DateTime, Time};
114    /// #
115    /// assert_eq!(DateTime::MIN.is_valid(), true);
116    /// assert_eq!(DateTime::MAX.is_valid(), true);
117    ///
118    /// assert_eq!(
119    ///     DateTime::new(unsafe { Date::new_unchecked(u16::MAX) }, unsafe {
120    ///         Time::new_unchecked(u16::MAX)
121    ///     })
122    ///     .is_valid(),
123    ///     false
124    /// );
125    /// ```
126    #[must_use]
127    pub fn is_valid(self) -> bool {
128        self.date().is_valid() && self.time().is_valid()
129    }
130
131    /// Gets the [`Date`] of this `DateTime`.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// # use dos_date_time::{Date, DateTime};
137    /// #
138    /// assert_eq!(DateTime::MIN.date(), Date::MIN);
139    /// assert_eq!(DateTime::MAX.date(), Date::MAX);
140    /// ```
141    #[must_use]
142    pub const fn date(self) -> Date {
143        self.date
144    }
145
146    /// Gets the [`Time`] of this `DateTime`.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// # use dos_date_time::{DateTime, Time};
152    /// #
153    /// assert_eq!(DateTime::MIN.time(), Time::MIN);
154    /// assert_eq!(DateTime::MAX.time(), Time::MAX);
155    /// ```
156    #[must_use]
157    pub const fn time(self) -> Time {
158        self.time
159    }
160
161    /// Gets the year of this `DateTime`.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// # use dos_date_time::DateTime;
167    /// #
168    /// assert_eq!(DateTime::MIN.year(), 1980);
169    /// assert_eq!(DateTime::MAX.year(), 2107);
170    /// ```
171    #[must_use]
172    pub const fn year(self) -> u16 {
173        self.date().year()
174    }
175
176    /// Gets the month of this `DateTime`.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// # use dos_date_time::{DateTime, time::Month};
182    /// #
183    /// assert_eq!(DateTime::MIN.month(), Month::January);
184    /// assert_eq!(DateTime::MAX.month(), Month::December);
185    /// ```
186    #[must_use]
187    pub fn month(self) -> Month {
188        self.date().month()
189    }
190
191    /// Gets the day of this `DateTime`.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// # use dos_date_time::DateTime;
197    /// #
198    /// assert_eq!(DateTime::MIN.day(), 1);
199    /// assert_eq!(DateTime::MAX.day(), 31);
200    /// ```
201    #[must_use]
202    pub fn day(self) -> u8 {
203        self.date().day()
204    }
205
206    /// Gets the hour of this `DateTime`.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// # use dos_date_time::DateTime;
212    /// #
213    /// assert_eq!(DateTime::MIN.hour(), 0);
214    /// assert_eq!(DateTime::MAX.hour(), 23);
215    /// ```
216    #[must_use]
217    pub fn hour(self) -> u8 {
218        self.time().hour()
219    }
220
221    /// Gets the minute of this `DateTime`.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # use dos_date_time::DateTime;
227    /// #
228    /// assert_eq!(DateTime::MIN.minute(), 0);
229    /// assert_eq!(DateTime::MAX.minute(), 59);
230    /// ```
231    #[must_use]
232    pub fn minute(self) -> u8 {
233        self.time().minute()
234    }
235
236    /// Gets the second of this `DateTime`.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// # use dos_date_time::DateTime;
242    /// #
243    /// assert_eq!(DateTime::MIN.second(), 0);
244    /// assert_eq!(DateTime::MAX.second(), 58);
245    /// ```
246    #[must_use]
247    pub fn second(self) -> u8 {
248        self.time().second()
249    }
250}
251
252impl Default for DateTime {
253    /// Returns the default value of "1980-01-01 00:00:00".
254    ///
255    /// Equivalent to [`DateTime::MIN`] except that it is not callable in const
256    /// contexts.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// # use dos_date_time::DateTime;
262    /// #
263    /// assert_eq!(DateTime::default(), DateTime::MIN);
264    /// ```
265    fn default() -> Self {
266        Self::MIN
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    #[cfg(feature = "std")]
273    use std::{
274        collections::hash_map::DefaultHasher,
275        hash::{Hash, Hasher},
276    };
277
278    use time::macros::{date, time};
279
280    use super::*;
281    use crate::error::DateTimeRangeErrorKind;
282
283    #[test]
284    fn clone() {
285        assert_eq!(DateTime::MIN.clone(), DateTime::MIN);
286    }
287
288    #[test]
289    fn copy() {
290        let a = DateTime::MIN;
291        let b = a;
292        assert_eq!(a, b);
293    }
294
295    #[cfg(feature = "std")]
296    #[test]
297    fn hash() {
298        assert_ne!(
299            {
300                let mut hasher = DefaultHasher::new();
301                DateTime::MIN.hash(&mut hasher);
302                hasher.finish()
303            },
304            {
305                let mut hasher = DefaultHasher::new();
306                DateTime::MAX.hash(&mut hasher);
307                hasher.finish()
308            }
309        );
310    }
311
312    #[test]
313    fn new() {
314        assert_eq!(DateTime::new(Date::MIN, Time::MIN), DateTime::MIN);
315        assert_eq!(DateTime::new(Date::MAX, Time::MAX), DateTime::MAX);
316    }
317
318    #[test]
319    const fn new_is_const_fn() {
320        const _: DateTime = DateTime::new(Date::MIN, Time::MIN);
321    }
322
323    #[test]
324    fn from_date_time_before_dos_date_time_epoch() {
325        assert_eq!(
326            DateTime::from_date_time(date!(1979-12-31), time!(23:59:58)).unwrap_err(),
327            DateTimeRangeErrorKind::Negative.into()
328        );
329        assert_eq!(
330            DateTime::from_date_time(date!(1979-12-31), time!(23:59:59)).unwrap_err(),
331            DateTimeRangeErrorKind::Negative.into()
332        );
333    }
334
335    #[test]
336    fn from_date_time() {
337        assert_eq!(
338            DateTime::from_date_time(date!(1980-01-01), time::Time::MIDNIGHT).unwrap(),
339            DateTime::MIN
340        );
341        assert_eq!(
342            DateTime::from_date_time(date!(1980-01-01), time!(00:00:01)).unwrap(),
343            DateTime::MIN
344        );
345        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
346        assert_eq!(
347            DateTime::from_date_time(date!(2002-11-26), time!(19:25:00)).unwrap(),
348            DateTime::new(
349                Date::new(0b0010_1101_0111_1010).unwrap(),
350                Time::new(0b1001_1011_0010_0000).unwrap()
351            )
352        );
353        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
354        assert_eq!(
355            DateTime::from_date_time(date!(2018-11-17), time!(10:38:30)).unwrap(),
356            DateTime::new(
357                Date::new(0b0100_1101_0111_0001).unwrap(),
358                Time::new(0b0101_0100_1100_1111).unwrap()
359            )
360        );
361        assert_eq!(
362            DateTime::from_date_time(date!(2107-12-31), time!(23:59:58)).unwrap(),
363            DateTime::MAX
364        );
365        assert_eq!(
366            DateTime::from_date_time(date!(2107-12-31), time!(23:59:59)).unwrap(),
367            DateTime::MAX
368        );
369    }
370
371    #[test]
372    fn from_date_time_with_too_big_date_time() {
373        assert_eq!(
374            DateTime::from_date_time(date!(2108-01-01), time::Time::MIDNIGHT).unwrap_err(),
375            DateTimeRangeErrorKind::Overflow.into()
376        );
377    }
378
379    #[test]
380    fn is_valid() {
381        assert!(DateTime::MIN.is_valid());
382        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
383        assert!(
384            DateTime::new(
385                Date::new(0b0010_1101_0111_1010).unwrap(),
386                Time::new(0b1001_1011_0010_0000).unwrap()
387            )
388            .is_valid()
389        );
390        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
391        assert!(
392            DateTime::new(
393                Date::new(0b0100_1101_0111_0001).unwrap(),
394                Time::new(0b0101_0100_1100_1111).unwrap()
395            )
396            .is_valid()
397        );
398        assert!(DateTime::MAX.is_valid());
399    }
400
401    #[test]
402    fn is_valid_with_invalid_date() {
403        // The Day field is 0.
404        assert!(
405            !DateTime::new(
406                unsafe { Date::new_unchecked(0b0000_0000_0010_0000) },
407                Time::MIN
408            )
409            .is_valid()
410        );
411        // The Day field is 30, which is after the last day of February.
412        assert!(
413            !DateTime::new(
414                unsafe { Date::new_unchecked(0b0000_0000_0101_1110) },
415                Time::MIN
416            )
417            .is_valid()
418        );
419        // The Month field is 0.
420        assert!(
421            !DateTime::new(
422                unsafe { Date::new_unchecked(0b0000_0000_0000_0001) },
423                Time::MIN
424            )
425            .is_valid()
426        );
427        // The Month field is 13.
428        assert!(
429            !DateTime::new(
430                unsafe { Date::new_unchecked(0b0000_0001_1010_0001) },
431                Time::MIN
432            )
433            .is_valid()
434        );
435    }
436
437    #[test]
438    fn is_valid_with_invalid_time() {
439        // The DoubleSeconds field is 30.
440        assert!(
441            !DateTime::new(Date::MIN, unsafe {
442                Time::new_unchecked(0b0000_0000_0001_1110)
443            })
444            .is_valid()
445        );
446        // The Minute field is 60.
447        assert!(
448            !DateTime::new(Date::MIN, unsafe {
449                Time::new_unchecked(0b0000_0111_1000_0000)
450            })
451            .is_valid()
452        );
453        // The Hour field is 24.
454        assert!(
455            !DateTime::new(Date::MIN, unsafe {
456                Time::new_unchecked(0b1100_0000_0000_0000)
457            })
458            .is_valid()
459        );
460    }
461
462    #[test]
463    fn is_valid_with_invalid_date_time() {
464        assert!(
465            !DateTime::new(unsafe { Date::new_unchecked(u16::MAX) }, unsafe {
466                Time::new_unchecked(u16::MAX)
467            })
468            .is_valid()
469        );
470    }
471
472    #[test]
473    fn date() {
474        assert_eq!(DateTime::MIN.date(), Date::MIN);
475        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
476        assert_eq!(
477            DateTime::new(
478                Date::new(0b0010_1101_0111_1010).unwrap(),
479                Time::new(0b1001_1011_0010_0000).unwrap()
480            )
481            .date(),
482            Date::new(0b0010_1101_0111_1010).unwrap()
483        );
484        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
485        assert_eq!(
486            DateTime::new(
487                Date::new(0b0100_1101_0111_0001).unwrap(),
488                Time::new(0b0101_0100_1100_1111).unwrap()
489            )
490            .date(),
491            Date::new(0b0100_1101_0111_0001).unwrap()
492        );
493        assert_eq!(DateTime::MAX.date(), Date::MAX);
494    }
495
496    #[test]
497    const fn date_is_const_fn() {
498        const _: Date = DateTime::MIN.date();
499    }
500
501    #[test]
502    fn time() {
503        assert_eq!(DateTime::MIN.time(), Time::MIN);
504        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
505        assert_eq!(
506            DateTime::new(
507                Date::new(0b0010_1101_0111_1010).unwrap(),
508                Time::new(0b1001_1011_0010_0000).unwrap()
509            )
510            .time(),
511            Time::new(0b1001_1011_0010_0000).unwrap()
512        );
513        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
514        assert_eq!(
515            DateTime::new(
516                Date::new(0b0100_1101_0111_0001).unwrap(),
517                Time::new(0b0101_0100_1100_1111).unwrap()
518            )
519            .time(),
520            Time::new(0b0101_0100_1100_1111).unwrap()
521        );
522        assert_eq!(DateTime::MAX.time(), Time::MAX);
523    }
524
525    #[test]
526    const fn time_is_const_fn() {
527        const _: Time = DateTime::MIN.time();
528    }
529
530    #[test]
531    fn year() {
532        assert_eq!(DateTime::MIN.year(), 1980);
533        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
534        assert_eq!(
535            DateTime::new(
536                Date::new(0b0010_1101_0111_1010).unwrap(),
537                Time::new(0b1001_1011_0010_0000).unwrap()
538            )
539            .year(),
540            2002
541        );
542        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
543        assert_eq!(
544            DateTime::new(
545                Date::new(0b0100_1101_0111_0001).unwrap(),
546                Time::new(0b0101_0100_1100_1111).unwrap()
547            )
548            .year(),
549            2018
550        );
551        assert_eq!(DateTime::MAX.year(), 2107);
552    }
553
554    #[test]
555    const fn year_is_const_fn() {
556        const _: u16 = DateTime::MIN.year();
557    }
558
559    #[test]
560    fn month() {
561        assert_eq!(DateTime::MIN.month(), Month::January);
562        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
563        assert_eq!(
564            DateTime::new(
565                Date::new(0b0010_1101_0111_1010).unwrap(),
566                Time::new(0b1001_1011_0010_0000).unwrap()
567            )
568            .month(),
569            Month::November
570        );
571        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
572        assert_eq!(
573            DateTime::new(
574                Date::new(0b0100_1101_0111_0001).unwrap(),
575                Time::new(0b0101_0100_1100_1111).unwrap()
576            )
577            .month(),
578            Month::November
579        );
580        assert_eq!(DateTime::MAX.month(), Month::December);
581    }
582
583    #[test]
584    fn day() {
585        assert_eq!(DateTime::MIN.day(), 1);
586        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
587        assert_eq!(
588            DateTime::new(
589                Date::new(0b0010_1101_0111_1010).unwrap(),
590                Time::new(0b1001_1011_0010_0000).unwrap()
591            )
592            .day(),
593            26
594        );
595        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
596        assert_eq!(
597            DateTime::new(
598                Date::new(0b0100_1101_0111_0001).unwrap(),
599                Time::new(0b0101_0100_1100_1111).unwrap()
600            )
601            .day(),
602            17
603        );
604        assert_eq!(DateTime::MAX.day(), 31);
605    }
606
607    #[test]
608    fn hour() {
609        assert_eq!(DateTime::MIN.hour(), u8::MIN);
610        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
611        assert_eq!(
612            DateTime::new(
613                Date::new(0b0010_1101_0111_1010).unwrap(),
614                Time::new(0b1001_1011_0010_0000).unwrap()
615            )
616            .hour(),
617            19
618        );
619        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
620        assert_eq!(
621            DateTime::new(
622                Date::new(0b0100_1101_0111_0001).unwrap(),
623                Time::new(0b0101_0100_1100_1111).unwrap()
624            )
625            .hour(),
626            10
627        );
628        assert_eq!(DateTime::MAX.hour(), 23);
629    }
630
631    #[test]
632    fn minute() {
633        assert_eq!(DateTime::MIN.minute(), u8::MIN);
634        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
635        assert_eq!(
636            DateTime::new(
637                Date::new(0b0010_1101_0111_1010).unwrap(),
638                Time::new(0b1001_1011_0010_0000).unwrap()
639            )
640            .minute(),
641            25
642        );
643        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
644        assert_eq!(
645            DateTime::new(
646                Date::new(0b0100_1101_0111_0001).unwrap(),
647                Time::new(0b0101_0100_1100_1111).unwrap()
648            )
649            .minute(),
650            38
651        );
652        assert_eq!(DateTime::MAX.minute(), 59);
653    }
654
655    #[test]
656    fn second() {
657        assert_eq!(DateTime::MIN.second(), u8::MIN);
658        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
659        assert_eq!(
660            DateTime::new(
661                Date::new(0b0010_1101_0111_1010).unwrap(),
662                Time::new(0b1001_1011_0010_0000).unwrap()
663            )
664            .second(),
665            u8::MIN
666        );
667        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
668        assert_eq!(
669            DateTime::new(
670                Date::new(0b0100_1101_0111_0001).unwrap(),
671                Time::new(0b0101_0100_1100_1111).unwrap()
672            )
673            .second(),
674            30
675        );
676        assert_eq!(DateTime::MAX.second(), 58);
677    }
678
679    #[test]
680    fn default() {
681        assert_eq!(DateTime::default(), DateTime::MIN);
682    }
683}