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