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 Ok(Self::new(date, time))
108 }
109
110 /// Returns [`true`] if `self` is valid MS-DOS date and time, and [`false`]
111 /// otherwise.
112 #[must_use]
113 pub fn is_valid(self) -> bool {
114 self.date().is_valid() && self.time().is_valid()
115 }
116
117 /// Gets the [`Date`] of this `DateTime`.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// # use dos_date_time::{Date, DateTime};
123 /// #
124 /// assert_eq!(DateTime::MIN.date(), Date::MIN);
125 /// assert_eq!(DateTime::MAX.date(), Date::MAX);
126 /// ```
127 #[must_use]
128 pub const fn date(self) -> Date {
129 self.date
130 }
131
132 /// Gets the [`Time`] of this `DateTime`.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// # use dos_date_time::{DateTime, Time};
138 /// #
139 /// assert_eq!(DateTime::MIN.time(), Time::MIN);
140 /// assert_eq!(DateTime::MAX.time(), Time::MAX);
141 /// ```
142 #[must_use]
143 pub const fn time(self) -> Time {
144 self.time
145 }
146
147 /// Gets the year of this `DateTime`.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// # use dos_date_time::DateTime;
153 /// #
154 /// assert_eq!(DateTime::MIN.year(), 1980);
155 /// assert_eq!(DateTime::MAX.year(), 2107);
156 /// ```
157 #[must_use]
158 pub const fn year(self) -> u16 {
159 self.date().year()
160 }
161
162 /// Gets the month of this `DateTime`.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// # use dos_date_time::{DateTime, time::Month};
168 /// #
169 /// assert_eq!(DateTime::MIN.month(), Month::January);
170 /// assert_eq!(DateTime::MAX.month(), Month::December);
171 /// ```
172 #[must_use]
173 pub fn month(self) -> Month {
174 self.date().month()
175 }
176
177 /// Gets the day of this `DateTime`.
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// # use dos_date_time::DateTime;
183 /// #
184 /// assert_eq!(DateTime::MIN.day(), 1);
185 /// assert_eq!(DateTime::MAX.day(), 31);
186 /// ```
187 #[must_use]
188 pub fn day(self) -> u8 {
189 self.date().day()
190 }
191
192 /// Gets the hour of this `DateTime`.
193 ///
194 /// # Examples
195 ///
196 /// ```
197 /// # use dos_date_time::DateTime;
198 /// #
199 /// assert_eq!(DateTime::MIN.hour(), 0);
200 /// assert_eq!(DateTime::MAX.hour(), 23);
201 /// ```
202 #[must_use]
203 pub fn hour(self) -> u8 {
204 self.time().hour()
205 }
206
207 /// Gets the minute of this `DateTime`.
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// # use dos_date_time::DateTime;
213 /// #
214 /// assert_eq!(DateTime::MIN.minute(), 0);
215 /// assert_eq!(DateTime::MAX.minute(), 59);
216 /// ```
217 #[must_use]
218 pub fn minute(self) -> u8 {
219 self.time().minute()
220 }
221
222 /// Gets the second of this `DateTime`.
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// # use dos_date_time::DateTime;
228 /// #
229 /// assert_eq!(DateTime::MIN.second(), 0);
230 /// assert_eq!(DateTime::MAX.second(), 58);
231 /// ```
232 #[must_use]
233 pub fn second(self) -> u8 {
234 self.time().second()
235 }
236}
237
238impl Default for DateTime {
239 /// Returns the default value of "1980-01-01 00:00:00".
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}