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