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                Ok(unsafe { Self::new_unchecked(date) })
108            }
109        }
110    }
111
112    /// Returns [`true`] if `self` is a valid MS-DOS date, and [`false`]
113    /// otherwise.
114    #[must_use]
115    pub fn is_valid(self) -> bool {
116        Self::new(self.to_raw()).is_some()
117    }
118
119    /// Returns this `Date` as the underlying [`u16`] value.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// # use dos_date_time::Date;
125    /// #
126    /// assert_eq!(Date::MIN.to_raw(), 0b0000_0000_0010_0001);
127    /// assert_eq!(Date::MAX.to_raw(), 0b1111_1111_1001_1111);
128    /// ```
129    #[must_use]
130    pub const fn to_raw(self) -> u16 {
131        self.0
132    }
133
134    /// Gets the year of this `Date`.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// # use dos_date_time::Date;
140    /// #
141    /// assert_eq!(Date::MIN.year(), 1980);
142    /// assert_eq!(Date::MAX.year(), 2107);
143    /// ```
144    #[must_use]
145    pub const fn year(self) -> u16 {
146        1980 + (self.to_raw() >> 9)
147    }
148
149    #[expect(clippy::missing_panics_doc)]
150    /// Gets the month of this `Date`.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// # use dos_date_time::{Date, time::Month};
156    /// #
157    /// assert_eq!(Date::MIN.month(), Month::January);
158    /// assert_eq!(Date::MAX.month(), Month::December);
159    /// ```
160    #[must_use]
161    pub fn month(self) -> Month {
162        u8::try_from((self.to_raw() >> 5) & 0x0F)
163            .expect("month should be in the range of `u8`")
164            .try_into()
165            .expect("month should be in the range of `Month`")
166    }
167
168    #[expect(clippy::missing_panics_doc)]
169    /// Gets the day of this `Date`.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use dos_date_time::Date;
175    /// #
176    /// assert_eq!(Date::MIN.day(), 1);
177    /// assert_eq!(Date::MAX.day(), 31);
178    /// ```
179    #[must_use]
180    pub fn day(self) -> u8 {
181        (self.to_raw() & 0x1F)
182            .try_into()
183            .expect("day should be in the range of `u8`")
184    }
185}
186
187impl Default for Date {
188    /// Returns the default value of "1980-01-01".
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// # use dos_date_time::Date;
194    /// #
195    /// assert_eq!(Date::default(), Date::MIN);
196    /// ```
197    fn default() -> Self {
198        Self::MIN
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use core::mem;
205    #[cfg(feature = "std")]
206    use std::{
207        collections::hash_map::DefaultHasher,
208        hash::{Hash, Hasher},
209    };
210
211    use time::macros::date;
212
213    use super::*;
214
215    #[test]
216    fn size_of() {
217        assert_eq!(mem::size_of::<Date>(), mem::size_of::<u16>());
218    }
219
220    #[test]
221    fn align_of() {
222        assert_eq!(mem::align_of::<Date>(), mem::align_of::<u16>());
223    }
224
225    #[test]
226    fn clone() {
227        assert_eq!(Date::MIN.clone(), Date::MIN);
228    }
229
230    #[test]
231    fn copy() {
232        let a = Date::MIN;
233        let b = a;
234        assert_eq!(a, b);
235    }
236
237    #[cfg(feature = "std")]
238    #[test]
239    fn hash() {
240        assert_ne!(
241            {
242                let mut hasher = DefaultHasher::new();
243                Date::MIN.hash(&mut hasher);
244                hasher.finish()
245            },
246            {
247                let mut hasher = DefaultHasher::new();
248                Date::MAX.hash(&mut hasher);
249                hasher.finish()
250            }
251        );
252    }
253
254    #[test]
255    fn new() {
256        assert_eq!(Date::new(0b0000_0000_0010_0001).unwrap(), Date::MIN);
257        assert_eq!(Date::new(0b1111_1111_1001_1111).unwrap(), Date::MAX);
258    }
259
260    #[test]
261    fn new_with_invalid_date() {
262        // The Day field is 0.
263        assert!(Date::new(0b0000_0000_0010_0000).is_none());
264        // The Day field is 30, which is after the last day of February.
265        assert!(Date::new(0b0000_0000_0101_1110).is_none());
266        // The Month field is 0.
267        assert!(Date::new(0b0000_0000_0000_0001).is_none());
268        // The Month field is 13.
269        assert!(Date::new(0b0000_0001_1010_0001).is_none());
270    }
271
272    #[test]
273    fn new_unchecked() {
274        assert_eq!(
275            unsafe { Date::new_unchecked(0b0000_0000_0010_0001) },
276            Date::MIN
277        );
278        assert_eq!(
279            unsafe { Date::new_unchecked(0b1111_1111_1001_1111) },
280            Date::MAX
281        );
282    }
283
284    #[test]
285    const fn new_unchecked_is_const_fn() {
286        const _: Date = unsafe { Date::new_unchecked(0b0000_0000_0010_0001) };
287    }
288
289    #[test]
290    fn from_date_before_dos_date_epoch() {
291        assert_eq!(
292            Date::from_date(date!(1979-12-31)).unwrap_err(),
293            DateRangeErrorKind::Negative.into()
294        );
295        assert_eq!(
296            Date::from_date(date!(1979-12-31)).unwrap_err(),
297            DateRangeErrorKind::Negative.into()
298        );
299    }
300
301    #[test]
302    fn from_date() {
303        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
304        assert_eq!(Date::from_date(date!(1980-01-01)).unwrap(), Date::MIN);
305        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
306        assert_eq!(
307            Date::from_date(date!(2002-11-26)).unwrap(),
308            Date::new(0b0010_1101_0111_1010).unwrap()
309        );
310        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
311        assert_eq!(
312            Date::from_date(date!(2018-11-17)).unwrap(),
313            Date::new(0b0100_1101_0111_0001).unwrap()
314        );
315        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
316        assert_eq!(Date::from_date(date!(2107-12-31)).unwrap(), Date::MAX);
317    }
318
319    #[test]
320    fn from_date_with_too_big_date() {
321        assert_eq!(
322            Date::from_date(date!(2108-01-01)).unwrap_err(),
323            DateRangeErrorKind::Overflow.into()
324        );
325    }
326
327    #[test]
328    fn is_valid() {
329        assert!(Date::MIN.is_valid());
330        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
331        assert!(Date::new(0b0010_1101_0111_1010).unwrap().is_valid());
332        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
333        assert!(Date::new(0b0100_1101_0111_0001).unwrap().is_valid());
334        assert!(Date::MAX.is_valid());
335    }
336
337    #[test]
338    fn is_valid_with_invalid_date() {
339        // The Day field is 0.
340        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0010_0000) }.is_valid());
341        // The Day field is 30, which is after the last day of February.
342        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0101_1110) }.is_valid());
343        // The Month field is 0.
344        assert!(!unsafe { Date::new_unchecked(0b0000_0000_0000_0001) }.is_valid());
345        // The Month field is 13.
346        assert!(!unsafe { Date::new_unchecked(0b0000_0001_1010_0001) }.is_valid());
347    }
348
349    #[test]
350    fn to_raw() {
351        assert_eq!(Date::MIN.to_raw(), 0b0000_0000_0010_0001);
352        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
353        assert_eq!(
354            Date::new(0b0010_1101_0111_1010).unwrap().to_raw(),
355            0b0010_1101_0111_1010
356        );
357        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
358        assert_eq!(
359            Date::new(0b0100_1101_0111_0001).unwrap().to_raw(),
360            0b0100_1101_0111_0001
361        );
362        assert_eq!(Date::MAX.to_raw(), 0b1111_1111_1001_1111);
363    }
364
365    #[test]
366    const fn to_raw_is_const_fn() {
367        const _: u16 = Date::MIN.to_raw();
368    }
369
370    #[test]
371    fn year() {
372        assert_eq!(Date::MIN.year(), 1980);
373        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
374        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().year(), 2002);
375        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
376        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().year(), 2018);
377        assert_eq!(Date::MAX.year(), 2107);
378    }
379
380    #[test]
381    const fn year_is_const_fn() {
382        const _: u16 = Date::MIN.year();
383    }
384
385    #[test]
386    fn month() {
387        assert_eq!(Date::MIN.month(), Month::January);
388        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
389        assert_eq!(
390            Date::new(0b0010_1101_0111_1010).unwrap().month(),
391            Month::November
392        );
393        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
394        assert_eq!(
395            Date::new(0b0100_1101_0111_0001).unwrap().month(),
396            Month::November
397        );
398        assert_eq!(Date::MAX.month(), Month::December);
399    }
400
401    #[test]
402    fn day() {
403        assert_eq!(Date::MIN.day(), 1);
404        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
405        assert_eq!(Date::new(0b0010_1101_0111_1010).unwrap().day(), 26);
406        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
407        assert_eq!(Date::new(0b0100_1101_0111_0001).unwrap().day(), 17);
408        assert_eq!(Date::MAX.day(), 31);
409    }
410
411    #[test]
412    fn default() {
413        assert_eq!(Date::default(), Date::MIN);
414    }
415}