Skip to main content

embedded_date_time/
date.rs

1use core::fmt;
2
3use crate::DateTime;
4use crate::Time;
5use crate::Weekday;
6
7/// A simple date in the Gregorian calendar without time zone information.
8#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)]
9pub struct Date {
10    /// The year as commonly used in a date.
11    ///
12    /// The range of 2000 to 2099 is guaranteed to be supported by all methods.
13    /// For support beyond this range, consult the documentation of the respective implementation.
14    pub year: u16,
15
16    /// Month of the year [1-12].
17    pub month: u8,
18
19    /// Day of the month [1-31].
20    pub day: u8,
21}
22
23impl Date {
24    /// Create a new Date from year, month and day.
25    ///
26    /// No checks will be performed to validate the date.
27    #[must_use]
28    pub fn new(year: u16, month: u8, day: u8) -> Self {
29        Self { year, month, day }
30    }
31
32    /// Create a new Date from year, month and day.
33    ///
34    /// Returns None if the year, month or day is invalid.
35    ///
36    /// The year must be between 2000 and 2199.
37    /// This range might be extended in the future if the implementation supports it.
38    #[must_use]
39    pub fn new_checked(year: u16, month: u8, day: u8) -> Option<Self> {
40        let result = Self::new(year, month, day);
41        result.is_valid().then_some(result)
42    }
43
44    /// Determine whether the date is a valid combination of year, month and day.
45    ///
46    /// This is guaranteed to work for the years between 2000 and 2199, inclusive.
47    ///
48    /// Years outside of this range will return arbitrary results or may panic.
49    #[must_use]
50    pub fn is_valid(self) -> bool {
51        if !(2000..=2199).contains(&self.year) {
52            return false;
53        }
54
55        let days_in_month = match self.month {
56            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
57            4 | 6 | 9 | 11 => 30,
58            2 => 28 + u8::from(self.is_leap_year()),
59            _ => return false,
60        };
61
62        (1..=days_in_month).contains(&self.day)
63    }
64
65    /// Helper function to determine whether a given year is a leap year.
66    ///
67    /// This is guaranteed to work for the years between 2000 and 2199.
68    ///
69    /// Years outside of this range will return arbitrary results or may panic.
70    #[must_use]
71    pub fn is_leap_year(self) -> bool {
72        let year2000 = self.year2000();
73        // check if the year is divisible by 4 but make an exception for 2100
74        year2000.trailing_zeros() >= 2 && year2000 != 100
75    }
76
77    /// Create a new `DateTime` from a `Date` and a `Time`.
78    ///
79    /// Returns None if the hours, minutes or seconds is invalid.
80    #[must_use]
81    pub fn with_time(self, time: Time) -> DateTime {
82        DateTime { date: self, time }
83    }
84
85    /// Returns the year relative to 2000.
86    ///
87    /// This is guaranteed to work for the years between 2000 and 2255, inclusive.
88    ///
89    /// The purpose of this method is to obtain a year with reduced width for cheaper calculations.
90    /// Subtracting 2000 preserves the leap year pattern.
91    ///
92    /// Years outside of this range will return arbitrary results or may panic.
93    #[must_use]
94    #[expect(
95        clippy::cast_possible_truncation,
96        reason = "the limitation is documented"
97    )]
98    pub fn year2000(self) -> u8 {
99        (self.year - 2000) as u8
100    }
101
102    /// Returns the weekday of the date.
103    ///
104    /// This is guaranteed to work for all valid dates within the years 2000 to 2171.
105    ///
106    /// Invalid dates will return arbitrary results or may panic.
107    #[must_use]
108    pub fn weekday(self) -> Weekday {
109        // number of week-day shifts for each month of a non-leap year
110        // To keep this number low, we use modulo 7 at compile time which won't affect the result.
111        let months = [
112            0_u8, // enables 1-based indexing of months
113            0,    // January
114            31 % 7,
115            59 % 7,
116            90 % 7,
117            120 % 7,
118            151 % 7,
119            181 % 7,
120            212 % 7,
121            243 % 7,
122            (273 % 7) as u8,
123            (304 % 7) as u8,
124            (334 % 7) as u8, // December
125        ];
126
127        // number of week-day shifts for the year
128        // one extra-shift for each leap year (with a correction for the skipped year 2100)
129        let year2000 = self.year2000();
130        let year_ordinal = year2000 + year2000.div_ceil(4) - u8::from(year2000 > 100);
131
132        // insert Feb. 29 for leap years
133        let leap_ordinal = u8::from(self.month > 2 && self.is_leap_year());
134
135        // number of week-day shifts for the month
136        let month_ordinal = *months.get(usize::from(self.month)).unwrap_or(&0);
137
138        // number of week-day shifts for the day
139        let day_ordinal = self.day;
140
141        // total number of week-day shifts
142        // This will overflow past the year 2171; extend to u16 if this becomes a real limitation.
143        let ordinal_date = year_ordinal + leap_ordinal + month_ordinal + day_ordinal;
144
145        // this is the only real division (modulo 7)
146        // the offset has been chosen to make the result match the ordinal value of the Weekday-enum
147        Weekday::from((ordinal_date + 5) % 7)
148    }
149}
150
151impl fmt::Debug for Date {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        fmt::Display::fmt(&self, f)
154    }
155}
156
157impl fmt::Display for Date {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
160    }
161}
162
163#[cfg(feature = "defmt")]
164impl defmt::Format for Date {
165    fn format(&self, fmt: defmt::Formatter<'_>) {
166        defmt::write!(fmt, "{:04}-{:02}-{:02}", self.year, self.month, self.day);
167    }
168}
169
170#[cfg(feature = "ufmt")]
171impl ufmt::uDebug for Date {
172    fn fmt<W>(&self, fmt: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
173    where
174        W: ufmt::uWrite + ?Sized,
175    {
176        ufmt::uDisplay::fmt(&self, fmt)
177    }
178}
179
180#[cfg(feature = "ufmt")]
181impl ufmt::uDisplay for Date {
182    fn fmt<W>(&self, fmt: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
183    where
184        W: ufmt::uWrite + ?Sized,
185    {
186        use ufmt::uwrite;
187
188        let Self { year, month, day } = *self;
189
190        uwrite!(fmt, "{}-", year)?;
191
192        if month < 10 {
193            uwrite!(fmt, "0{}-", month)?;
194        } else {
195            uwrite!(fmt, "{}-", month)?;
196        }
197
198        if day < 10 {
199            uwrite!(fmt, "0{}", day)?;
200        } else {
201            uwrite!(fmt, "{}", day)?;
202        }
203
204        Ok(())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    #![expect(clippy::panic, reason = "this is a test")]
211
212    #[test]
213    fn test_validation() {
214        // iterate through all valid (and some invalid) dates and test whether `chrono` and
215        // `embedded-date-time` agree on the validation.
216        for year in 2000_u16..=2199 {
217            for month in 0_u8..=13 {
218                for day in 0_u8..=32 {
219                    let embedded_date = crate::Date::new_checked(year, month, day);
220                    let chrono_date = chrono::NaiveDate::from_ymd_opt(
221                        i32::from(year),
222                        u32::from(month),
223                        u32::from(day),
224                    );
225
226                    match (embedded_date, chrono_date) {
227                        (None, Some(date)) => {
228                            panic!("chrono validated but embedded didn't: {date}")
229                        }
230                        (Some(date), None) => {
231                            panic!("embedded validated but chrono didn't: {date:?}")
232                        }
233                        (None, None) | (Some(_), Some(_)) => {}
234                    }
235                }
236            }
237        }
238    }
239}