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}