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