Repository: sorairolake/nt-time (language: rust)

Issue:
Add methods to convert `FileTime` from and to DOS date and time
### Checklist

- [X] I have searched the existing issues

### Summary

Both DOS date and time and `FileTime` are used in Windows. This is used by FAT, exFAT and ZIP format. The Win32 API provides functions for similar purposes.[^1][^2]

[^1]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime
[^2]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:
## Planned API

### `FileTime` to  DOS date and time

```rust
pub fn to_dos_date_time(
    self,
    offset: Option<UtcOffset>
) -> Result<(u16, u16, u8, Option<UtcOffset>), DosDateTimeRangeError>
```

### FileTimeDOS date and time to `FileTime`

```rust
pub fn from_dos_date_time(
    date: u16,
    time: u16,
    resolution: Option<u8>,
    offset: Option<UtcOffset>
) -> Result<Self, ComponentRange>
```

Fix patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 01ad13f..059b623 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -16,6 +16,10 @@ project adheres to https://semver.org/[Semantic Versioning].
 
 == {compare-url}/v0.6.1\...HEAD[Unreleased]
 
+=== Added
+
+* Add conversion methods from/to DOS date and time ({pull-request-url}/70[#70])
+
 === Changed
 
 * Bump MSRV to 1.70.0 ({pull-request-url}/68[#68])
diff --git a/src/error.rs b/src/error.rs
index d01aa09..c751d09 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -6,6 +6,71 @@
 
 use core::fmt;
 
+/// The error type indicating that [DOS date and time] was out of range.
+///
+/// [DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
+#[allow(clippy::module_name_repetitions)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct DosDateTimeRangeError(DosDateTimeRangeErrorKind);
+
+impl DosDateTimeRangeError {
+    #[inline]
+    pub(crate) const fn new(kind: DosDateTimeRangeErrorKind) -> Self {
+        Self(kind)
+    }
+
+    /// Returns the corresponding [`DosDateTimeRangeErrorKind`] for this error.
+    #[must_use]
+    #[inline]
+    pub const fn kind(&self) -> DosDateTimeRangeErrorKind {
+        self.0
+    }
+}
+
+impl fmt::Display for DosDateTimeRangeError {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.kind().fmt(f)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for DosDateTimeRangeError {}
+
+/// Details of the error that caused a [`DosDateTimeRangeError`].
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum DosDateTimeRangeErrorKind {
+    /// Value was negative.
+    ///
+    /// This means the date and time was before "1980-01-01 00:00:00 UTC".
+    Negative,
+
+    /// Value was too big to be represented as [DOS date and time].
+    ///
+    /// This means the date and time was after "2107-12-31 23:59:59.990000000
+    /// UTC".
+    ///
+    /// [DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
+    Overflow,
+}
+
+impl fmt::Display for DosDateTimeRangeErrorKind {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Negative => {
+                write!(f, "date and time is before `1980-01-01 00:00:00 UTC`")
+            }
+            Self::Overflow => {
+                write!(
+                    f,
+                    "date and time is after `2107-12-31 23:59:59.990000000 UTC`"
+                )
+            }
+        }
+    }
+}
+
 /// The error type indicating that a [`OffsetDateTime`](time::OffsetDateTime)
 /// was out of range.
 #[allow(clippy::module_name_repetitions)]
@@ -88,6 +153,189 @@ impl fmt::Display for FileTimeRangeErrorKind {
 mod tests {
     use super::*;
 
+    #[test]
+    fn clone_dos_date_time_range_error() {
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative).clone(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow).clone(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+    }
+
+    #[test]
+    fn copy_dos_date_time_range_error() {
+        {
+            let a = DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative);
+            let b = a;
+            assert_eq!(a, b);
+        }
+
+        {
+            let a = DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow);
+            let b = a;
+            assert_eq!(a, b);
+        }
+    }
+
+    #[test]
+    fn debug_dos_date_time_range_error() {
+        assert_eq!(
+            format!(
+                "{:?}",
+                DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+            ),
+            "DosDateTimeRangeError(Negative)"
+        );
+        assert_eq!(
+            format!(
+                "{:?}",
+                DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+            ),
+            "DosDateTimeRangeError(Overflow)"
+        );
+    }
+
+    #[test]
+    fn dos_date_time_range_error_equality() {
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        assert_ne!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+        assert_ne!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+    }
+
+    #[test]
+    fn kind_dos_date_time_range_error() {
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative).kind(),
+            DosDateTimeRangeErrorKind::Negative
+        );
+        assert_eq!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow).kind(),
+            DosDateTimeRangeErrorKind::Overflow
+        );
+    }
+
+    #[test]
+    fn display_dos_date_time_range_error() {
+        assert_eq!(
+            format!(
+                "{}",
+                DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+            ),
+            "date and time is before `1980-01-01 00:00:00 UTC`"
+        );
+        assert_eq!(
+            format!(
+                "{}",
+                DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+            ),
+            "date and time is after `2107-12-31 23:59:59.990000000 UTC`"
+        );
+    }
+
+    #[cfg(feature = "std")]
+    #[test]
+    fn source_dos_date_time_range_error() {
+        use std::error::Error;
+
+        assert!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+                .source()
+                .is_none()
+        );
+        assert!(
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+                .source()
+                .is_none()
+        );
+    }
+
+    #[test]
+    fn clone_dos_date_time_range_error_kind() {
+        assert_eq!(
+            DosDateTimeRangeErrorKind::Negative.clone(),
+            DosDateTimeRangeErrorKind::Negative
+        );
+        assert_eq!(
+            DosDateTimeRangeErrorKind::Overflow.clone(),
+            DosDateTimeRangeErrorKind::Overflow
+        );
+    }
+
+    #[test]
+    fn copy_dos_date_time_range_error_kind() {
+        {
+            let a = DosDateTimeRangeErrorKind::Negative;
+            let b = a;
+            assert_eq!(a, b);
+        }
+
+        {
+            let a = DosDateTimeRangeErrorKind::Overflow;
+            let b = a;
+            assert_eq!(a, b);
+        }
+    }
+
+    #[test]
+    fn debug_dos_date_time_range_error_kind() {
+        assert_eq!(
+            format!("{:?}", DosDateTimeRangeErrorKind::Negative),
+            "Negative"
+        );
+        assert_eq!(
+            format!("{:?}", DosDateTimeRangeErrorKind::Overflow),
+            "Overflow"
+        );
+    }
+
+    #[test]
+    fn dos_date_time_range_error_kind_equality() {
+        assert_eq!(
+            DosDateTimeRangeErrorKind::Negative,
+            DosDateTimeRangeErrorKind::Negative
+        );
+        assert_ne!(
+            DosDateTimeRangeErrorKind::Negative,
+            DosDateTimeRangeErrorKind::Overflow
+        );
+        assert_ne!(
+            DosDateTimeRangeErrorKind::Overflow,
+            DosDateTimeRangeErrorKind::Negative
+        );
+        assert_eq!(
+            DosDateTimeRangeErrorKind::Overflow,
+            DosDateTimeRangeErrorKind::Overflow
+        );
+    }
+
+    #[test]
+    fn display_dos_date_time_range_error_kind() {
+        assert_eq!(
+            format!("{}", DosDateTimeRangeErrorKind::Negative),
+            "date and time is before `1980-01-01 00:00:00 UTC`"
+        );
+        assert_eq!(
+            format!("{}", DosDateTimeRangeErrorKind::Overflow),
+            "date and time is after `2107-12-31 23:59:59.990000000 UTC`"
+        );
+    }
+
     #[test]
     fn clone_offset_date_time_range_error() {
         assert_eq!(OffsetDateTimeRangeError.clone(), OffsetDateTimeRangeError);
diff --git a/src/file_time.rs b/src/file_time.rs
index 66632f1..6e42f1b 100644
--- a/src/file_time.rs
+++ b/src/file_time.rs
@@ -12,9 +12,15 @@ use core::{
     ops::{Add, AddAssign, Sub, SubAssign},
 };
 
-use time::{macros::datetime, OffsetDateTime};
+use time::{
+    error::ComponentRange, macros::datetime, Date, OffsetDateTime, PrimitiveDateTime, Time,
+    UtcOffset,
+};
 
-use crate::error::{FileTimeRangeError, FileTimeRangeErrorKind, OffsetDateTimeRangeError};
+use crate::error::{
+    DosDateTimeRangeError, DosDateTimeRangeErrorKind, FileTimeRangeError, FileTimeRangeErrorKind,
+    OffsetDateTimeRangeError,
+};
 
 const FILE_TIMES_PER_SEC: u64 = 10_000_000;
 
@@ -294,6 +300,212 @@ impl FileTime {
             .map(Self::new)
     }
 
+    #[allow(clippy::missing_panics_doc)]
+    /// Returns [DOS date and time] represents the same date and time as this
+    /// `FileTime`.
+    ///
+    /// This date and time is used as the timestamp for [FAT], [exFAT], [ZIP]
+    /// file format, etc.
+    ///
+    /// This method returns a `(date, time, resolution, offset)` tuple. `date`
+    /// and `time` represents local date and time. `resolution` represents
+    /// additional time [resolution] in 10-millisecond multiples. When the
+    /// `offset` parameter is [`Some`], converts local date and time from UTC to
+    /// the provided UTC offset and returns the [UTC offset] of local date and
+    /// time. Note that the `offset` parameter should be a multiple of 15 minute
+    /// intervals.
+    ///
+    /// # Errors
+    ///
+    /// Returns [`Err`] if the resulting date and time is out of range for DOS
+    /// date and time.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use nt_time::{time::macros::offset, FileTime};
+    /// #
+    /// // `1980-01-01 00:00:00 UTC`.
+    /// assert_eq!(
+    ///     FileTime::new(119_600_064_000_000_000)
+    ///         .to_dos_date_time(None)
+    ///         .unwrap(),
+    ///     (0x0021, u16::MIN, u8::MIN, None)
+    /// );
+    /// // `2107-12-31 23:59:59 UTC`.
+    /// assert_eq!(
+    ///     FileTime::new(159_992_927_990_000_000)
+    ///         .to_dos_date_time(None)
+    ///         .unwrap(),
+    ///     (0xff9f, 0xbf7d, 100, None)
+    /// );
+    ///
+    /// // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
+    /// // `2002-11-27 03:25:00 UTC`.
+    /// assert_eq!(
+    ///     FileTime::new(126_828_411_000_000_000)
+    ///         .to_dos_date_time(Some(offset!(-08:00)))
+    ///         .unwrap(),
+    ///     (0x2d7a, 0x9b20, u8::MIN, Some(offset!(-08:00)))
+    /// );
+    ///
+    /// // Before `1980-01-01 00:00:00 UTC`.
+    /// assert!(FileTime::new(119_600_063_990_000_000)
+    ///     .to_dos_date_time(None)
+    ///     .is_err());
+    /// // After `2107-12-31 23:59:59.990000000 UTC`.
+    /// assert!(FileTime::new(159_992_928_000_000_000)
+    ///     .to_dos_date_time(None)
+    ///     .is_err());
+    /// ```
+    ///
+    /// [DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
+    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
+    /// [exFAT]: https://en.wikipedia.org/wiki/ExFAT
+    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
+    /// [resolution]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#749-10msincrement-fields
+    /// [UTC offset]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#7410-utcoffset-fields
+    pub fn to_dos_date_time(
+        self,
+        offset: Option<UtcOffset>,
+    ) -> Result<(u16, u16, u8, Option<UtcOffset>), DosDateTimeRangeError> {
+        let mut dt = OffsetDateTime::try_from(self)
+            .map_err(|_| DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow))?;
+        if let Some(o) = offset {
+            dt = dt.to_offset(o);
+        }
+        match dt.year() {
+            ..=1979 => Err(DosDateTimeRangeError::new(
+                DosDateTimeRangeErrorKind::Negative,
+            )),
+            2108.. => Err(DosDateTimeRangeError::new(
+                DosDateTimeRangeErrorKind::Overflow,
+            )),
+            _ => {
+                let (date, time) = (dt.date(), dt.time());
+
+                let (second, minute, hour) = (time.second() / 2, time.minute(), time.hour());
+                let resolution = ((time
+                    - Time::from_hms(hour, minute, second * 2)
+                        .expect("DOS time should be in the range of `Time`"))
+                .whole_milliseconds()
+                    / 10)
+                    .try_into()
+                    .expect("resolution should be in the range of `u8`");
+                let (second, minute, hour) =
+                    (u16::from(second), u16::from(minute), u16::from(hour));
+                let dos_time = second + (minute << 5) + (hour << 11);
+
+                let (day, month, year) = (
+                    i32::from(date.day()),
+                    i32::from(u8::from(date.month())),
+                    date.year() - 1980,
+                );
+                let dos_date = (day + (month << 5) + (year << 9))
+                    .try_into()
+                    .expect("DOS date should be in the range of `u16`");
+
+                Ok((dos_date, dos_time, resolution, offset))
+            }
+        }
+    }
+
+    /// Creates a `FileTime` with the given [DOS date and time].
+    ///
+    /// This date and time is used as the timestamp for [FAT], [exFAT], [ZIP]
+    /// file format, etc.
+    ///
+    /// When `resolution` is [`Some`], additional time [resolution] in
+    /// 10-millisecond multiples is added to `time`. When `offset` is [`None`],
+    /// the [UTC offset] of local date and time is considered UTC. Note that
+    /// `offset` should be a multiple of 15 minute intervals.
+    ///
+    /// # Errors
+    ///
+    /// Returns [`Err`] if `date` or `time` is an invalid date and time.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `resolution` is greater than 199.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use nt_time::{time::macros::offset, FileTime};
+    /// #
+    /// // `1980-01-01 00:00:00 UTC`.
+    /// assert_eq!(
+    ///     FileTime::from_dos_date_time(0x0021, u16::MIN, None, None).unwrap(),
+    ///     FileTime::new(119_600_064_000_000_000)
+    /// );
+    /// // `2107-12-31 23:59:59 UTC`.
+    /// assert_eq!(
+    ///     FileTime::from_dos_date_time(0xff9f, 0xbf7d, Some(100), None).unwrap(),
+    ///     FileTime::new(159_992_927_990_000_000)
+    /// );
+    ///
+    /// // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
+    /// // `2002-11-26 19:25:00 -08:00`.
+    /// assert_eq!(
+    ///     FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:00))).unwrap(),
+    ///     FileTime::new(126_828_411_000_000_000)
+    /// );
+    ///
+    /// // The Day field is invalid.
+    /// assert!(FileTime::from_dos_date_time(0x0020, u16::MIN, None, None).is_err());
+    /// // The DoubleSeconds field is invalid.
+    /// assert!(FileTime::from_dos_date_time(0x0021, 0x001e, None, None).is_err());
+    /// ```
+    ///
+    /// [DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
+    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
+    /// [exFAT]: https://en.wikipedia.org/wiki/ExFAT
+    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
+    /// [resolution]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#749-10msincrement-fields
+    /// [UTC offset]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#7410-utcoffset-fields
+    pub fn from_dos_date_time(
+        date: u16,
+        time: u16,
+        resolution: Option<u8>,
+        offset: Option<UtcOffset>,
+    ) -> Result<Self, ComponentRange> {
+        use core::time::Duration;
+
+        let (second, minute, hour) = (
+            ((time & 0x1f) * 2)
+                .try_into()
+                .expect("second should be in the range of `u8`"),
+            ((time >> 5) & 0x3f)
+                .try_into()
+                .expect("minute should be in the range of `u8`"),
+            (time >> 11)
+                .try_into()
+                .expect("hour should be in the range of `u8`"),
+        );
+        let mut time = Time::from_hms(hour, minute, second)?;
+        if let Some(res) = resolution {
+            assert!(res <= 199);
+            time += Duration::from_millis(u64::from(res) * 10);
+        }
+
+        let (day, month, year) = (
+            (date & 0x1f)
+                .try_into()
+                .expect("day should be in the range of `u8`"),
+            u8::try_from((date >> 5) & 0x0f)
+                .expect("month should be in the range of `u8`")
+                .try_into()?,
+            ((date >> 9) + 1980).into(),
+        );
+        let date = Date::from_calendar_date(year, month, day)?;
+
+        let ft = PrimitiveDateTime::new(date, time)
+            .assume_offset(offset.unwrap_or(UtcOffset::UTC))
+            .try_into()
+            .expect("DOS date and time should be in the range of `FileTime`");
+        Ok(ft)
+    }
+
     /// Computes `self + rhs`, returning [`None`] if overflow occurred. The part
     /// of `rhs` less than 100-nanosecond is truncated.
     ///
@@ -1474,6 +1686,200 @@ mod tests {
         );
     }
 
+    #[test]
+    fn to_dos_date_time_before_dos_date_time_epoch() {
+        use time::macros::offset;
+
+        // `1979-12-31 23:59:58 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_063_980_000_000)
+                .to_dos_date_time(None)
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        // `1979-12-31 23:59:59 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_063_990_000_000)
+                .to_dos_date_time(None)
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        // `1980-01-01 00:59:58 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_099_980_000_000)
+                .to_dos_date_time(Some(offset!(-01:00)))
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        // `1980-01-01 00:59:59 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_099_990_000_000)
+                .to_dos_date_time(Some(offset!(-01:00)))
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+    }
+
+    #[test]
+    fn to_dos_date_time() {
+        use time::macros::offset;
+
+        // `1980-01-01 00:00:00 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_064_000_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0x0021, u16::MIN, u8::MIN, None)
+        );
+        // `1980-01-01 00:00:01 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_064_010_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0x0021, u16::MIN, 100, None)
+        );
+        // `2018-11-17 10:38:30 UTC`.
+        assert_eq!(
+            FileTime::new(131_869_247_100_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0x4d71, 0x54cf, u8::MIN, None)
+        );
+        // `2107-12-31 23:59:58 UTC`.
+        assert_eq!(
+            FileTime::new(159_992_927_980_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0xff9f, 0xbf7d, u8::MIN, None)
+        );
+        // `2107-12-31 23:59:59 UTC`.
+        assert_eq!(
+            FileTime::new(159_992_927_990_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0xff9f, 0xbf7d, 100, None)
+        );
+
+        // `1980-01-01 00:00:00.010000000 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_064_000_100_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0x0021, u16::MIN, 1, None)
+        );
+        // `1980-01-01 00:00:00.100000000 UTC`.
+        assert_eq!(
+            FileTime::new(119_600_064_001_000_000)
+                .to_dos_date_time(None)
+                .unwrap(),
+            (0x0021, u16::MIN, 10, None)
+        );
+
+        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
+        // `2002-11-27 03:25:00 UTC`.
+        assert_eq!(
+            FileTime::new(126_828_411_000_000_000)
+                .to_dos_date_time(Some(offset!(-08:00)))
+                .unwrap(),
+            (0x2d7a, 0x9b20, u8::MIN, Some(offset!(-08:00)))
+        );
+    }
+
+    #[test]
+    fn to_dos_date_time_with_too_big_date_time() {
+        use time::macros::offset;
+
+        // `2108-01-01 00:00:00 UTC`.
+        assert_eq!(
+            FileTime::new(159_992_928_000_000_000)
+                .to_dos_date_time(None)
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+        // `2107-12-31 23:00:00 UTC`.
+        assert_eq!(
+            FileTime::new(159_992_892_000_000_000)
+                .to_dos_date_time(Some(offset!(+01:00)))
+                .unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+    }
+
+    #[test]
+    fn from_dos_date_time() {
+        use time::macros::offset;
+
+        // `1980-01-01 00:00:00 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x0021, u16::MIN, None, None).unwrap(),
+            FileTime::new(119_600_064_000_000_000)
+        );
+        // `1980-01-01 00:00:01 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(100), None).unwrap(),
+            FileTime::new(119_600_064_010_000_000)
+        );
+        // `2018-11-17 10:38:30 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x4d71, 0x54cf, None, None).unwrap(),
+            FileTime::new(131_869_247_100_000_000)
+        );
+        // `2107-12-31 23:59:58 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0xff9f, 0xbf7d, None, None).unwrap(),
+            FileTime::new(159_992_927_980_000_000)
+        );
+        // `2107-12-31 23:59:59 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0xff9f, 0xbf7d, Some(100), None).unwrap(),
+            FileTime::new(159_992_927_990_000_000)
+        );
+
+        // `1980-01-01 00:00:00.010000000 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(1), None).unwrap(),
+            FileTime::new(119_600_064_000_100_000)
+        );
+        // `1980-01-01 00:00:00.100000000 UTC`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(10), None).unwrap(),
+            FileTime::new(119_600_064_001_000_000)
+        );
+
+        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
+        // `2002-11-26 19:25:00 -08:00`.
+        assert_eq!(
+            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:00))).unwrap(),
+            FileTime::new(126_828_411_000_000_000)
+        );
+    }
+
+    #[test]
+    fn from_dos_date_time_with_invalid_date_time() {
+        // The Day field is 0.
+        assert!(FileTime::from_dos_date_time(0x0020, u16::MIN, None, None).is_err());
+        // The Day field is 30, which is after the last day of February.
+        assert!(FileTime::from_dos_date_time(0x005e, u16::MIN, None, None).is_err());
+        // The Month field is 0.
+        assert!(FileTime::from_dos_date_time(0x0001, u16::MIN, None, None).is_err());
+        // The Month field is 13.
+        assert!(FileTime::from_dos_date_time(0x01a1, u16::MIN, None, None).is_err());
+
+        // The DoubleSeconds field is 30.
+        assert!(FileTime::from_dos_date_time(0x0021, 0x001e, None, None).is_err());
+        // The Minute field is 60.
+        assert!(FileTime::from_dos_date_time(0x0021, 0x0780, None, None).is_err());
+        // The Hour field is 24.
+        assert!(FileTime::from_dos_date_time(0x0021, 0xc000, None, None).is_err());
+    }
+
+    #[test]
+    #[should_panic(expected = "assertion failed: res <= 199")]
+    fn from_dos_date_time_with_invalid_resolution() {
+        let _: Result<FileTime, ComponentRange> =
+            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(200), None);
+    }
+
     #[test]
     fn checked_add() {
         use core::time::Duration;


Test patch: