Skip to main content

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