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