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}