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