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}