Skip to main content

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