rustversion_detect/
date.rs

1//! Contains a basic [`Date`] type used for differentiating nightly versions of rust.
2//!
3//! Intentionally ignores timezone information, making it simpler than the [`time` crate]
4//!
5//! [`time` crate]: https://github.com/time-rs/time
6
7use core::fmt::{self, Display};
8
9/// Indicates the date.
10///
11/// Used for the nightly versions of rust.
12///
13/// The timezone is not explicitly specified here,
14/// and matches whatever one the rust team uses for nightly releases.
15#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub struct Date {
17    /// The year
18    pub year: u16,
19    /// The month
20    pub month: u8,
21    /// The day of the month.
22    pub day: u8,
23}
24impl Date {
25    /// Create a date, using YYYY-MM-DD format (ISO 8601).
26    ///
27    /// For example, `Date::new(2018, 10, 21)`
28    ///
29    /// If possible, perform some basic validation.
30    ///
31    /// TODO: Don't make this a `const fn` function,
32    /// because doing validation is more important.
33    #[inline]
34    #[cfg_attr(has_track_caller, track_caller)]
35    pub const fn new(year: u16, month: u8, day: u8) -> Self {
36        #[cfg(has_const_panic)]
37        {
38            assert!(year >= 1, "Invalid year");
39            assert!(month >= 1 && month <= 12, "Invalid month");
40            assert!(day >= 1 && day <= 31, "Invalid day of month");
41        }
42        Date { year, month, day }
43    }
44
45    maybe_const_fn! {
46        #[cfg_const(has_const_match)]
47        /// Check if this date is equal to or after the specified start.
48        ///
49        /// Equivalent to `self >= start`,
50        /// but available as a `const` function.
51        ///
52        /// ## Example
53        /// ```
54        /// # use rustversion_detect::Date;;
55        /// assert!(Date::new(2024, 11, 16).is_since(Date::new(2024, 7, 28)));
56        /// ```
57        #[inline]
58        pub const fn is_since(&self, start: Date) -> bool {
59            self.year > start.year
60                || (self.year == start.year
61                    && (self.month > start.month
62                        || (self.month == start.month && self.day >= start.day)))
63        }
64
65        #[cfg_const(has_const_match)]
66        /// Check if this date is before the specified end.
67        ///
68        /// Equivalent to `self < end`,
69        /// but available as a `const` function.
70        ///
71        /// ## Example
72        /// ```
73        /// # use rustversion_detect::Date;
74        /// assert!(Date::new(2018, 12, 14).is_before(Date::new(2022, 8, 16)));
75        /// assert!(Date::new(2024, 11, 14).is_before(Date::new(2024, 12, 7)));
76        /// assert!(Date::new(2024, 11, 14).is_before(Date::new(2024, 11, 17)));
77        /// ```
78        #[inline]
79        pub const fn is_before(&self, end: Date) -> bool {
80            !self.is_since(end)
81        }
82    }
83}
84
85/// Declare a [`Date`] using the YYYY-MM-DD format (ISO 8601).
86///
87/// This is deprecated, because it cannot be implemented on Rust 1.31
88/// and the whole point of the crate is supporting old versions.
89#[macro_export]
90#[cfg(supports_macro_literal)]
91#[deprecated(note = "Cannot be supported on Rust 1.31")]
92macro_rules! date {
93    ($year:literal - $month:literal - $day:literal) => {{
94        // NOTE: The Date::new function sometimes perfroms validation
95        // It only validates if `const_panic` is stablized.
96        const DTE: $crate::date::Date = $crate::date::Date::new($year, $month, $day);
97        DTE
98    }};
99}
100
101/// Displays the date consistent with the ISO 8601 standard.
102impl Display for Date {
103    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
104        write!(
105            formatter,
106            "{:04}-{:02}-{:02}",
107            self.year, self.month, self.day,
108        )
109    }
110}
111
112#[cfg(test)]
113mod test {
114    use super::Date;
115
116    // (before, after)
117    fn test_dates() -> Vec<(Date, Date)> {
118        vec![
119            (Date::new(2018, 12, 14), Date::new(2022, 8, 16)),
120            (Date::new(2024, 11, 14), Date::new(2024, 12, 7)),
121            (Date::new(2024, 11, 14), Date::new(2024, 11, 17)),
122        ]
123    }
124
125    #[test]
126    fn test_before_after() {
127        for (before, after) in test_dates() {
128            assert!(before.is_before(after), "{} & {}", before, after);
129            assert!(after.is_since(before), "{} & {}", before, after);
130            // check equal dates
131            for &date in [before, after].iter() {
132                assert!(date.is_since(date), "{}", date);
133                assert!(!date.is_before(date), "{}", date);
134            }
135        }
136    }
137
138    #[test]
139    #[cfg_attr(has_const_panic, should_panic(expected = "Invalid year"))]
140    fn test_invalid_year() {
141        Date::new(0, 7, 18);
142    }
143
144    #[test]
145    #[cfg_attr(has_const_panic, should_panic(expected = "Invalid month"))]
146    fn test_invalid_month() {
147        Date::new(2014, 13, 18);
148    }
149
150    #[test]
151    #[cfg_attr(has_const_panic, should_panic(expected = "Invalid day of month"))]
152    fn test_invalid_date() {
153        Date::new(2014, 7, 36);
154    }
155}