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    #[expect(clippy::missing_panics_doc)]
34    /// Creates a new `Date` with the given underlying [`u16`] value.
35    ///
36    /// Returns [`None`] if the given value 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 underlying [`u16`] value.
66    ///
67    /// # Safety
68    ///
69    /// The given value 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    #[expect(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 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    #[expect(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    #[expect(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    /// # Examples
192    ///
193    /// ```
194    /// # use dos_date_time::Date;
195    /// #
196    /// assert_eq!(Date::default(), Date::MIN);
197    /// ```
198    fn default() -> Self {
199        Self::MIN
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use core::mem;
206    #[cfg(feature = "std")]
207    use std::{
208        collections::hash_map::DefaultHasher,
209        hash::{Hash, Hasher},
210    };
211
212    use time::macros::date;
213
214    use super::*;
215
216    #[test]
217    fn size_of() {
218        assert_eq!(mem::size_of::<Date>(), mem::size_of::<u16>());
219    }
220
221    #[test]
222    fn align_of() {
223        assert_eq!(mem::align_of::<Date>(), mem::align_of::<u16>());
224    }
225
226    #[test]
227    fn clone() {
228        assert_eq!(Date::MIN.clone(), Date::MIN);
229    }
230
231    #[test]
232    fn copy() {
233        let a = Date::MIN;
234        let b = a;
235        assert_eq!(a, b);
236    }
237
238    #[cfg(feature = "std")]
239    #[test]
240    fn hash() {
241        assert_ne!(
242            {
243                let mut hasher = DefaultHasher::new();
244                Date::MIN.hash(&mut hasher);
245                hasher.finish()
246            },
247            {
248                let mut hasher = DefaultHasher::new();
249                Date::MAX.hash(&mut hasher);
250                hasher.finish()
251            }
252        );
253    }
254
255    #[test]
256    fn new() {
257        assert_eq!(Date::new(0b0000_0000_0010_0001).unwrap(), Date::MIN);
258        assert_eq!(Date::new(0b1111_1111_1001_1111).unwrap(), Date::MAX);
259    }
260
261    #[test]
262    fn new_with_invalid_date() {
263        // The Day field is 0.
264        assert!(Date::new(0b0000_0000_0010_0000).is_none());
265        // The Day field is 30, which is after the last day of February.
266        assert!(Date::new(0b0000_0000_0101_1110).is_none());
267        // The Month field is 0.
268        assert!(Date::new(0b0000_0000_0000_0001).is_none());
269        // The Month field is 13.
270        assert!(Date::new(0b0000_0001_1010_0001).is_none());
271    }
272
273    #[test]
274    fn new_unchecked() {
275        assert_eq!(
276            unsafe { Date::new_unchecked(0b0000_0000_0010_0001) },
277            Date::MIN
278        );
279        assert_eq!(
280            unsafe { Date::new_unchecked(0b1111_1111_1001_1111) },
281            Date::MAX
282        );
283    }
284
285    #[test]
286    const fn new_unchecked_is_const_fn() {
287        const _: Date = unsafe { Date::new_unchecked(0b0000_0000_0010_0001) };
288    }
289
290    #[test]
291    fn from_date_before_dos_date_epoch() {
292        assert_eq!(
293            Date::from_date(date!(1979-12-31)).unwrap_err(),
294            DateRangeErrorKind::Negative.into()
295        );
296        assert_eq!(
297            Date::from_date(date!(1979-12-31)).unwrap_err(),
298            DateRangeErrorKind::Negative.into()
299        );
300    }
301
302    #[test]
303    fn from_date() {
304        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
305        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
306        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
307        assert_eq!(
308            Date::from_date(date!(2002-11-26)).unwrap(),
309            Date::new(0b0010_1101_0111_1010).unwrap()
310        );
311        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
312        assert_eq!(
313            Date::from_date(date!(2018-11-17)).unwrap(),
314            Date::new(0b0100_1101_0111_0001).unwrap()
315        );
316        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
317        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
318    }
319
320    #[test]
321    fn from_date_with_too_big_date() {
322        assert_eq!(
323            Date::from_date(date!(2108-01-01)).unwrap_err(),
324            DateRangeErrorKind::Overflow.into()
325        );
326    }
327
328    #[test]
329    fn is_valid() {
330        assert!(Date::MIN.is_valid());
331        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
332        assert!(Date::new(0b0010_1101_0111_1010).unwrap().is_valid());
333        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
334        assert!(Date::new(0b0100_1101_0111_0001).unwrap().is_valid());
335        assert!(Date::MAX.is_valid());
336    }
337
338    #[test]
339    fn is_valid_with_invalid_date() {
340        // The Day field is 0.
341        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0010_0000) }.is_valid());
342        // The Day field is 30, which is after the last day of February.
343        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0101_1110) }.is_valid());
344        // The Month field is 0.
345        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0000_0001) }.is_valid());
346        // The Month field is 13.
347        assert!(!unsafe { Date::new_unchecked(0b0000_0001_1010_0001) }.is_valid());
348    }
349
350    #[test]
351    fn to_raw() {
352        assert_eq!(Date::MIN.to_raw(), 0b0000_0000_0010_0001);
353        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
354        assert_eq!(
355            Date::new(0b0010_1101_0111_1010).unwrap().to_raw(),
356            0b0010_1101_0111_1010
357        );
358        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
359        assert_eq!(
360            Date::new(0b0100_1101_0111_0001).unwrap().to_raw(),
361            0b0100_1101_0111_0001
362        );
363        assert_eq!(Date::MAX.to_raw(), 0b1111_1111_1001_1111);
364    }
365
366    #[test]
367    const fn to_raw_is_const_fn() {
368        const _: u16 = Date::MIN.to_raw();
369    }
370
371    #[test]
372    fn year() {
373        assert_eq!(Date::MIN.year(), 1980);
374        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
375        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().year(), 2002);
376        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
377        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().year(), 2018);
378        assert_eq!(Date::MAX.year(), 2107);
379    }
380
381    #[test]
382    const fn year_is_const_fn() {
383        const _: u16 = Date::MIN.year();
384    }
385
386    #[test]
387    fn month() {
388        assert_eq!(Date::MIN.month(), Month::January);
389        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
390        assert_eq!(
391            Date::new(0b0010_1101_0111_1010).unwrap().month(),
392            Month::November
393        );
394        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
395        assert_eq!(
396            Date::new(0b0100_1101_0111_0001).unwrap().month(),
397            Month::November
398        );
399        assert_eq!(Date::MAX.month(), Month::December);
400    }
401
402    #[test]
403    fn day() {
404        assert_eq!(Date::MIN.day(), 1);
405        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
406        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().day(), 26);
407        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
408        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().day(), 17);
409        assert_eq!(Date::MAX.day(), 31);
410    }
411
412    #[test]
413    fn default() {
414        assert_eq!(Date::default(), Date::MIN);
415    }
416}