Skip to main content

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.
17///
18/// <div class="warning">
19///
20/// It has the following peculiarities:
21///
22/// - It has a resolution of 2 seconds.
23/// - It does not support leap seconds.
24///
25/// </div>
26///
27/// See the [format specification] for [Kaitai Struct] for more details on the
28/// structure of the MS-DOS time.
29///
30/// [MS-DOS time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
31/// [format specification]: https://formats.kaitai.io/dos_datetime/
32/// [Kaitai Struct]: https://kaitai.io/
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34#[repr(transparent)]
35pub struct Time(u16);
36
37impl Time {
38    #[expect(clippy::missing_panics_doc)]
39    /// Creates a new `Time` with the given underlying [`u16`] value.
40    ///
41    /// Returns [`None`] if the given value is not a valid MS-DOS time.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// # use dos_date_time::Time;
47    /// #
48    /// assert_eq!(Time::new(u16::MIN), Some(Time::MIN));
49    /// assert_eq!(Time::new(0b1011_1111_0111_1101), Some(Time::MAX));
50    ///
51    /// // The DoubleSeconds field is 30.
52    /// assert_eq!(Time::new(0b0000_0000_0001_1110), None);
53    /// ```
54    #[must_use]
55    pub fn new(time: u16) -> Option<Self> {
56        let (hour, minute, second) = (
57            (time >> 11)
58                .try_into()
59                .expect("hour should be in the range of `u8`"),
60            ((time >> 5) & 0x3F)
61                .try_into()
62                .expect("minute should be in the range of `u8`"),
63            ((time & 0x1F) * 2)
64                .try_into()
65                .expect("second should be in the range of `u8`"),
66        );
67        let time = time::Time::from_hms(hour, minute, second).ok()?;
68        Some(Self::from_time(time))
69    }
70
71    /// Creates a new `Time` with the given underlying [`u16`] value.
72    ///
73    /// # Safety
74    ///
75    /// The given value must be a valid MS-DOS time.
76    #[must_use]
77    pub const unsafe fn new_unchecked(time: u16) -> Self {
78        Self(time)
79    }
80
81    /// Creates a new `Time` with the given [`time::Time`].
82    ///
83    /// <div class="warning">
84    ///
85    /// This method may round towards zero, truncating more precise times that a
86    /// `Time` cannot store.
87    ///
88    /// </div>
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// # use dos_date_time::{
94    /// #     Time,
95    /// #     time::{self, macros::time},
96    /// # };
97    /// #
98    /// assert_eq!(Time::from_time(time::Time::MIDNIGHT), Time::MIN);
99    /// assert_eq!(Time::from_time(time!(23:59:58)), Time::MAX);
100    /// ```
101    #[must_use]
102    pub fn from_time(time: time::Time) -> Self {
103        let (hour, minute, second) = (
104            u16::from(time.hour()),
105            u16::from(time.minute()),
106            u16::from(time.second() / 2),
107        );
108        let second = second.min(29);
109        let time = (hour << 11) | (minute << 5) | second;
110        // SAFETY: `time` is a valid as the MS-DOS time.
111        unsafe { Self::new_unchecked(time) }
112    }
113
114    /// Returns [`true`] if `self` is a valid MS-DOS time, and [`false`]
115    /// otherwise.
116    #[must_use]
117    pub fn is_valid(self) -> bool {
118        Self::new(self.to_raw()).is_some()
119    }
120
121    /// Returns this `Time` as the underlying [`u16`] value.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// # use dos_date_time::Time;
127    /// #
128    /// assert_eq!(Time::MIN.to_raw(), u16::MIN);
129    /// assert_eq!(Time::MAX.to_raw(), 0b1011_1111_0111_1101);
130    /// ```
131    #[must_use]
132    pub const fn to_raw(self) -> u16 {
133        self.0
134    }
135
136    #[expect(clippy::missing_panics_doc)]
137    /// Gets the hour of this `Time`.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// # use dos_date_time::Time;
143    /// #
144    /// assert_eq!(Time::MIN.hour(), 0);
145    /// assert_eq!(Time::MAX.hour(), 23);
146    /// ```
147    #[must_use]
148    pub fn hour(self) -> u8 {
149        (self.to_raw() >> 11)
150            .try_into()
151            .expect("hour should be in the range of `u8`")
152    }
153
154    #[expect(clippy::missing_panics_doc)]
155    /// Gets the minute of this `Time`.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// # use dos_date_time::Time;
161    /// #
162    /// assert_eq!(Time::MIN.minute(), 0);
163    /// assert_eq!(Time::MAX.minute(), 59);
164    /// ```
165    #[must_use]
166    pub fn minute(self) -> u8 {
167        ((self.to_raw() >> 5) & 0x3F)
168            .try_into()
169            .expect("minute should be in the range of `u8`")
170    }
171
172    #[expect(clippy::missing_panics_doc)]
173    /// Gets the second of this `Time`.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// # use dos_date_time::Time;
179    /// #
180    /// assert_eq!(Time::MIN.second(), 0);
181    /// assert_eq!(Time::MAX.second(), 58);
182    /// ```
183    #[must_use]
184    pub fn second(self) -> u8 {
185        ((self.to_raw() & 0x1F) * 2)
186            .try_into()
187            .expect("second should be in the range of `u8`")
188    }
189}
190
191impl Default for Time {
192    /// Returns the default value of "00:00:00".
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// # use dos_date_time::Time;
198    /// #
199    /// assert_eq!(Time::default(), Time::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::time;
216
217    use super::*;
218
219    #[test]
220    fn size_of() {
221        assert_eq!(mem::size_of::<Time>(), mem::size_of::<u16>());
222    }
223
224    #[test]
225    fn align_of() {
226        assert_eq!(mem::align_of::<Time>(), mem::align_of::<u16>());
227    }
228
229    #[test]
230    fn clone() {
231        assert_eq!(Time::MIN.clone(), Time::MIN);
232    }
233
234    #[test]
235    fn copy() {
236        let a = Time::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                Time::MIN.hash(&mut hasher);
248                hasher.finish()
249            },
250            {
251                let mut hasher = DefaultHasher::new();
252                Time::MAX.hash(&mut hasher);
253                hasher.finish()
254            }
255        );
256    }
257
258    #[test]
259    fn new() {
260        assert_eq!(Time::new(u16::MIN).unwrap(), Time::MIN);
261        assert_eq!(Time::new(0b1011_1111_0111_1101).unwrap(), Time::MAX);
262    }
263
264    #[test]
265    fn new_with_invalid_time() {
266        // The DoubleSeconds field is 30.
267        assert!(Time::new(0b0000_0000_0001_1110).is_none());
268        // The Minute field is 60.
269        assert!(Time::new(0b0000_0111_1000_0000).is_none());
270        // The Hour field is 24.
271        assert!(Time::new(0b1100_0000_0000_0000).is_none());
272    }
273
274    #[test]
275    fn new_unchecked() {
276        assert_eq!(unsafe { Time::new_unchecked(u16::MIN) }, Time::MIN);
277        assert_eq!(
278            unsafe { Time::new_unchecked(0b1011_1111_0111_1101) },
279            Time::MAX
280        );
281    }
282
283    #[test]
284    const fn new_unchecked_is_const_fn() {
285        const _: Time = unsafe { Time::new_unchecked(u16::MIN) };
286    }
287
288    #[test]
289    fn from_time() {
290        assert_eq!(Time::from_time(time::Time::MIDNIGHT), Time::MIN);
291        assert_eq!(Time::from_time(time!(00:00:01)), Time::MIN);
292        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
293        assert_eq!(
294            Time::from_time(time!(19:25:00)),
295            Time::new(0b1001_1011_0010_0000).unwrap()
296        );
297        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
298        assert_eq!(
299            Time::from_time(time!(10:38:30)),
300            Time::new(0b0101_0100_1100_1111).unwrap()
301        );
302        assert_eq!(Time::from_time(time!(23:59:58)), Time::MAX);
303        assert_eq!(Time::from_time(time!(23:59:59)), Time::MAX);
304    }
305
306    #[test]
307    fn is_valid() {
308        assert!(Time::MIN.is_valid());
309        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
310        assert!(Time::new(0b1001_1011_0010_0000).unwrap().is_valid());
311        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
312        assert!(Time::new(0b0101_0100_1100_1111).unwrap().is_valid());
313        assert!(Time::MAX.is_valid());
314    }
315
316    #[test]
317    fn is_valid_with_invalid_time() {
318        // The DoubleSeconds field is 30.
319        assert!(!unsafe { Time::new_unchecked(0b0000_0000_0001_1110) }.is_valid());
320        // The Minute field is 60.
321        assert!(!unsafe { Time::new_unchecked(0b0000_0111_1000_0000) }.is_valid());
322        // The Hour field is 24.
323        assert!(!unsafe { Time::new_unchecked(0b1100_0000_0000_0000) }.is_valid());
324    }
325
326    #[test]
327    fn to_raw() {
328        assert_eq!(Time::MIN.to_raw(), u16::MIN);
329        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
330        assert_eq!(
331            Time::new(0b1001_1011_0010_0000).unwrap().to_raw(),
332            0b1001_1011_0010_0000
333        );
334        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
335        assert_eq!(
336            Time::new(0b0101_0100_1100_1111).unwrap().to_raw(),
337            0b0101_0100_1100_1111
338        );
339        assert_eq!(Time::MAX.to_raw(), 0b1011_1111_0111_1101);
340    }
341
342    #[test]
343    const fn to_raw_is_const_fn() {
344        const _: u16 = Time::MIN.to_raw();
345    }
346
347    #[test]
348    fn hour() {
349        assert_eq!(Time::MIN.hour(), u8::MIN);
350        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
351        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().hour(), 19);
352        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
353        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().hour(), 10);
354        assert_eq!(Time::MAX.hour(), 23);
355    }
356
357    #[test]
358    fn minute() {
359        assert_eq!(Time::MIN.minute(), u8::MIN);
360        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
361        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().minute(), 25);
362        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
363        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().minute(), 38);
364        assert_eq!(Time::MAX.minute(), 59);
365    }
366
367    #[test]
368    fn second() {
369        assert_eq!(Time::MIN.second(), u8::MIN);
370        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
371        assert_eq!(Time::new(0b1001_1011_0010_0000).unwrap().second(), u8::MIN);
372        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
373        assert_eq!(Time::new(0b0101_0100_1100_1111).unwrap().second(), 30);
374        assert_eq!(Time::MAX.second(), 58);
375    }
376
377    #[test]
378    fn default() {
379        assert_eq!(Time::default(), Time::MIN);
380    }
381}