dos_date_time/
dos_date.rs

1// SPDX-FileCopyrightText: 2025 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! The [MS-DOS date].
6//!
7//! [MS-DOS date]: 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::error::{DateRangeError, DateRangeErrorKind};
17
18/// `Date` is a type that represents the [MS-DOS date].
19///
20/// This is a packed 16-bit unsigned integer value that specify the date an
21/// MS-DOS file was last written to, and is used as timestamps such as [FAT] or
22/// [ZIP] file format.
23///
24/// See the [format specification] for [Kaitai Struct] for more details on the
25/// structure of the MS-DOS date.
26///
27/// [MS-DOS date]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
28/// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
29/// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
30/// [format specification]: https://formats.kaitai.io/dos_datetime/
31/// [Kaitai Struct]: https://kaitai.io/
32#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33#[repr(transparent)]
34pub struct Date(u16);
35
36impl Date {
37    #[allow(clippy::missing_panics_doc)]
38    /// Creates a new `Date` with the given MS-DOS date.
39    ///
40    /// Returns [`None`] if the given MS-DOS date is not a valid MS-DOS date.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// # use dos_date_time::Date;
46    /// #
47    /// assert_eq!(Date::new(0b0000_0000_0010_0001), Some(Date::MIN));
48    /// assert_eq!(Date::new(0b1111_1111_1001_1111), Some(Date::MAX));
49    ///
50    /// // The Day field is 0.
51    /// assert_eq!(Date::new(0b0000_0000_0010_0000), None);
52    /// ```
53    #[must_use]
54    pub fn new(date: u16) -> Option<Self> {
55        let (year, month, day) = (
56            (1980 + (date >> 9)).into(),
57            u8::try_from((date >> 5) & 0x0f)
58                .expect("month should be in the range of `u8`")
59                .try_into()
60                .ok()?,
61            (date & 0x1f)
62                .try_into()
63                .expect("day should be in the range of `u8`"),
64        );
65        let date = time::Date::from_calendar_date(year, month, day).ok()?;
66        Self::from_date(date).ok()
67    }
68
69    /// Creates a new `Date` with the given MS-DOS date.
70    ///
71    /// # Safety
72    ///
73    /// The given MS-DOS date must be a valid MS-DOS date.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// # use dos_date_time::Date;
79    /// #
80    /// assert_eq!(
81    ///     unsafe { Date::new_unchecked(0b0000_0000_0010_0001) },
82    ///     Date::MIN
83    /// );
84    /// assert_eq!(
85    ///     unsafe { Date::new_unchecked(0b1111_1111_1001_1111) },
86    ///     Date::MAX
87    /// );
88    /// ```
89    #[must_use]
90    pub const unsafe fn new_unchecked(date: u16) -> Self {
91        Self(date)
92    }
93
94    #[allow(clippy::missing_panics_doc)]
95    /// Creates a new `Date` with the given [`time::Date`].
96    ///
97    /// # Errors
98    ///
99    /// Returns [`Err`] if `date` is an invalid as the MS-DOS date.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// # use dos_date_time::{Date, time::macros::date};
105    /// #
106    /// assert_eq!(Date::from_date(date!(1980-01-01)), Ok(Date::MIN));
107    /// assert_eq!(Date::from_date(date!(2107-12-31)), Ok(Date::MAX));
108    ///
109    /// // Before `1980-01-01`.
110    /// assert!(Date::from_date(date!(1979-12-31)).is_err());
111    /// // After `2107-12-31`.
112    /// assert!(Date::from_date(date!(2108-01-01)).is_err());
113    /// ```
114    pub fn from_date(date: time::Date) -> Result<Self, DateRangeError> {
115        match date.year() {
116            ..=1979 => Err(DateRangeErrorKind::Negative.into()),
117            2108.. => Err(DateRangeErrorKind::Overflow.into()),
118            year => {
119                let (year, month, day) = (
120                    u16::try_from(year - 1980).expect("year should be in the range of `u16`"),
121                    u16::from(u8::from(date.month())),
122                    u16::from(date.day()),
123                );
124                let date = (year << 9) | (month << 5) | day;
125                // SAFETY: `date` is a valid as the MS-DOS date.
126                let date = unsafe { Self::new_unchecked(date) };
127                Ok(date)
128            }
129        }
130    }
131
132    /// Returns [`true`] if `self` is a valid MS-DOS date, and [`false`]
133    /// otherwise.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// # use dos_date_time::Date;
139    /// #
140    /// assert_eq!(Date::MIN.is_valid(), true);
141    /// assert_eq!(Date::MAX.is_valid(), true);
142    ///
143    /// // The Day field is 0.
144    /// assert_eq!(
145    ///     unsafe { Date::new_unchecked(0b0000_0000_0010_0000) }.is_valid(),
146    ///     false
147    /// );
148    /// ```
149    #[must_use]
150    pub fn is_valid(self) -> bool {
151        Self::new(self.to_raw()).is_some()
152    }
153
154    /// Returns the MS-DOS date of this `Date` as the underlying [`u16`] value.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// # use dos_date_time::Date;
160    /// #
161    /// assert_eq!(Date::MIN.to_raw(), 0b0000_0000_0010_0001);
162    /// assert_eq!(Date::MAX.to_raw(), 0b1111_1111_1001_1111);
163    /// ```
164    #[must_use]
165    pub const fn to_raw(self) -> u16 {
166        self.0
167    }
168
169    /// Gets the year of this `Date`.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use dos_date_time::Date;
175    /// #
176    /// assert_eq!(Date::MIN.year(), 1980);
177    /// assert_eq!(Date::MAX.year(), 2107);
178    /// ```
179    #[must_use]
180    pub const fn year(self) -> u16 {
181        1980 + (self.to_raw() >> 9)
182    }
183
184    #[allow(clippy::missing_panics_doc)]
185    /// Gets the month of this `Date`.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// # use dos_date_time::{Date, time::Month};
191    /// #
192    /// assert_eq!(Date::MIN.month(), Month::January);
193    /// assert_eq!(Date::MAX.month(), Month::December);
194    /// ```
195    #[must_use]
196    pub fn month(self) -> Month {
197        u8::try_from((self.to_raw() >> 5) & 0x0f)
198            .expect("month should be in the range of `u8`")
199            .try_into()
200            .expect("month should be in the range of `Month`")
201    }
202
203    #[allow(clippy::missing_panics_doc)]
204    /// Gets the day of this `Date`.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// # use dos_date_time::Date;
210    /// #
211    /// assert_eq!(Date::MIN.day(), 1);
212    /// assert_eq!(Date::MAX.day(), 31);
213    /// ```
214    #[must_use]
215    pub fn day(self) -> u8 {
216        (self.to_raw() & 0x1f)
217            .try_into()
218            .expect("day should be in the range of `u8`")
219    }
220}
221
222impl Default for Date {
223    /// Returns the default value of "1980-01-01".
224    ///
225    /// Equivalent to [`Date::MIN`] except that it is not callable in const
226    /// contexts.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// # use dos_date_time::Date;
232    /// #
233    /// assert_eq!(Date::default(), Date::MIN);
234    /// ```
235    fn default() -> Self {
236        Self::MIN
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use core::mem;
243    #[cfg(feature = "std")]
244    use std::{
245        collections::hash_map::DefaultHasher,
246        hash::{Hash, Hasher},
247    };
248
249    use time::macros::date;
250
251    use super::*;
252
253    #[test]
254    fn size_of() {
255        assert_eq!(mem::size_of::<Date>(), mem::size_of::<u16>());
256    }
257
258    #[test]
259    fn align_of() {
260        assert_eq!(mem::align_of::<Date>(), mem::align_of::<u16>());
261    }
262
263    #[test]
264    fn clone() {
265        assert_eq!(Date::MIN.clone(), Date::MIN);
266    }
267
268    #[test]
269    fn copy() {
270        let a = Date::MIN;
271        let b = a;
272        assert_eq!(a, b);
273    }
274
275    #[cfg(feature = "std")]
276    #[test]
277    fn hash() {
278        assert_ne!(
279            {
280                let mut hasher = DefaultHasher::new();
281                Date::MIN.hash(&mut hasher);
282                hasher.finish()
283            },
284            {
285                let mut hasher = DefaultHasher::new();
286                Date::MAX.hash(&mut hasher);
287                hasher.finish()
288            }
289        );
290    }
291
292    #[test]
293    fn new() {
294        assert_eq!(Date::new(0b0000_0000_0010_0001).unwrap(), Date::MIN);
295        assert_eq!(Date::new(0b1111_1111_1001_1111).unwrap(), Date::MAX);
296    }
297
298    #[test]
299    fn new_with_invalid_date() {
300        // The Day field is 0.
301        assert!(Date::new(0b0000_0000_0010_0000).is_none());
302        // The Day field is 30, which is after the last day of February.
303        assert!(Date::new(0b0000_0000_0101_1110).is_none());
304        // The Month field is 0.
305        assert!(Date::new(0b0000_0000_0000_0001).is_none());
306        // The Month field is 13.
307        assert!(Date::new(0b0000_0001_1010_0001).is_none());
308    }
309
310    #[test]
311    fn new_unchecked() {
312        assert_eq!(
313            unsafe { Date::new_unchecked(0b0000_0000_0010_0001) },
314            Date::MIN
315        );
316        assert_eq!(
317            unsafe { Date::new_unchecked(0b1111_1111_1001_1111) },
318            Date::MAX
319        );
320    }
321
322    #[test]
323    const fn new_unchecked_is_const_fn() {
324        const _: Date = unsafe { Date::new_unchecked(0b0000_0000_0010_0001) };
325    }
326
327    #[test]
328    fn from_date_before_dos_date_epoch() {
329        assert_eq!(
330            Date::from_date(date!(1979-12-31)).unwrap_err(),
331            DateRangeErrorKind::Negative.into()
332        );
333        assert_eq!(
334            Date::from_date(date!(1979-12-31)).unwrap_err(),
335            DateRangeErrorKind::Negative.into()
336        );
337    }
338
339    #[test]
340    fn from_date() {
341        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
342        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
343        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
344        assert_eq!(
345            Date::from_date(date!(2002-11-26)).unwrap(),
346            Date::new(0b0010_1101_0111_1010).unwrap()
347        );
348        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
349        assert_eq!(
350            Date::from_date(date!(2018-11-17)).unwrap(),
351            Date::new(0b0100_1101_0111_0001).unwrap()
352        );
353        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
354        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
355    }
356
357    #[test]
358    fn from_date_with_too_big_date() {
359        assert_eq!(
360            Date::from_date(date!(2108-01-01)).unwrap_err(),
361            DateRangeErrorKind::Overflow.into()
362        );
363    }
364
365    #[test]
366    fn is_valid() {
367        assert!(Date::MIN.is_valid());
368        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
369        assert!(Date::new(0b0010_1101_0111_1010).unwrap().is_valid());
370        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
371        assert!(Date::new(0b0100_1101_0111_0001).unwrap().is_valid());
372        assert!(Date::MAX.is_valid());
373    }
374
375    #[test]
376    fn is_valid_with_invalid_date() {
377        // The Day field is 0.
378        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0010_0000) }.is_valid());
379        // The Day field is 30, which is after the last day of February.
380        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0101_1110) }.is_valid());
381        // The Month field is 0.
382        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0000_0001) }.is_valid());
383        // The Month field is 13.
384        assert!(!unsafe { Date::new_unchecked(0b0000_0001_1010_0001) }.is_valid());
385    }
386
387    #[test]
388    fn to_raw() {
389        assert_eq!(Date::MIN.to_raw(), 0b0000_0000_0010_0001);
390        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
391        assert_eq!(
392            Date::new(0b0010_1101_0111_1010).unwrap().to_raw(),
393            0b0010_1101_0111_1010
394        );
395        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
396        assert_eq!(
397            Date::new(0b0100_1101_0111_0001).unwrap().to_raw(),
398            0b0100_1101_0111_0001
399        );
400        assert_eq!(Date::MAX.to_raw(), 0b1111_1111_1001_1111);
401    }
402
403    #[test]
404    const fn to_raw_is_const_fn() {
405        const _: u16 = Date::MIN.to_raw();
406    }
407
408    #[test]
409    fn year() {
410        assert_eq!(Date::MIN.year(), 1980);
411        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
412        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().year(), 2002);
413        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
414        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().year(), 2018);
415        assert_eq!(Date::MAX.year(), 2107);
416    }
417
418    #[test]
419    const fn year_is_const_fn() {
420        const _: u16 = Date::MIN.year();
421    }
422
423    #[test]
424    fn month() {
425        assert_eq!(Date::MIN.month(), Month::January);
426        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
427        assert_eq!(
428            Date::new(0b0010_1101_0111_1010).unwrap().month(),
429            Month::November
430        );
431        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
432        assert_eq!(
433            Date::new(0b0100_1101_0111_0001).unwrap().month(),
434            Month::November
435        );
436        assert_eq!(Date::MAX.month(), Month::December);
437    }
438
439    #[test]
440    fn day() {
441        assert_eq!(Date::MIN.day(), 1);
442        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
443        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().day(), 26);
444        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
445        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().day(), 17);
446        assert_eq!(Date::MAX.day(), 31);
447    }
448
449    #[test]
450    fn default() {
451        assert_eq!(Date::default(), Date::MIN);
452    }
453}