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