dos_date_time/
dos_time.rs

1// SPDX-FileCopyrightText: 2025 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! The [MS-DOS time].
6//!
7//! [MS-DOS time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
8
9mod cmp;
10mod consts;
11mod convert;
12mod fmt;
13
14/// `Time` is a type that represents the [MS-DOS time].
15///
16/// This is a packed 16-bit unsigned integer value that specify the time an
17/// MS-DOS file was last written to, and is used as timestamps such as [FAT] or
18/// [ZIP] file format.
19///
20/// <div class="warning">
21///
22/// The resolution of MS-DOS time is 2 seconds.
23///
24/// </div>
25///
26/// See the [format specification] for [Kaitai Struct] for more details on the
27/// structure of the MS-DOS time.
28///
29/// [MS-DOS time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
30/// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
31/// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
32/// [format specification]: https://formats.kaitai.io/dos_datetime/
33/// [Kaitai Struct]: https://kaitai.io/
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35#[repr(transparent)]
36pub struct Time(u16);
37
38impl Time {
39    #[allow(clippy::missing_panics_doc)]
40    /// Creates a new `Time` with the given MS-DOS time.
41    ///
42    /// Returns [`None`] if the given MS-DOS time is not a valid MS-DOS time.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// # use dos_date_time::Time;
48    /// #
49    /// assert_eq!(Time::new(u16::MIN), Some(Time::MIN));
50    /// assert_eq!(Time::new(0b1011_1111_0111_1101), Some(Time::MAX));
51    ///
52    /// // The DoubleSeconds field is 30.
53    /// assert_eq!(Time::new(0b0000_0000_0001_1110), None);
54    /// ```
55    #[must_use]
56    pub fn new(time: u16) -> Option<Self> {
57        let (hour, minute, second) = (
58            (time >> 11)
59                .try_into()
60                .expect("hour should be in the range of `u8`"),
61            ((time >> 5) & 0x3f)
62                .try_into()
63                .expect("minute should be in the range of `u8`"),
64            ((time & 0x1f) * 2)
65                .try_into()
66                .expect("second should be in the range of `u8`"),
67        );
68        let time = time::Time::from_hms(hour, minute, second).ok()?;
69        let time = Self::from_time(time);
70        Some(time)
71    }
72
73    /// Creates a new `Time` with the given MS-DOS time.
74    ///
75    /// # Safety
76    ///
77    /// The given MS-DOS time must be a valid MS-DOS time.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// # use dos_date_time::Time;
83    /// #
84    /// assert_eq!(unsafe { Time::new_unchecked(u16::MIN) }, Time::MIN);
85    /// assert_eq!(
86    ///     unsafe { Time::new_unchecked(0b1011_1111_0111_1101) },
87    ///     Time::MAX
88    /// );
89    /// ```
90    #[must_use]
91    pub const unsafe fn new_unchecked(time: u16) -> Self {
92        Self(time)
93    }
94
95    /// Creates a new `Time` with the given [`time::Time`].
96    ///
97    /// <div class="warning">
98    ///
99    /// The resolution of MS-DOS time is 2 seconds. So this method rounds
100    /// towards zero, truncating any fractional part of the exact result of
101    /// dividing seconds by 2.
102    ///
103    /// </div>
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// # use dos_date_time::{
109    /// #     Time,
110    /// #     time::{self, macros::time},
111    /// # };
112    /// #
113    /// assert_eq!(Time::from_time(time::Time::MIDNIGHT), Time::MIN);
114    /// assert_eq!(Time::from_time(time!(23:59:58)), Time::MAX);
115    /// ```
116    #[must_use]
117    pub fn from_time(time: time::Time) -> Self {
118        let (hour, minute, second) = (
119            u16::from(time.hour()),
120            u16::from(time.minute()),
121            u16::from(time.second() / 2),
122        );
123        // <https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#7481-doubleseconds-field>.
124        let second = second.min(29);
125        let time = (hour << 11) | (minute << 5) | second;
126        // SAFETY: `time` is a valid as the MS-DOS time.
127        unsafe { Self::new_unchecked(time) }
128    }
129
130    /// Returns [`true`] if `self` is a valid MS-DOS time, and [`false`]
131    /// otherwise.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// # use dos_date_time::Time;
137    /// #
138    /// assert_eq!(Time::MIN.is_valid(), true);
139    /// assert_eq!(Time::MAX.is_valid(), true);
140    ///
141    /// // The DoubleSeconds field is 30.
142    /// assert_eq!(
143    ///     unsafe { Time::new_unchecked(0b0000_0000_0001_1110) }.is_valid(),
144    ///     false
145    /// );
146    /// ```
147    #[must_use]
148    pub fn is_valid(self) -> bool {
149        Self::new(self.to_raw()).is_some()
150    }
151
152    /// Returns the MS-DOS time of this `Time` as the underlying [`u16`] value.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// # use dos_date_time::Time;
158    /// #
159    /// assert_eq!(Time::MIN.to_raw(), u16::MIN);
160    /// assert_eq!(Time::MAX.to_raw(), 0b1011_1111_0111_1101);
161    /// ```
162    #[must_use]
163    pub const fn to_raw(self) -> u16 {
164        self.0
165    }
166
167    #[allow(clippy::missing_panics_doc)]
168    /// Gets the hour of this `Time`.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// # use dos_date_time::Time;
174    /// #
175    /// assert_eq!(Time::MIN.hour(), 0);
176    /// assert_eq!(Time::MAX.hour(), 23);
177    /// ```
178    #[must_use]
179    pub fn hour(self) -> u8 {
180        (self.to_raw() >> 11)
181            .try_into()
182            .expect("hour should be in the range of `u8`")
183    }
184
185    #[allow(clippy::missing_panics_doc)]
186    /// Gets the minute of this `Time`.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// # use dos_date_time::Time;
192    /// #
193    /// assert_eq!(Time::MIN.minute(), 0);
194    /// assert_eq!(Time::MAX.minute(), 59);
195    /// ```
196    #[must_use]
197    pub fn minute(self) -> u8 {
198        ((self.to_raw() >> 5) & 0x3f)
199            .try_into()
200            .expect("minute should be in the range of `u8`")
201    }
202
203    #[allow(clippy::missing_panics_doc)]
204    /// Gets the second of this `Time`.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// # use dos_date_time::Time;
210    /// #
211    /// assert_eq!(Time::MIN.second(), 0);
212    /// assert_eq!(Time::MAX.second(), 58);
213    /// ```
214    #[must_use]
215    pub fn second(self) -> u8 {
216        ((self.to_raw() & 0x1f) * 2)
217            .try_into()
218            .expect("second should be in the range of `u8`")
219    }
220}
221
222impl Default for Time {
223    /// Returns the default value of "00:00:00".
224    ///
225    /// Equivalent to [`Time::MIN`] except that it is not callable in const
226    /// contexts.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// # use dos_date_time::Time;
232    /// #
233    /// assert_eq!(Time::default(), Time::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::time;
250
251    use super::*;
252
253    #[test]
254    fn size_of() {
255        assert_eq!(mem::size_of::<Time>(), mem::size_of::<u16>());
256    }
257
258    #[test]
259    fn align_of() {
260        assert_eq!(mem::align_of::<Time>(), mem::align_of::<u16>());
261    }
262
263    #[test]
264    fn clone() {
265        assert_eq!(Time::MIN.clone(), Time::MIN);
266    }
267
268    #[test]
269    fn copy() {
270        let a = Time::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                Time::MIN.hash(&mut hasher);
282                hasher.finish()
283            },
284            {
285                let mut hasher = DefaultHasher::new();
286                Time::MAX.hash(&mut hasher);
287                hasher.finish()
288            }
289        );
290    }
291
292    #[test]
293    fn new() {
294        assert_eq!(Time::new(u16::MIN).unwrap(), Time::MIN);
295        assert_eq!(Time::new(0b1011_1111_0111_1101).unwrap(), Time::MAX);
296    }
297
298    #[test]
299    fn new_with_invalid_time() {
300        // The DoubleSeconds field is 30.
301        assert!(Time::new(0b0000_0000_0001_1110).is_none());
302        // The Minute field is 60.
303        assert!(Time::new(0b0000_0111_1000_0000).is_none());
304        // The Hour field is 24.
305        assert!(Time::new(0b1100_0000_0000_0000).is_none());
306    }
307
308    #[test]
309    fn new_unchecked() {
310        assert_eq!(unsafe { Time::new_unchecked(u16::MIN) }, Time::MIN);
311        assert_eq!(
312            unsafe { Time::new_unchecked(0b1011_1111_0111_1101) },
313            Time::MAX
314        );
315    }
316
317    #[test]
318    const fn new_unchecked_is_const_fn() {
319        const _: Time = unsafe { Time::new_unchecked(u16::MIN) };
320    }
321
322    #[test]
323    fn from_time() {
324        assert_eq!(Time::from_time(time::Time::MIDNIGHT), Time::MIN);
325        assert_eq!(Time::from_time(time!(00:00:01)), Time::MIN);
326        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
327        assert_eq!(
328            Time::from_time(time!(19:25:00)),
329            Time::new(0b1001_1011_0010_0000).unwrap()
330        );
331        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
332        assert_eq!(
333            Time::from_time(time!(10:38:30)),
334            Time::new(0b0101_0100_1100_1111).unwrap()
335        );
336        assert_eq!(Time::from_time(time!(23:59:58)), Time::MAX);
337        assert_eq!(Time::from_time(time!(23:59:59)), Time::MAX);
338    }
339
340    #[test]
341    fn is_valid() {
342        assert!(Time::MIN.is_valid());
343        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
344        assert!(Time::new(0b1001_1011_0010_0000).unwrap().is_valid());
345        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
346        assert!(Time::new(0b0101_0100_1100_1111).unwrap().is_valid());
347        assert!(Time::MAX.is_valid());
348    }
349
350    #[test]
351    fn is_valid_with_invalid_time() {
352        // The DoubleSeconds field is 30.
353        assert!(!unsafe { Time::new_unchecked(0b0000_0000_0001_1110) }.is_valid());
354        // The Minute field is 60.
355        assert!(!unsafe { Time::new_unchecked(0b0000_0111_1000_0000) }.is_valid());
356        // The Hour field is 24.
357        assert!(!unsafe { Time::new_unchecked(0b1100_0000_0000_0000) }.is_valid());
358    }
359
360    #[test]
361    fn to_raw() {
362        assert_eq!(Time::MIN.to_raw(), u16::MIN);
363        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
364        assert_eq!(
365            Time::new(0b1001_1011_0010_0000).unwrap().to_raw(),
366            0b1001_1011_0010_0000
367        );
368        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
369        assert_eq!(
370            Time::new(0b0101_0100_1100_1111).unwrap().to_raw(),
371            0b0101_0100_1100_1111
372        );
373        assert_eq!(Time::MAX.to_raw(), 0b1011_1111_0111_1101);
374    }
375
376    #[test]
377    const fn to_raw_is_const_fn() {
378        const _: u16 = Time::MIN.to_raw();
379    }
380
381    #[test]
382    fn hour() {
383        assert_eq!(Time::MIN.hour(), u8::MIN);
384        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
385        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().hour(), 19);
386        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
387        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().hour(), 10);
388        assert_eq!(Time::MAX.hour(), 23);
389    }
390
391    #[test]
392    fn minute() {
393        assert_eq!(Time::MIN.minute(), u8::MIN);
394        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
395        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().minute(), 25);
396        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
397        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().minute(), 38);
398        assert_eq!(Time::MAX.minute(), 59);
399    }
400
401    #[test]
402    fn second() {
403        assert_eq!(Time::MIN.second(), u8::MIN);
404        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
405        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().second(), u8::MIN);
406        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
407        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().second(), 30);
408        assert_eq!(Time::MAX.second(), 58);
409    }
410
411    #[test]
412    fn default() {
413        assert_eq!(Time::default(), Time::MIN);
414    }
415}