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        Ok(Self::new(date, time))
108    }
109
110    /// Returns [`true`] if `self` is valid MS-DOS date and time, and [`false`]
111    /// otherwise.
112    #[must_use]
113    pub fn is_valid(self) -> bool {
114        self.date().is_valid() && self.time().is_valid()
115    }
116
117    /// Gets the [`Date`] of this `DateTime`.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// # use dos_date_time::{Date, DateTime};
123    /// #
124    /// assert_eq!(DateTime::MIN.date(), Date::MIN);
125    /// assert_eq!(DateTime::MAX.date(), Date::MAX);
126    /// ```
127    #[must_use]
128    pub const fn date(self) -> Date {
129        self.date
130    }
131
132    /// Gets the [`Time`] of this `DateTime`.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// # use dos_date_time::{DateTime, Time};
138    /// #
139    /// assert_eq!(DateTime::MIN.time(), Time::MIN);
140    /// assert_eq!(DateTime::MAX.time(), Time::MAX);
141    /// ```
142    #[must_use]
143    pub const fn time(self) -> Time {
144        self.time
145    }
146
147    /// Gets the year of this `DateTime`.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// # use dos_date_time::DateTime;
153    /// #
154    /// assert_eq!(DateTime::MIN.year(), 1980);
155    /// assert_eq!(DateTime::MAX.year(), 2107);
156    /// ```
157    #[must_use]
158    pub const fn year(self) -> u16 {
159        self.date().year()
160    }
161
162    /// Gets the month of this `DateTime`.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// # use dos_date_time::{DateTime, time::Month};
168    /// #
169    /// assert_eq!(DateTime::MIN.month(), Month::January);
170    /// assert_eq!(DateTime::MAX.month(), Month::December);
171    /// ```
172    #[must_use]
173    pub fn month(self) -> Month {
174        self.date().month()
175    }
176
177    /// Gets the day of this `DateTime`.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// # use dos_date_time::DateTime;
183    /// #
184    /// assert_eq!(DateTime::MIN.day(), 1);
185    /// assert_eq!(DateTime::MAX.day(), 31);
186    /// ```
187    #[must_use]
188    pub fn day(self) -> u8 {
189        self.date().day()
190    }
191
192    /// Gets the hour of this `DateTime`.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// # use dos_date_time::DateTime;
198    /// #
199    /// assert_eq!(DateTime::MIN.hour(), 0);
200    /// assert_eq!(DateTime::MAX.hour(), 23);
201    /// ```
202    #[must_use]
203    pub fn hour(self) -> u8 {
204        self.time().hour()
205    }
206
207    /// Gets the minute of this `DateTime`.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// # use dos_date_time::DateTime;
213    /// #
214    /// assert_eq!(DateTime::MIN.minute(), 0);
215    /// assert_eq!(DateTime::MAX.minute(), 59);
216    /// ```
217    #[must_use]
218    pub fn minute(self) -> u8 {
219        self.time().minute()
220    }
221
222    /// Gets the second of this `DateTime`.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// # use dos_date_time::DateTime;
228    /// #
229    /// assert_eq!(DateTime::MIN.second(), 0);
230    /// assert_eq!(DateTime::MAX.second(), 58);
231    /// ```
232    #[must_use]
233    pub fn second(self) -> u8 {
234        self.time().second()
235    }
236}
237
238impl Default for DateTime {
239    /// Returns the default value of "1980-01-01 00:00:00".
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}