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 ///
110 /// # Examples
111 ///
112 /// ```
113 /// # use dos_date_time::{Date, DateTime, Time};
114 /// #
115 /// assert_eq!(DateTime::MIN.is_valid(), true);
116 /// assert_eq!(DateTime::MAX.is_valid(), true);
117 ///
118 /// assert_eq!(
119 /// DateTime::new(unsafe { Date::new_unchecked(u16::MAX) }, unsafe {
120 /// Time::new_unchecked(u16::MAX)
121 /// })
122 /// .is_valid(),
123 /// false
124 /// );
125 /// ```
126 #[must_use]
127 pub fn is_valid(self) -> bool {
128 self.date().is_valid() && self.time().is_valid()
129 }
130
131 /// Gets the [`Date`] of this `DateTime`.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// # use dos_date_time::{Date, DateTime};
137 /// #
138 /// assert_eq!(DateTime::MIN.date(), Date::MIN);
139 /// assert_eq!(DateTime::MAX.date(), Date::MAX);
140 /// ```
141 #[must_use]
142 pub const fn date(self) -> Date {
143 self.date
144 }
145
146 /// Gets the [`Time`] of this `DateTime`.
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// # use dos_date_time::{DateTime, Time};
152 /// #
153 /// assert_eq!(DateTime::MIN.time(), Time::MIN);
154 /// assert_eq!(DateTime::MAX.time(), Time::MAX);
155 /// ```
156 #[must_use]
157 pub const fn time(self) -> Time {
158 self.time
159 }
160
161 /// Gets the year of this `DateTime`.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// # use dos_date_time::DateTime;
167 /// #
168 /// assert_eq!(DateTime::MIN.year(), 1980);
169 /// assert_eq!(DateTime::MAX.year(), 2107);
170 /// ```
171 #[must_use]
172 pub const fn year(self) -> u16 {
173 self.date().year()
174 }
175
176 /// Gets the month of this `DateTime`.
177 ///
178 /// # Examples
179 ///
180 /// ```
181 /// # use dos_date_time::{DateTime, time::Month};
182 /// #
183 /// assert_eq!(DateTime::MIN.month(), Month::January);
184 /// assert_eq!(DateTime::MAX.month(), Month::December);
185 /// ```
186 #[must_use]
187 pub fn month(self) -> Month {
188 self.date().month()
189 }
190
191 /// Gets the day of this `DateTime`.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// # use dos_date_time::DateTime;
197 /// #
198 /// assert_eq!(DateTime::MIN.day(), 1);
199 /// assert_eq!(DateTime::MAX.day(), 31);
200 /// ```
201 #[must_use]
202 pub fn day(self) -> u8 {
203 self.date().day()
204 }
205
206 /// Gets the hour of this `DateTime`.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// # use dos_date_time::DateTime;
212 /// #
213 /// assert_eq!(DateTime::MIN.hour(), 0);
214 /// assert_eq!(DateTime::MAX.hour(), 23);
215 /// ```
216 #[must_use]
217 pub fn hour(self) -> u8 {
218 self.time().hour()
219 }
220
221 /// Gets the minute of this `DateTime`.
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// # use dos_date_time::DateTime;
227 /// #
228 /// assert_eq!(DateTime::MIN.minute(), 0);
229 /// assert_eq!(DateTime::MAX.minute(), 59);
230 /// ```
231 #[must_use]
232 pub fn minute(self) -> u8 {
233 self.time().minute()
234 }
235
236 /// Gets the second of this `DateTime`.
237 ///
238 /// # Examples
239 ///
240 /// ```
241 /// # use dos_date_time::DateTime;
242 /// #
243 /// assert_eq!(DateTime::MIN.second(), 0);
244 /// assert_eq!(DateTime::MAX.second(), 58);
245 /// ```
246 #[must_use]
247 pub fn second(self) -> u8 {
248 self.time().second()
249 }
250}
251
252impl Default for DateTime {
253 /// Returns the default value of "1980-01-01 00:00:00".
254 ///
255 /// Equivalent to [`DateTime::MIN`] except that it is not callable in const
256 /// contexts.
257 ///
258 /// # Examples
259 ///
260 /// ```
261 /// # use dos_date_time::DateTime;
262 /// #
263 /// assert_eq!(DateTime::default(), DateTime::MIN);
264 /// ```
265 fn default() -> Self {
266 Self::MIN
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 #[cfg(feature = "std")]
273 use std::{
274 collections::hash_map::DefaultHasher,
275 hash::{Hash, Hasher},
276 };
277
278 use time::macros::{date, time};
279
280 use super::*;
281 use crate::error::DateTimeRangeErrorKind;
282
283 #[test]
284 fn clone() {
285 assert_eq!(DateTime::MIN.clone(), DateTime::MIN);
286 }
287
288 #[test]
289 fn copy() {
290 let a = DateTime::MIN;
291 let b = a;
292 assert_eq!(a, b);
293 }
294
295 #[cfg(feature = "std")]
296 #[test]
297 fn hash() {
298 assert_ne!(
299 {
300 let mut hasher = DefaultHasher::new();
301 DateTime::MIN.hash(&mut hasher);
302 hasher.finish()
303 },
304 {
305 let mut hasher = DefaultHasher::new();
306 DateTime::MAX.hash(&mut hasher);
307 hasher.finish()
308 }
309 );
310 }
311
312 #[test]
313 fn new() {
314 assert_eq!(DateTime::new(Date::MIN, Time::MIN), DateTime::MIN);
315 assert_eq!(DateTime::new(Date::MAX, Time::MAX), DateTime::MAX);
316 }
317
318 #[test]
319 const fn new_is_const_fn() {
320 const _: DateTime = DateTime::new(Date::MIN, Time::MIN);
321 }
322
323 #[test]
324 fn from_date_time_before_dos_date_time_epoch() {
325 assert_eq!(
326 DateTime::from_date_time(date!(1979-12-31), time!(23:59:58)).unwrap_err(),
327 DateTimeRangeErrorKind::Negative.into()
328 );
329 assert_eq!(
330 DateTime::from_date_time(date!(1979-12-31), time!(23:59:59)).unwrap_err(),
331 DateTimeRangeErrorKind::Negative.into()
332 );
333 }
334
335 #[test]
336 fn from_date_time() {
337 assert_eq!(
338 DateTime::from_date_time(date!(1980-01-01), time::Time::MIDNIGHT).unwrap(),
339 DateTime::MIN
340 );
341 assert_eq!(
342 DateTime::from_date_time(date!(1980-01-01), time!(00:00:01)).unwrap(),
343 DateTime::MIN
344 );
345 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
346 assert_eq!(
347 DateTime::from_date_time(date!(2002-11-26), time!(19:25:00)).unwrap(),
348 DateTime::new(
349 Date::new(0b0010_1101_0111_1010).unwrap(),
350 Time::new(0b1001_1011_0010_0000).unwrap()
351 )
352 );
353 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
354 assert_eq!(
355 DateTime::from_date_time(date!(2018-11-17), time!(10:38:30)).unwrap(),
356 DateTime::new(
357 Date::new(0b0100_1101_0111_0001).unwrap(),
358 Time::new(0b0101_0100_1100_1111).unwrap()
359 )
360 );
361 assert_eq!(
362 DateTime::from_date_time(date!(2107-12-31), time!(23:59:58)).unwrap(),
363 DateTime::MAX
364 );
365 assert_eq!(
366 DateTime::from_date_time(date!(2107-12-31), time!(23:59:59)).unwrap(),
367 DateTime::MAX
368 );
369 }
370
371 #[test]
372 fn from_date_time_with_too_big_date_time() {
373 assert_eq!(
374 DateTime::from_date_time(date!(2108-01-01), time::Time::MIDNIGHT).unwrap_err(),
375 DateTimeRangeErrorKind::Overflow.into()
376 );
377 }
378
379 #[test]
380 fn is_valid() {
381 assert!(DateTime::MIN.is_valid());
382 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
383 assert!(
384 DateTime::new(
385 Date::new(0b0010_1101_0111_1010).unwrap(),
386 Time::new(0b1001_1011_0010_0000).unwrap()
387 )
388 .is_valid()
389 );
390 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
391 assert!(
392 DateTime::new(
393 Date::new(0b0100_1101_0111_0001).unwrap(),
394 Time::new(0b0101_0100_1100_1111).unwrap()
395 )
396 .is_valid()
397 );
398 assert!(DateTime::MAX.is_valid());
399 }
400
401 #[test]
402 fn is_valid_with_invalid_date() {
403 // The Day field is 0.
404 assert!(
405 !DateTime::new(
406 unsafe { Date::new_unchecked(0b0000_0000_0010_0000) },
407 Time::MIN
408 )
409 .is_valid()
410 );
411 // The Day field is 30, which is after the last day of February.
412 assert!(
413 !DateTime::new(
414 unsafe { Date::new_unchecked(0b0000_0000_0101_1110) },
415 Time::MIN
416 )
417 .is_valid()
418 );
419 // The Month field is 0.
420 assert!(
421 !DateTime::new(
422 unsafe { Date::new_unchecked(0b0000_0000_0000_0001) },
423 Time::MIN
424 )
425 .is_valid()
426 );
427 // The Month field is 13.
428 assert!(
429 !DateTime::new(
430 unsafe { Date::new_unchecked(0b0000_0001_1010_0001) },
431 Time::MIN
432 )
433 .is_valid()
434 );
435 }
436
437 #[test]
438 fn is_valid_with_invalid_time() {
439 // The DoubleSeconds field is 30.
440 assert!(
441 !DateTime::new(Date::MIN, unsafe {
442 Time::new_unchecked(0b0000_0000_0001_1110)
443 })
444 .is_valid()
445 );
446 // The Minute field is 60.
447 assert!(
448 !DateTime::new(Date::MIN, unsafe {
449 Time::new_unchecked(0b0000_0111_1000_0000)
450 })
451 .is_valid()
452 );
453 // The Hour field is 24.
454 assert!(
455 !DateTime::new(Date::MIN, unsafe {
456 Time::new_unchecked(0b1100_0000_0000_0000)
457 })
458 .is_valid()
459 );
460 }
461
462 #[test]
463 fn is_valid_with_invalid_date_time() {
464 assert!(
465 !DateTime::new(unsafe { Date::new_unchecked(u16::MAX) }, unsafe {
466 Time::new_unchecked(u16::MAX)
467 })
468 .is_valid()
469 );
470 }
471
472 #[test]
473 fn date() {
474 assert_eq!(DateTime::MIN.date(), Date::MIN);
475 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
476 assert_eq!(
477 DateTime::new(
478 Date::new(0b0010_1101_0111_1010).unwrap(),
479 Time::new(0b1001_1011_0010_0000).unwrap()
480 )
481 .date(),
482 Date::new(0b0010_1101_0111_1010).unwrap()
483 );
484 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
485 assert_eq!(
486 DateTime::new(
487 Date::new(0b0100_1101_0111_0001).unwrap(),
488 Time::new(0b0101_0100_1100_1111).unwrap()
489 )
490 .date(),
491 Date::new(0b0100_1101_0111_0001).unwrap()
492 );
493 assert_eq!(DateTime::MAX.date(), Date::MAX);
494 }
495
496 #[test]
497 const fn date_is_const_fn() {
498 const _: Date = DateTime::MIN.date();
499 }
500
501 #[test]
502 fn time() {
503 assert_eq!(DateTime::MIN.time(), Time::MIN);
504 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
505 assert_eq!(
506 DateTime::new(
507 Date::new(0b0010_1101_0111_1010).unwrap(),
508 Time::new(0b1001_1011_0010_0000).unwrap()
509 )
510 .time(),
511 Time::new(0b1001_1011_0010_0000).unwrap()
512 );
513 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
514 assert_eq!(
515 DateTime::new(
516 Date::new(0b0100_1101_0111_0001).unwrap(),
517 Time::new(0b0101_0100_1100_1111).unwrap()
518 )
519 .time(),
520 Time::new(0b0101_0100_1100_1111).unwrap()
521 );
522 assert_eq!(DateTime::MAX.time(), Time::MAX);
523 }
524
525 #[test]
526 const fn time_is_const_fn() {
527 const _: Time = DateTime::MIN.time();
528 }
529
530 #[test]
531 fn year() {
532 assert_eq!(DateTime::MIN.year(), 1980);
533 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
534 assert_eq!(
535 DateTime::new(
536 Date::new(0b0010_1101_0111_1010).unwrap(),
537 Time::new(0b1001_1011_0010_0000).unwrap()
538 )
539 .year(),
540 2002
541 );
542 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
543 assert_eq!(
544 DateTime::new(
545 Date::new(0b0100_1101_0111_0001).unwrap(),
546 Time::new(0b0101_0100_1100_1111).unwrap()
547 )
548 .year(),
549 2018
550 );
551 assert_eq!(DateTime::MAX.year(), 2107);
552 }
553
554 #[test]
555 const fn year_is_const_fn() {
556 const _: u16 = DateTime::MIN.year();
557 }
558
559 #[test]
560 fn month() {
561 assert_eq!(DateTime::MIN.month(), Month::January);
562 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
563 assert_eq!(
564 DateTime::new(
565 Date::new(0b0010_1101_0111_1010).unwrap(),
566 Time::new(0b1001_1011_0010_0000).unwrap()
567 )
568 .month(),
569 Month::November
570 );
571 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
572 assert_eq!(
573 DateTime::new(
574 Date::new(0b0100_1101_0111_0001).unwrap(),
575 Time::new(0b0101_0100_1100_1111).unwrap()
576 )
577 .month(),
578 Month::November
579 );
580 assert_eq!(DateTime::MAX.month(), Month::December);
581 }
582
583 #[test]
584 fn day() {
585 assert_eq!(DateTime::MIN.day(), 1);
586 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
587 assert_eq!(
588 DateTime::new(
589 Date::new(0b0010_1101_0111_1010).unwrap(),
590 Time::new(0b1001_1011_0010_0000).unwrap()
591 )
592 .day(),
593 26
594 );
595 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
596 assert_eq!(
597 DateTime::new(
598 Date::new(0b0100_1101_0111_0001).unwrap(),
599 Time::new(0b0101_0100_1100_1111).unwrap()
600 )
601 .day(),
602 17
603 );
604 assert_eq!(DateTime::MAX.day(), 31);
605 }
606
607 #[test]
608 fn hour() {
609 assert_eq!(DateTime::MIN.hour(), u8::MIN);
610 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
611 assert_eq!(
612 DateTime::new(
613 Date::new(0b0010_1101_0111_1010).unwrap(),
614 Time::new(0b1001_1011_0010_0000).unwrap()
615 )
616 .hour(),
617 19
618 );
619 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
620 assert_eq!(
621 DateTime::new(
622 Date::new(0b0100_1101_0111_0001).unwrap(),
623 Time::new(0b0101_0100_1100_1111).unwrap()
624 )
625 .hour(),
626 10
627 );
628 assert_eq!(DateTime::MAX.hour(), 23);
629 }
630
631 #[test]
632 fn minute() {
633 assert_eq!(DateTime::MIN.minute(), u8::MIN);
634 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
635 assert_eq!(
636 DateTime::new(
637 Date::new(0b0010_1101_0111_1010).unwrap(),
638 Time::new(0b1001_1011_0010_0000).unwrap()
639 )
640 .minute(),
641 25
642 );
643 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
644 assert_eq!(
645 DateTime::new(
646 Date::new(0b0100_1101_0111_0001).unwrap(),
647 Time::new(0b0101_0100_1100_1111).unwrap()
648 )
649 .minute(),
650 38
651 );
652 assert_eq!(DateTime::MAX.minute(), 59);
653 }
654
655 #[test]
656 fn second() {
657 assert_eq!(DateTime::MIN.second(), u8::MIN);
658 // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
659 assert_eq!(
660 DateTime::new(
661 Date::new(0b0010_1101_0111_1010).unwrap(),
662 Time::new(0b1001_1011_0010_0000).unwrap()
663 )
664 .second(),
665 u8::MIN
666 );
667 // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
668 assert_eq!(
669 DateTime::new(
670 Date::new(0b0100_1101_0111_0001).unwrap(),
671 Time::new(0b0101_0100_1100_1111).unwrap()
672 )
673 .second(),
674 30
675 );
676 assert_eq!(DateTime::MAX.second(), 58);
677 }
678
679 #[test]
680 fn default() {
681 assert_eq!(DateTime::default(), DateTime::MIN);
682 }
683}