dos_date_time/dos_date_time.rs
1// SPDX-FileCopyrightText: 2025 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! [MS-DOS date and time].
6//!
7//! [MS-DOS date and 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
14use time::Month;
15
16use crate::{Date, Time, error::DateTimeRangeError};
17
18/// `DateTime` is a type that combines a [`Date`] and a [`Time`] and represents
19/// [MS-DOS date and time].
20///
21/// These are packed 16-bit unsigned integer values that specify the date and
22/// time an MS-DOS file was last written to, and are used as timestamps such as
23/// [FAT] or [ZIP] file format.
24///
25/// <div class="warning">
26///
27/// The resolution of MS-DOS date and time is 2 seconds.
28///
29/// </div>
30///
31/// See the [format specification] for [Kaitai Struct] for more details on the
32/// structure of MS-DOS date and time.
33///
34/// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
35/// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
36/// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
37/// [format specification]: https://formats.kaitai.io/dos_datetime/
38/// [Kaitai Struct]: https://kaitai.io/
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct DateTime {
41 date: Date,
42 time: Time,
43}
44
45impl DateTime {
46 /// Creates a new `DateTime` with the given [`Date`] and [`Time`].
47 ///
48 /// # Examples
49 ///
50 /// ```
51 /// # use dos_date_time::{Date, DateTime, Time};
52 /// #
53 /// assert_eq!(DateTime::new(Date::MIN, Time::MIN), DateTime::MIN);
54 /// assert_eq!(DateTime::new(Date::MAX, Time::MAX), DateTime::MAX);
55 /// ```
56 #[must_use]
57 pub const fn new(date: Date, time: Time) -> Self {
58 Self { date, time }
59 }
60
61 /// Creates a new `DateTime` with the given [`time::Date`] and
62 /// [`time::Time`].
63 ///
64 /// <div class="warning">
65 ///
66 /// The resolution of MS-DOS date and time is 2 seconds. So this method
67 /// rounds towards zero, truncating any fractional part of the exact result
68 /// of dividing seconds by 2.
69 ///
70 /// </div>
71 ///
72 /// # Errors
73 ///
74 /// Returns [`Err`] if `date` or `time` are invalid as MS-DOS date and time.
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// # use dos_date_time::{
80 /// # DateTime,
81 /// # time::{
82 /// # Time,
83 /// # macros::{date, time},
84 /// # },
85 /// # };
86 /// #
87 /// assert_eq!(
88 /// DateTime::from_date_time(date!(1980-01-01), Time::MIDNIGHT),
89 /// Ok(DateTime::MIN)
90 /// );
91 /// assert_eq!(
92 /// DateTime::from_date_time(date!(2107-12-31), time!(23:59:58)),
93 /// Ok(DateTime::MAX)
94 /// );
95 ///
96 /// // Before `1980-01-01 00:00:00`.
97 /// assert!(DateTime::from_date_time(date!(1979-12-31), time!(23:59:59)).is_err());
98 /// // After `2107-12-31 23:59:59`.
99 /// assert!(DateTime::from_date_time(date!(2108-01-01), Time::MIDNIGHT).is_err());
100 /// ```
101 pub fn from_date_time(date: time::Date, time: time::Time) -> Result<Self, DateTimeRangeError> {
102 let (date, time) = (date.try_into()?, time.into());
103 let dt = Self::new(date, time);
104 Ok(dt)
105 }
106
107 /// Returns [`true`] if `self` is valid MS-DOS date and time, and [`false`]
108 /// otherwise.
109 #[must_use]
110 pub fn is_valid(self) -> bool {
111 self.date().is_valid() && self.time().is_valid()
112 }
113
114 /// Gets the [`Date`] of this `DateTime`.
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// # use dos_date_time::{Date, DateTime};
120 /// #
121 /// assert_eq!(DateTime::MIN.date(), Date::MIN);
122 /// assert_eq!(DateTime::MAX.date(), Date::MAX);
123 /// ```
124 #[must_use]
125 pub const fn date(self) -> Date {
126 self.date
127 }
128
129 /// Gets the [`Time`] of this `DateTime`.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// # use dos_date_time::{DateTime, Time};
135 /// #
136 /// assert_eq!(DateTime::MIN.time(), Time::MIN);
137 /// assert_eq!(DateTime::MAX.time(), Time::MAX);
138 /// ```
139 #[must_use]
140 pub const fn time(self) -> Time {
141 self.time
142 }
143
144 /// Gets the year of this `DateTime`.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// # use dos_date_time::DateTime;
150 /// #
151 /// assert_eq!(DateTime::MIN.year(), 1980);
152 /// assert_eq!(DateTime::MAX.year(), 2107);
153 /// ```
154 #[must_use]
155 pub const fn year(self) -> u16 {
156 self.date().year()
157 }
158
159 /// Gets the month of this `DateTime`.
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// # use dos_date_time::{DateTime, time::Month};
165 /// #
166 /// assert_eq!(DateTime::MIN.month(), Month::January);
167 /// assert_eq!(DateTime::MAX.month(), Month::December);
168 /// ```
169 #[must_use]
170 pub fn month(self) -> Month {
171 self.date().month()
172 }
173
174 /// Gets the day of this `DateTime`.
175 ///
176 /// # Examples
177 ///
178 /// ```
179 /// # use dos_date_time::DateTime;
180 /// #
181 /// assert_eq!(DateTime::MIN.day(), 1);
182 /// assert_eq!(DateTime::MAX.day(), 31);
183 /// ```
184 #[must_use]
185 pub fn day(self) -> u8 {
186 self.date().day()
187 }
188
189 /// Gets the hour of this `DateTime`.
190 ///
191 /// # Examples
192 ///
193 /// ```
194 /// # use dos_date_time::DateTime;
195 /// #
196 /// assert_eq!(DateTime::MIN.hour(), 0);
197 /// assert_eq!(DateTime::MAX.hour(), 23);
198 /// ```
199 #[must_use]
200 pub fn hour(self) -> u8 {
201 self.time().hour()
202 }
203
204 /// Gets the minute of this `DateTime`.
205 ///
206 /// # Examples
207 ///
208 /// ```
209 /// # use dos_date_time::DateTime;
210 /// #
211 /// assert_eq!(DateTime::MIN.minute(), 0);
212 /// assert_eq!(DateTime::MAX.minute(), 59);
213 /// ```
214 #[must_use]
215 pub fn minute(self) -> u8 {
216 self.time().minute()
217 }
218
219 /// Gets the second of this `DateTime`.
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// # use dos_date_time::DateTime;
225 /// #
226 /// assert_eq!(DateTime::MIN.second(), 0);
227 /// assert_eq!(DateTime::MAX.second(), 58);
228 /// ```
229 #[must_use]
230 pub fn second(self) -> u8 {
231 self.time().second()
232 }
233}
234
235impl Default for DateTime {
236 /// Returns the default value of "1980-01-01 00:00:00".
237 ///
238 /// Equivalent to [`DateTime::MIN`] except that it is not callable in const
239 /// contexts.
240 ///
241 /// # Examples
242 ///
243 /// ```
244 /// # use dos_date_time::DateTime;
245 /// #
246 /// assert_eq!(DateTime::default(), DateTime::MIN);
247 /// ```
248 fn default() -> Self {
249 Self::MIN
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 #[cfg(feature = "std")]
256 use std::{
257 collections::hash_map::DefaultHasher,
258 hash::{Hash, Hasher},
259 };
260
261 use time::macros::{date, time};
262
263 use super::*;
264 use crate::error::DateTimeRangeErrorKind;
265
266 #[test]
267 fn clone() {
268 assert_eq!(DateTime::MIN.clone(), DateTime::MIN);
269 }
270
271 #[test]
272 fn copy() {
273 let a = DateTime::MIN;
274 let b = a;
275 assert_eq!(a, b);
276 }
277
278 #[cfg(feature = "std")]
279 #[test]
280 fn hash() {
281 assert_ne!(
282 {
283 let mut hasher = DefaultHasher::new();
284 DateTime::MIN.hash(&mut hasher);
285 hasher.finish()
286 },
287 {
288 let mut hasher = DefaultHasher::new();
289 DateTime::MAX.hash(&mut hasher);
290 hasher.finish()
291 }
292 );
293 }
294
295 #[test]
296 fn new() {
297 assert_eq!(DateTime::new(Date::MIN, Time::MIN), DateTime::MIN);
298 assert_eq!(DateTime::new(Date::MAX, Time::MAX), DateTime::MAX);
299 }
300
301 #[test]
302 const fn new_is_const_fn() {
303 const _: DateTime = DateTime::new(Date::MIN, Time::MIN);
304 }
305
306 #[test]
307 fn from_date_time_before_dos_date_time_epoch() {
308 assert_eq!(
309 DateTime::from_date_time(date!(1979-12-31), time!(23:59:58)).unwrap_err(),
310 DateTimeRangeErrorKind::Negative.into()
311 );
312 assert_eq!(
313 DateTime::from_date_time(date!(1979-12-31), time!(23:59:59)).unwrap_err(),
314 DateTimeRangeErrorKind::Negative.into()
315 );
316 }
317
318 #[test]
319 fn from_date_time() {
320 assert_eq!(
321 DateTime::from_date_time(date!(1980-01-01), time::Time::MIDNIGHT).unwrap(),
322 DateTime::MIN
323 );
324 assert_eq!(
325 DateTime::from_date_time(date!(1980-01-01), time!(00:00:01)).unwrap(),
326 DateTime::MIN
327 );
328 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
329 assert_eq!(
330 DateTime::from_date_time(date!(2002-11-26), time!(19:25:00)).unwrap(),
331 DateTime::new(
332 Date::new(0b0010_1101_0111_1010).unwrap(),
333 Time::new(0b1001_1011_0010_0000).unwrap()
334 )
335 );
336 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
337 assert_eq!(
338 DateTime::from_date_time(date!(2018-11-17), time!(10:38:30)).unwrap(),
339 DateTime::new(
340 Date::new(0b0100_1101_0111_0001).unwrap(),
341 Time::new(0b0101_0100_1100_1111).unwrap()
342 )
343 );
344 assert_eq!(
345 DateTime::from_date_time(date!(2107-12-31), time!(23:59:58)).unwrap(),
346 DateTime::MAX
347 );
348 assert_eq!(
349 DateTime::from_date_time(date!(2107-12-31), time!(23:59:59)).unwrap(),
350 DateTime::MAX
351 );
352 }
353
354 #[test]
355 fn from_date_time_with_too_big_date_time() {
356 assert_eq!(
357 DateTime::from_date_time(date!(2108-01-01), time::Time::MIDNIGHT).unwrap_err(),
358 DateTimeRangeErrorKind::Overflow.into()
359 );
360 }
361
362 #[test]
363 fn is_valid() {
364 assert!(DateTime::MIN.is_valid());
365 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
366 assert!(
367 DateTime::new(
368 Date::new(0b0010_1101_0111_1010).unwrap(),
369 Time::new(0b1001_1011_0010_0000).unwrap()
370 )
371 .is_valid()
372 );
373 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
374 assert!(
375 DateTime::new(
376 Date::new(0b0100_1101_0111_0001).unwrap(),
377 Time::new(0b0101_0100_1100_1111).unwrap()
378 )
379 .is_valid()
380 );
381 assert!(DateTime::MAX.is_valid());
382 }
383
384 #[test]
385 fn is_valid_with_invalid_date() {
386 // The Day field is 0.
387 assert!(
388 !DateTime::new(
389 unsafe { Date::new_unchecked(0b0000_0000_0010_0000) },
390 Time::MIN
391 )
392 .is_valid()
393 );
394 // The Day field is 30, which is after the last day of February.
395 assert!(
396 !DateTime::new(
397 unsafe { Date::new_unchecked(0b0000_0000_0101_1110) },
398 Time::MIN
399 )
400 .is_valid()
401 );
402 // The Month field is 0.
403 assert!(
404 !DateTime::new(
405 unsafe { Date::new_unchecked(0b0000_0000_0000_0001) },
406 Time::MIN
407 )
408 .is_valid()
409 );
410 // The Month field is 13.
411 assert!(
412 !DateTime::new(
413 unsafe { Date::new_unchecked(0b0000_0001_1010_0001) },
414 Time::MIN
415 )
416 .is_valid()
417 );
418 }
419
420 #[test]
421 fn is_valid_with_invalid_time() {
422 // The DoubleSeconds field is 30.
423 assert!(
424 !DateTime::new(Date::MIN, unsafe {
425 Time::new_unchecked(0b0000_0000_0001_1110)
426 })
427 .is_valid()
428 );
429 // The Minute field is 60.
430 assert!(
431 !DateTime::new(Date::MIN, unsafe {
432 Time::new_unchecked(0b0000_0111_1000_0000)
433 })
434 .is_valid()
435 );
436 // The Hour field is 24.
437 assert!(
438 !DateTime::new(Date::MIN, unsafe {
439 Time::new_unchecked(0b1100_0000_0000_0000)
440 })
441 .is_valid()
442 );
443 }
444
445 #[test]
446 fn is_valid_with_invalid_date_time() {
447 assert!(
448 !DateTime::new(unsafe { Date::new_unchecked(u16::MAX) }, unsafe {
449 Time::new_unchecked(u16::MAX)
450 })
451 .is_valid()
452 );
453 }
454
455 #[test]
456 fn date() {
457 assert_eq!(DateTime::MIN.date(), Date::MIN);
458 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
459 assert_eq!(
460 DateTime::new(
461 Date::new(0b0010_1101_0111_1010).unwrap(),
462 Time::new(0b1001_1011_0010_0000).unwrap()
463 )
464 .date(),
465 Date::new(0b0010_1101_0111_1010).unwrap()
466 );
467 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
468 assert_eq!(
469 DateTime::new(
470 Date::new(0b0100_1101_0111_0001).unwrap(),
471 Time::new(0b0101_0100_1100_1111).unwrap()
472 )
473 .date(),
474 Date::new(0b0100_1101_0111_0001).unwrap()
475 );
476 assert_eq!(DateTime::MAX.date(), Date::MAX);
477 }
478
479 #[test]
480 const fn date_is_const_fn() {
481 const _: Date = DateTime::MIN.date();
482 }
483
484 #[test]
485 fn time() {
486 assert_eq!(DateTime::MIN.time(), Time::MIN);
487 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
488 assert_eq!(
489 DateTime::new(
490 Date::new(0b0010_1101_0111_1010).unwrap(),
491 Time::new(0b1001_1011_0010_0000).unwrap()
492 )
493 .time(),
494 Time::new(0b1001_1011_0010_0000).unwrap()
495 );
496 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
497 assert_eq!(
498 DateTime::new(
499 Date::new(0b0100_1101_0111_0001).unwrap(),
500 Time::new(0b0101_0100_1100_1111).unwrap()
501 )
502 .time(),
503 Time::new(0b0101_0100_1100_1111).unwrap()
504 );
505 assert_eq!(DateTime::MAX.time(), Time::MAX);
506 }
507
508 #[test]
509 const fn time_is_const_fn() {
510 const _: Time = DateTime::MIN.time();
511 }
512
513 #[test]
514 fn year() {
515 assert_eq!(DateTime::MIN.year(), 1980);
516 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
517 assert_eq!(
518 DateTime::new(
519 Date::new(0b0010_1101_0111_1010).unwrap(),
520 Time::new(0b1001_1011_0010_0000).unwrap()
521 )
522 .year(),
523 2002
524 );
525 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
526 assert_eq!(
527 DateTime::new(
528 Date::new(0b0100_1101_0111_0001).unwrap(),
529 Time::new(0b0101_0100_1100_1111).unwrap()
530 )
531 .year(),
532 2018
533 );
534 assert_eq!(DateTime::MAX.year(), 2107);
535 }
536
537 #[test]
538 const fn year_is_const_fn() {
539 const _: u16 = DateTime::MIN.year();
540 }
541
542 #[test]
543 fn month() {
544 assert_eq!(DateTime::MIN.month(), Month::January);
545 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
546 assert_eq!(
547 DateTime::new(
548 Date::new(0b0010_1101_0111_1010).unwrap(),
549 Time::new(0b1001_1011_0010_0000).unwrap()
550 )
551 .month(),
552 Month::November
553 );
554 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
555 assert_eq!(
556 DateTime::new(
557 Date::new(0b0100_1101_0111_0001).unwrap(),
558 Time::new(0b0101_0100_1100_1111).unwrap()
559 )
560 .month(),
561 Month::November
562 );
563 assert_eq!(DateTime::MAX.month(), Month::December);
564 }
565
566 #[test]
567 fn day() {
568 assert_eq!(DateTime::MIN.day(), 1);
569 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
570 assert_eq!(
571 DateTime::new(
572 Date::new(0b0010_1101_0111_1010).unwrap(),
573 Time::new(0b1001_1011_0010_0000).unwrap()
574 )
575 .day(),
576 26
577 );
578 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
579 assert_eq!(
580 DateTime::new(
581 Date::new(0b0100_1101_0111_0001).unwrap(),
582 Time::new(0b0101_0100_1100_1111).unwrap()
583 )
584 .day(),
585 17
586 );
587 assert_eq!(DateTime::MAX.day(), 31);
588 }
589
590 #[test]
591 fn hour() {
592 assert_eq!(DateTime::MIN.hour(), u8::MIN);
593 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
594 assert_eq!(
595 DateTime::new(
596 Date::new(0b0010_1101_0111_1010).unwrap(),
597 Time::new(0b1001_1011_0010_0000).unwrap()
598 )
599 .hour(),
600 19
601 );
602 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
603 assert_eq!(
604 DateTime::new(
605 Date::new(0b0100_1101_0111_0001).unwrap(),
606 Time::new(0b0101_0100_1100_1111).unwrap()
607 )
608 .hour(),
609 10
610 );
611 assert_eq!(DateTime::MAX.hour(), 23);
612 }
613
614 #[test]
615 fn minute() {
616 assert_eq!(DateTime::MIN.minute(), u8::MIN);
617 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
618 assert_eq!(
619 DateTime::new(
620 Date::new(0b0010_1101_0111_1010).unwrap(),
621 Time::new(0b1001_1011_0010_0000).unwrap()
622 )
623 .minute(),
624 25
625 );
626 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
627 assert_eq!(
628 DateTime::new(
629 Date::new(0b0100_1101_0111_0001).unwrap(),
630 Time::new(0b0101_0100_1100_1111).unwrap()
631 )
632 .minute(),
633 38
634 );
635 assert_eq!(DateTime::MAX.minute(), 59);
636 }
637
638 #[test]
639 fn second() {
640 assert_eq!(DateTime::MIN.second(), u8::MIN);
641 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
642 assert_eq!(
643 DateTime::new(
644 Date::new(0b0010_1101_0111_1010).unwrap(),
645 Time::new(0b1001_1011_0010_0000).unwrap()
646 )
647 .second(),
648 u8::MIN
649 );
650 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
651 assert_eq!(
652 DateTime::new(
653 Date::new(0b0100_1101_0111_0001).unwrap(),
654 Time::new(0b0101_0100_1100_1111).unwrap()
655 )
656 .second(),
657 30
658 );
659 assert_eq!(DateTime::MAX.second(), 58);
660 }
661
662 #[test]
663 fn default() {
664 assert_eq!(DateTime::default(), DateTime::MIN);
665 }
666}