Skip to main content

dos_date_time/dos_date/
convert.rs

1// SPDX-FileCopyrightText: 2025 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Implementations of conversions between [`Date`] and other types.
6
7#[cfg(feature = "chrono")]
8use chrono::{Datelike, NaiveDate};
9#[cfg(feature = "jiff")]
10use jiff::civil;
11
12use super::Date;
13use crate::error::DateRangeError;
14
15impl From<Date> for u16 {
16    fn from(date: Date) -> Self {
17        date.to_raw()
18    }
19}
20
21impl From<Date> for time::Date {
22    /// Converts a `Date` to a [`time::Date`].
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// # use dos_date_time::{Date, time::macros::date};
28    /// #
29    /// assert_eq!(time::Date::from(Date::MIN), date!(1980-01-01));
30    /// assert_eq!(time::Date::from(Date::MAX), date!(2107-12-31));
31    /// ```
32    fn from(date: Date) -> Self {
33        let (year, month, day) = (date.year().into(), date.month(), date.day());
34        Self::from_calendar_date(year, month, day)
35            .expect("date should be in the range of `time::Date`")
36    }
37}
38
39#[cfg(feature = "chrono")]
40impl From<Date> for NaiveDate {
41    /// Converts a `Date` to a [`NaiveDate`].
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// # use dos_date_time::{Date, chrono::NaiveDate};
47    /// #
48    /// assert_eq!(
49    ///     NaiveDate::from(Date::MIN),
50    ///     "1980-01-01".parse::<NaiveDate>().unwrap()
51    /// );
52    /// assert_eq!(
53    ///     NaiveDate::from(Date::MAX),
54    ///     "2107-12-31".parse::<NaiveDate>().unwrap()
55    /// );
56    /// ```
57    fn from(date: Date) -> Self {
58        let (year, month, day) = (
59            date.year().into(),
60            u8::from(date.month()).into(),
61            date.day().into(),
62        );
63        Self::from_ymd_opt(year, month, day).expect("date should be in the range of `NaiveDate`")
64    }
65}
66
67#[cfg(feature = "jiff")]
68impl From<Date> for civil::Date {
69    /// Converts a `Date` to a [`civil::Date`].
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// # use dos_date_time::{Date, jiff::civil};
75    /// #
76    /// assert_eq!(civil::Date::from(Date::MIN), civil::date(1980, 1, 1));
77    /// assert_eq!(civil::Date::from(Date::MAX), civil::date(2107, 12, 31));
78    /// ```
79    fn from(date: Date) -> Self {
80        let (year, month, day) = (
81            date.year()
82                .try_into()
83                .expect("year should be in the range of `i16`"),
84            u8::from(date.month())
85                .try_into()
86                .expect("month should be in the range of `i8`"),
87            date.day()
88                .try_into()
89                .expect("day should be in the range of `i8`"),
90        );
91        civil::date(year, month, day)
92    }
93}
94
95impl TryFrom<time::Date> for Date {
96    type Error = DateRangeError;
97
98    /// Converts a [`time::Date`] to a `Date`.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`Err`] if `date` is out of range for the MS-DOS date.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// # use dos_date_time::{Date, time::macros::date};
108    /// #
109    /// assert_eq!(Date::try_from(date!(1980-01-01)), Ok(Date::MIN));
110    /// assert_eq!(Date::try_from(date!(2107-12-31)), Ok(Date::MAX));
111    ///
112    /// // Before `1980-01-01`.
113    /// assert!(Date::try_from(date!(1979-12-31)).is_err());
114    /// // After `2107-12-31`.
115    /// assert!(Date::try_from(date!(2108-01-01)).is_err());
116    /// ```
117    fn try_from(date: time::Date) -> Result<Self, Self::Error> {
118        Self::from_date(date)
119    }
120}
121
122#[cfg(feature = "chrono")]
123impl TryFrom<NaiveDate> for Date {
124    type Error = DateRangeError;
125
126    /// Converts a [`NaiveDate`] to a `Date`.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`Err`] if `date` is out of range for the MS-DOS date.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// # use dos_date_time::{Date, chrono::NaiveDate};
136    /// #
137    /// assert_eq!(
138    ///     Date::try_from("1980-01-01".parse::<NaiveDate>().unwrap()),
139    ///     Ok(Date::MIN)
140    /// );
141    /// assert_eq!(
142    ///     Date::try_from("2107-12-31".parse::<NaiveDate>().unwrap()),
143    ///     Ok(Date::MAX)
144    /// );
145    ///
146    /// // Before `1980-01-01`.
147    /// assert!(Date::try_from("1979-12-31".parse::<NaiveDate>().unwrap()).is_err());
148    /// // After `2107-12-31`.
149    /// assert!(Date::try_from("2108-01-01".parse::<NaiveDate>().unwrap()).is_err());
150    /// ```
151    fn try_from(date: NaiveDate) -> Result<Self, Self::Error> {
152        let (year, month, day) = (
153            date.year(),
154            u8::try_from(date.month())
155                .expect("month should be in the range of `u8`")
156                .try_into()
157                .expect("month should be in the range of `Month`"),
158            date.day()
159                .try_into()
160                .expect("day should be in the range of `u8`"),
161        );
162        let date = time::Date::from_calendar_date(year, month, day)
163            .expect("date should be in the range of `time::Date`");
164        Self::from_date(date)
165    }
166}
167
168#[cfg(feature = "jiff")]
169impl TryFrom<civil::Date> for Date {
170    type Error = DateRangeError;
171
172    /// Converts a [`civil::Date`] to a `Date`.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`Err`] if `date` is out of range for the MS-DOS date.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// # use dos_date_time::{Date, jiff::civil};
182    /// #
183    /// assert_eq!(Date::try_from(civil::date(1980, 1, 1)), Ok(Date::MIN));
184    /// assert_eq!(Date::try_from(civil::date(2107, 12, 31)), Ok(Date::MAX));
185    ///
186    /// // Before `1980-01-01`.
187    /// assert!(Date::try_from(civil::date(1979, 12, 31)).is_err());
188    /// // After `2107-12-31`.
189    /// assert!(Date::try_from(civil::date(2108, 1, 1)).is_err());
190    /// ```
191    fn try_from(date: civil::Date) -> Result<Self, Self::Error> {
192        let (year, month, day) = (
193            date.year().into(),
194            u8::try_from(date.month())
195                .expect("month should be in the range of `u8`")
196                .try_into()
197                .expect("month should be in the range of `Month`"),
198            date.day()
199                .try_into()
200                .expect("day should be in the range of `u8`"),
201        );
202        let date = time::Date::from_calendar_date(year, month, day)
203            .expect("date should be in the range of `time::Date`");
204        Self::from_date(date)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use time::macros::date;
211
212    use super::*;
213    use crate::error::DateRangeErrorKind;
214
215    #[test]
216    fn from_date_to_u16() {
217        assert_eq!(u16::from(Date::MIN), 0b0000_0000_0010_0001);
218        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
219        assert_eq!(
220            u16::from(Date::new(0b0010_1101_0111_1010).unwrap()),
221            0b0010_1101_0111_1010
222        );
223        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
224        assert_eq!(
225            u16::from(Date::new(0b0100_1101_0111_0001).unwrap()),
226            0b0100_1101_0111_0001
227        );
228        assert_eq!(u16::from(Date::MAX), 0b1111_1111_1001_1111);
229    }
230
231    #[test]
232    fn from_date_to_time_date() {
233        assert_eq!(time::Date::from(Date::MIN), date!(1980-01-01));
234        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
235        assert_eq!(
236            time::Date::from(Date::new(0b0010_1101_0111_1010).unwrap()),
237            date!(2002-11-26)
238        );
239        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
240        assert_eq!(
241            time::Date::from(Date::new(0b0100_1101_0111_0001).unwrap()),
242            date!(2018-11-17)
243        );
244        assert_eq!(time::Date::from(Date::MAX), date!(2107-12-31));
245    }
246
247    #[cfg(feature = "chrono")]
248    #[test]
249    fn from_date_to_chrono_naive_date() {
250        assert_eq!(
251            NaiveDate::from(Date::MIN),
252            "1980-01-01".parse::<NaiveDate>().unwrap()
253        );
254        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
255        assert_eq!(
256            NaiveDate::from(Date::new(0b0010_1101_0111_1010).unwrap()),
257            "2002-11-26".parse::<NaiveDate>().unwrap()
258        );
259        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
260        assert_eq!(
261            NaiveDate::from(Date::new(0b0100_1101_0111_0001).unwrap()),
262            "2018-11-17".parse::<NaiveDate>().unwrap()
263        );
264        assert_eq!(
265            NaiveDate::from(Date::MAX),
266            "2107-12-31".parse::<NaiveDate>().unwrap()
267        );
268    }
269
270    #[cfg(feature = "jiff")]
271    #[test]
272    fn from_date_to_jiff_civil_date() {
273        assert_eq!(civil::Date::from(Date::MIN), civil::date(1980, 1, 1));
274        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
275        assert_eq!(
276            civil::Date::from(Date::new(0b0010_1101_0111_1010).unwrap()),
277            civil::date(2002, 11, 26)
278        );
279        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
280        assert_eq!(
281            civil::Date::from(Date::new(0b0100_1101_0111_0001).unwrap()),
282            civil::date(2018, 11, 17)
283        );
284        assert_eq!(civil::Date::from(Date::MAX), civil::date(2107, 12, 31));
285    }
286
287    #[test]
288    fn try_from_time_date_to_date_before_dos_date_epoch() {
289        assert_eq!(
290            Date::try_from(date!(1979-12-31)).unwrap_err(),
291            DateRangeErrorKind::Negative.into()
292        );
293        assert_eq!(
294            Date::try_from(date!(1979-12-31)).unwrap_err(),
295            DateRangeErrorKind::Negative.into()
296        );
297    }
298
299    #[test]
300    fn try_from_time_date_to_date() {
301        assert_eq!(Date::try_from(date!(1980-01-01)).unwrap(), Date::MIN);
302        assert_eq!(Date::try_from(date!(1980-01-01)).unwrap(), Date::MIN);
303        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
304        assert_eq!(
305            Date::try_from(date!(2002-11-26)).unwrap(),
306            Date::new(0b0010_1101_0111_1010).unwrap()
307        );
308        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
309        assert_eq!(
310            Date::try_from(date!(2018-11-17)).unwrap(),
311            Date::new(0b0100_1101_0111_0001).unwrap()
312        );
313        assert_eq!(Date::try_from(date!(2107-12-31)).unwrap(), Date::MAX);
314        assert_eq!(Date::try_from(date!(2107-12-31)).unwrap(), Date::MAX);
315    }
316
317    #[test]
318    fn try_from_time_date_to_date_with_too_big_date() {
319        assert_eq!(
320            Date::try_from(date!(2108-01-01)).unwrap_err(),
321            DateRangeErrorKind::Overflow.into()
322        );
323    }
324
325    #[cfg(feature = "chrono")]
326    #[test]
327    fn try_from_chrono_naive_date_to_date_before_dos_date_epoch() {
328        assert_eq!(
329            Date::try_from("1979-12-31".parse::<NaiveDate>().unwrap()).unwrap_err(),
330            DateRangeErrorKind::Negative.into()
331        );
332        assert_eq!(
333            Date::try_from("1979-12-31".parse::<NaiveDate>().unwrap()).unwrap_err(),
334            DateRangeErrorKind::Negative.into()
335        );
336    }
337
338    #[cfg(feature = "chrono")]
339    #[test]
340    fn try_from_chrono_naive_date_to_date() {
341        assert_eq!(
342            Date::try_from("1980-01-01".parse::<NaiveDate>().unwrap()).unwrap(),
343            Date::MIN
344        );
345        assert_eq!(
346            Date::try_from("1980-01-01".parse::<NaiveDate>().unwrap()).unwrap(),
347            Date::MIN
348        );
349        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
350        assert_eq!(
351            Date::try_from("2002-11-26".parse::<NaiveDate>().unwrap()).unwrap(),
352            Date::new(0b0010_1101_0111_1010).unwrap()
353        );
354        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
355        assert_eq!(
356            Date::try_from("2018-11-17".parse::<NaiveDate>().unwrap()).unwrap(),
357            Date::new(0b0100_1101_0111_0001).unwrap()
358        );
359        assert_eq!(
360            Date::try_from("2107-12-31".parse::<NaiveDate>().unwrap()).unwrap(),
361            Date::MAX
362        );
363        assert_eq!(
364            Date::try_from("2107-12-31".parse::<NaiveDate>().unwrap()).unwrap(),
365            Date::MAX
366        );
367    }
368
369    #[cfg(feature = "chrono")]
370    #[test]
371    fn try_from_chrono_naive_date_to_date_with_too_big_date() {
372        assert_eq!(
373            Date::try_from("2108-01-01".parse::<NaiveDate>().unwrap()).unwrap_err(),
374            DateRangeErrorKind::Overflow.into()
375        );
376    }
377
378    #[cfg(feature = "jiff")]
379    #[test]
380    fn try_from_jiff_civil_date_to_date_before_dos_date_epoch() {
381        assert_eq!(
382            Date::try_from(civil::date(1979, 12, 31)).unwrap_err(),
383            DateRangeErrorKind::Negative.into()
384        );
385        assert_eq!(
386            Date::try_from(civil::date(1979, 12, 31)).unwrap_err(),
387            DateRangeErrorKind::Negative.into()
388        );
389    }
390
391    #[cfg(feature = "jiff")]
392    #[test]
393    fn try_from_jiff_civil_date_to_date() {
394        assert_eq!(Date::try_from(civil::date(1980, 1, 1)).unwrap(), Date::MIN);
395        assert_eq!(Date::try_from(civil::date(1980, 1, 1)).unwrap(), Date::MIN);
396        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
397        assert_eq!(
398            Date::try_from(civil::date(2002, 11, 26)).unwrap(),
399            Date::new(0b0010_1101_0111_1010).unwrap()
400        );
401        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
402        assert_eq!(
403            Date::try_from(civil::date(2018, 11, 17)).unwrap(),
404            Date::new(0b0100_1101_0111_0001).unwrap()
405        );
406        assert_eq!(
407            Date::try_from(civil::date(2107, 12, 31)).unwrap(),
408            Date::MAX
409        );
410        assert_eq!(
411            Date::try_from(civil::date(2107, 12, 31)).unwrap(),
412            Date::MAX
413        );
414    }
415
416    #[cfg(feature = "jiff")]
417    #[test]
418    fn try_from_jiff_civil_date_to_date_with_too_big_date() {
419        assert_eq!(
420            Date::try_from(civil::date(2108, 1, 1)).unwrap_err(),
421            DateRangeErrorKind::Overflow.into()
422        );
423    }
424}