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}