rtc_hal/
datetime.rs

1//! # DateTime Module
2//!
3//! This module defines a `DateTime` struct and helper functions for representing,
4//! validating, and working with calendar date and time values in embedded systems.
5//!
6//! ## Features
7//! - Stores year, month, day, hour, minute, second
8//! - Built-in validation for all fields (including leap years and month lengths)
9//! - Setter and getter methods that enforce validity
10//! - Utility functions for leap year detection, days in a month, and weekday calculation
11//!
12//! ## Year Range
13//! The default supported range is **year >= 1970**, which covers the widest set of
14//! popular RTC chips. For example:
15//!
16//! - DS1307, DS3231: 2000-2099
17//!
18//! Drivers are responsible for checking and enforcing the *exact* year range of the
19//! underlying hardware. The `DateTime` type itself only enforces the lower bound (1970)
20//! to remain reusable in contexts outside RTCs.
21//!
22//! ## Weekday Format
23//! - This module uses **1=Sunday to 7=Saturday**
24//! - Drivers must handle conversion if required
25
26/// Errors that can occur when working with DateTime
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DateTimeError {
29    /// Invalid month value
30    InvalidMonth,
31    /// Invalid day value
32    InvalidDay,
33    /// Invalid hour value
34    InvalidHour,
35    /// Invalid minute value
36    InvalidMinute,
37    /// Invalid second value
38    InvalidSecond,
39    /// Invalid weekday value
40    InvalidWeekday,
41    /// Invalid Year value
42    InvalidYear,
43}
44
45/// Date and time representation used across RTC drivers.
46///
47/// This type represents calendar date and time in a general-purpose way,
48/// independent of any specific RTC hardware.
49///
50/// - Validates that `year >= 1970`
51/// - Other limits (e.g., 2000-2099) must be enforced by individual drivers
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct DateTime {
54    /// Year (full year, e.g., 2024)
55    year: u16,
56    /// Month (1-12)
57    month: u8,
58    /// Day of the month (1-31 depending on month/year)
59    day_of_month: u8,
60    /// Hour (0-23)
61    hour: u8,
62    /// Minute (0-59)
63    minute: u8,
64    /// Second (0-59)
65    second: u8,
66}
67
68impl DateTime {
69    /// Create a new `DateTime` instance with validation.
70    ///
71    /// # Errors
72    ///
73    /// Returns a `DateTimeError` if any component is out of valid range.
74    pub fn new(
75        year: u16,
76        month: u8,
77        day_of_month: u8,
78        hour: u8,
79        minute: u8,
80        second: u8,
81    ) -> Result<Self, DateTimeError> {
82        let dt = DateTime {
83            year,
84            month,
85            day_of_month,
86            hour,
87            minute,
88            second,
89        };
90        dt.validate()?;
91        Ok(dt)
92    }
93
94    /// Validate all datetime components.
95    ///
96    /// # Errors
97    ///
98    /// Returns the first `DateTimeError` encountered.
99    pub fn validate(&self) -> Result<(), DateTimeError> {
100        Self::validate_year(self.year)?;
101        Self::validate_month(self.month)?;
102        Self::validate_day(self.year, self.month, self.day_of_month)?;
103        Self::validate_hour(self.hour)?;
104        Self::validate_minute(self.minute)?;
105        Self::validate_second(self.second)?;
106        Ok(())
107    }
108
109    /// Validate the year (must be >= 1970).
110    fn validate_year(year: u16) -> Result<(), DateTimeError> {
111        if year < 1970 {
112            return Err(DateTimeError::InvalidYear);
113        }
114        Ok(())
115    }
116
117    /// Validate the month (must be 1-12).
118    fn validate_month(month: u8) -> Result<(), DateTimeError> {
119        if month == 0 || month > 12 {
120            return Err(DateTimeError::InvalidMonth);
121        }
122        Ok(())
123    }
124
125    /// Validate the day (must be within the valid range for the month/year).
126    fn validate_day(year: u16, month: u8, day: u8) -> Result<(), DateTimeError> {
127        let max_day = days_in_month(year, month);
128        if day == 0 || day > max_day {
129            return Err(DateTimeError::InvalidDay);
130        }
131        Ok(())
132    }
133
134    /// Validate the hour (must be 0-23).
135    fn validate_hour(hour: u8) -> Result<(), DateTimeError> {
136        if hour > 23 {
137            return Err(DateTimeError::InvalidHour);
138        }
139        Ok(())
140    }
141
142    /// Validate the minute (must be 0-59).
143    fn validate_minute(minute: u8) -> Result<(), DateTimeError> {
144        if minute > 59 {
145            return Err(DateTimeError::InvalidMinute);
146        }
147        Ok(())
148    }
149
150    /// Validate the second (must be 0-59).
151    fn validate_second(second: u8) -> Result<(), DateTimeError> {
152        if second > 59 {
153            return Err(DateTimeError::InvalidSecond);
154        }
155        Ok(())
156    }
157
158    /// Get the year (e.g. 2025).
159    pub fn year(&self) -> u16 {
160        self.year
161    }
162
163    /// Get the month number (1-12).
164    pub fn month(&self) -> u8 {
165        self.month
166    }
167
168    /// Get the day of the month (1-31).
169    pub fn day_of_month(&self) -> u8 {
170        self.day_of_month
171    }
172
173    /// Get the hour (0-23).
174    pub fn hour(&self) -> u8 {
175        self.hour
176    }
177
178    /// Get the minute (0-59).
179    pub fn minute(&self) -> u8 {
180        self.minute
181    }
182
183    /// Get the second (0-59).
184    pub fn second(&self) -> u8 {
185        self.second
186    }
187
188    /// Set year with validation.
189    ///
190    /// Re-validates the day in case of leap-year or February issues.
191    pub fn set_year(&mut self, year: u16) -> Result<(), DateTimeError> {
192        Self::validate_year(year)?;
193        Self::validate_day(year, self.month, self.day_of_month)?;
194        self.year = year;
195        Ok(())
196    }
197
198    /// Set month with validation.
199    ///
200    /// Re-validates the day in case month/day mismatch occurs.
201    pub fn set_month(&mut self, month: u8) -> Result<(), DateTimeError> {
202        Self::validate_month(month)?;
203        Self::validate_day(self.year, month, self.day_of_month)?;
204        self.month = month;
205        Ok(())
206    }
207
208    /// Set day with validation.
209    pub fn set_day_of_month(&mut self, day_of_month: u8) -> Result<(), DateTimeError> {
210        Self::validate_day(self.year, self.month, day_of_month)?;
211        self.day_of_month = day_of_month;
212        Ok(())
213    }
214
215    /// Set hour with validation.
216    pub fn set_hour(&mut self, hour: u8) -> Result<(), DateTimeError> {
217        Self::validate_hour(hour)?;
218        self.hour = hour;
219        Ok(())
220    }
221
222    /// Set minute with validation.
223    pub fn set_minute(&mut self, minute: u8) -> Result<(), DateTimeError> {
224        Self::validate_minute(minute)?;
225        self.minute = minute;
226        Ok(())
227    }
228
229    /// Set second with validation.
230    pub fn set_second(&mut self, second: u8) -> Result<(), DateTimeError> {
231        Self::validate_second(second)?;
232        self.second = second;
233        Ok(())
234    }
235
236    /// Calculate weekday for this DateTime
237    pub fn calculate_weekday(&self) -> Result<Weekday, DateTimeError> {
238        calculate_weekday(self.year, self.month, self.day_of_month)
239    }
240}
241
242/// Day of the week (1 = Sunday .. 7 = Saturday)
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244#[repr(u8)]
245pub enum Weekday {
246    /// Sunday starts with 1
247    Sunday = 1,
248    /// Monday
249    Monday = 2,
250    /// Tuesday
251    Tuesday = 3,
252    /// Wednesday
253    Wednesday = 4,
254    /// Thursday
255    Thursday = 5,
256    /// Friday
257    Friday = 6,
258    /// Saturday
259    Saturday = 7,
260}
261
262impl Weekday {
263    /// Create a Weekday from a raw u8 (1 = Sunday .. 7 = Saturday).
264    pub fn from_number(n: u8) -> Result<Self, DateTimeError> {
265        match n {
266            1 => Ok(Self::Sunday),
267            2 => Ok(Self::Monday),
268            3 => Ok(Self::Tuesday),
269            4 => Ok(Self::Wednesday),
270            5 => Ok(Self::Thursday),
271            6 => Ok(Self::Friday),
272            7 => Ok(Self::Saturday),
273            _ => Err(DateTimeError::InvalidWeekday),
274        }
275    }
276
277    /// Get the number form (1 = Sunday .. 7 = Saturday).
278    pub fn to_number(self) -> u8 {
279        self as u8
280    }
281
282    /// Get the weekday name as a string slice
283    pub fn as_str(&self) -> &'static str {
284        match self {
285            Weekday::Sunday => "Sunday",
286            Weekday::Monday => "Monday",
287            Weekday::Tuesday => "Tuesday",
288            Weekday::Wednesday => "Wednesday",
289            Weekday::Thursday => "Thursday",
290            Weekday::Friday => "Friday",
291            Weekday::Saturday => "Saturday",
292        }
293    }
294}
295
296/// Check if a year is a leap year
297pub fn is_leap_year(year: u16) -> bool {
298    (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
299}
300
301/// Get the number of days in a month
302pub fn days_in_month(year: u16, month: u8) -> u8 {
303    match month {
304        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
305        4 | 6 | 9 | 11 => 30,
306        2 => {
307            if is_leap_year(year) {
308                29
309            } else {
310                28
311            }
312        }
313        _ => 0,
314    }
315}
316
317/// Calculate the day of the week using Zeller's congruence algorithm
318/// Returns 1=Sunday, 2=Monday, ..., 7=Saturday
319pub fn calculate_weekday(year: u16, month: u8, day_of_month: u8) -> Result<Weekday, DateTimeError> {
320    let (year, month) = if month < 3 {
321        (year - 1, month + 12)
322    } else {
323        (year, month)
324    };
325
326    let k = year % 100;
327    let j = year / 100;
328
329    let h =
330        (day_of_month as u16 + ((13 * (month as u16 + 1)) / 5) + k + (k / 4) + (j / 4) - 2 * j) % 7;
331
332    // Convert Zeller's result (0=Saturday) to our format (1=Sunday)
333    let weekday_num = ((h + 6) % 7) + 1;
334
335    // This should never fail since we're calculating a valid weekday
336    Weekday::from_number(weekday_num as u8)
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_valid_datetime_creation() {
345        let dt = DateTime::new(2024, 3, 15, 14, 30, 45).unwrap();
346        assert_eq!(dt.year(), 2024);
347        assert_eq!(dt.month(), 3);
348        assert_eq!(dt.day_of_month(), 15);
349        assert_eq!(dt.hour(), 14);
350        assert_eq!(dt.minute(), 30);
351        assert_eq!(dt.second(), 45);
352    }
353
354    #[test]
355    fn test_invalid_year() {
356        let result = DateTime::new(1969, 1, 1, 0, 0, 0);
357        assert_eq!(result.unwrap_err(), DateTimeError::InvalidYear);
358    }
359
360    #[test]
361    fn test_invalid_month() {
362        assert_eq!(
363            DateTime::new(2024, 0, 1, 0, 0, 0).unwrap_err(),
364            DateTimeError::InvalidMonth
365        );
366        assert_eq!(
367            DateTime::new(2024, 13, 1, 0, 0, 0).unwrap_err(),
368            DateTimeError::InvalidMonth
369        );
370    }
371
372    #[test]
373    fn test_invalid_day() {
374        // Test February 30th (invalid)
375        assert_eq!(
376            DateTime::new(2024, 2, 30, 0, 0, 0).unwrap_err(),
377            DateTimeError::InvalidDay
378        );
379
380        // Test day 0
381        assert_eq!(
382            DateTime::new(2024, 1, 0, 0, 0, 0).unwrap_err(),
383            DateTimeError::InvalidDay
384        );
385
386        // Test April 31st (invalid - April has 30 days)
387        assert_eq!(
388            DateTime::new(2024, 4, 31, 0, 0, 0).unwrap_err(),
389            DateTimeError::InvalidDay
390        );
391    }
392
393    #[test]
394    fn test_invalid_hour() {
395        assert_eq!(
396            DateTime::new(2024, 1, 1, 24, 0, 0).unwrap_err(),
397            DateTimeError::InvalidHour
398        );
399    }
400
401    #[test]
402    fn test_invalid_minute() {
403        assert_eq!(
404            DateTime::new(2024, 1, 1, 0, 60, 0).unwrap_err(),
405            DateTimeError::InvalidMinute
406        );
407    }
408
409    #[test]
410    fn test_invalid_second() {
411        assert_eq!(
412            DateTime::new(2024, 1, 1, 0, 0, 60).unwrap_err(),
413            DateTimeError::InvalidSecond
414        );
415    }
416
417    #[test]
418    fn test_leap_year_february_29() {
419        // 2024 is a leap year - February 29th should be valid
420        assert!(DateTime::new(2024, 2, 29, 0, 0, 0).is_ok());
421
422        // 2023 is not a leap year - February 29th should be invalid
423        assert_eq!(
424            DateTime::new(2023, 2, 29, 0, 0, 0).unwrap_err(),
425            DateTimeError::InvalidDay
426        );
427    }
428
429    #[test]
430    fn test_setters_with_validation() {
431        let mut dt = DateTime::new(2024, 1, 1, 0, 0, 0).unwrap();
432
433        // Valid operations
434        assert!(dt.set_year(2025).is_ok());
435        assert_eq!(dt.year(), 2025);
436
437        assert!(dt.set_month(12).is_ok());
438        assert_eq!(dt.month(), 12);
439
440        assert!(dt.set_hour(23).is_ok());
441        assert_eq!(dt.hour(), 23);
442
443        // Invalid operations
444        assert_eq!(dt.set_year(1969), Err(DateTimeError::InvalidYear));
445        assert_eq!(dt.set_month(13), Err(DateTimeError::InvalidMonth));
446        assert_eq!(dt.set_hour(24), Err(DateTimeError::InvalidHour));
447    }
448
449    #[test]
450    fn test_leap_year_edge_cases_in_setters() {
451        let mut dt = DateTime::new(2024, 2, 29, 0, 0, 0).unwrap(); // Leap year
452
453        // Changing to non-leap year should fail because Feb 29 becomes invalid
454        assert_eq!(dt.set_year(2023), Err(DateTimeError::InvalidDay));
455
456        // Original value should remain unchanged after failed operation
457        assert_eq!(dt.year(), 2024);
458        assert_eq!(dt.day_of_month(), 29);
459    }
460
461    #[test]
462    fn test_month_day_validation_in_setters() {
463        let mut dt = DateTime::new(2024, 1, 31, 0, 0, 0).unwrap(); // January 31st
464
465        // Changing to February should fail because Feb doesn't have 31 days
466        assert_eq!(dt.set_month(2), Err(DateTimeError::InvalidDay));
467
468        // Original value should remain unchanged
469        assert_eq!(dt.month(), 1);
470        assert_eq!(dt.day_of_month(), 31);
471
472        // But changing to March should work (March has 31 days)
473        assert!(dt.set_month(3).is_ok());
474        assert_eq!(dt.month(), 3);
475    }
476
477    #[test]
478    fn test_weekday_calculation() {
479        let dt = DateTime::new(2024, 1, 1, 0, 0, 0).unwrap(); // New Year 2024
480        let weekday = dt.calculate_weekday().unwrap();
481        assert_eq!(weekday, Weekday::Monday); // January 1, 2024 was a Monday
482
483        let dt = DateTime::new(2024, 12, 25, 0, 0, 0).unwrap();
484        let weekday = dt.calculate_weekday().unwrap();
485        assert_eq!(weekday, Weekday::Wednesday); // December 25, 2024 is a Wednesday
486    }
487
488    #[test]
489    fn test_weekday_from_number() {
490        assert_eq!(Weekday::from_number(1).unwrap(), Weekday::Sunday);
491        assert_eq!(Weekday::from_number(2).unwrap(), Weekday::Monday);
492        assert_eq!(Weekday::from_number(7).unwrap(), Weekday::Saturday);
493
494        assert_eq!(
495            Weekday::from_number(0).unwrap_err(),
496            DateTimeError::InvalidWeekday
497        );
498        assert_eq!(
499            Weekday::from_number(8).unwrap_err(),
500            DateTimeError::InvalidWeekday
501        );
502    }
503
504    #[test]
505    fn test_weekday_to_number() {
506        assert_eq!(Weekday::Sunday.to_number(), 1);
507        assert_eq!(Weekday::Monday.to_number(), 2);
508        assert_eq!(Weekday::Saturday.to_number(), 7);
509    }
510
511    #[test]
512    fn test_weekday_as_str() {
513        assert_eq!(Weekday::Sunday.as_str(), "Sunday");
514        assert_eq!(Weekday::Monday.as_str(), "Monday");
515        assert_eq!(Weekday::Tuesday.as_str(), "Tuesday");
516        assert_eq!(Weekday::Wednesday.as_str(), "Wednesday");
517        assert_eq!(Weekday::Thursday.as_str(), "Thursday");
518        assert_eq!(Weekday::Friday.as_str(), "Friday");
519        assert_eq!(Weekday::Saturday.as_str(), "Saturday");
520    }
521
522    #[test]
523    fn test_calculate_weekday_known_dates() {
524        // Test some known dates
525        assert_eq!(calculate_weekday(2000, 1, 1).unwrap(), Weekday::Saturday);
526        assert_eq!(calculate_weekday(2024, 1, 1).unwrap(), Weekday::Monday);
527        assert_eq!(calculate_weekday(2025, 8, 15).unwrap(), Weekday::Friday);
528
529        // Test leap year boundary
530        assert_eq!(calculate_weekday(2024, 2, 29).unwrap(), Weekday::Thursday); // Leap day 2024
531    }
532
533    #[test]
534    fn test_is_leap_year() {
535        // Regular leap years (divisible by 4)
536        assert!(is_leap_year(2024));
537        assert!(is_leap_year(2020));
538        assert!(is_leap_year(1996));
539
540        // Non-leap years
541        assert!(!is_leap_year(2023));
542        assert!(!is_leap_year(2021));
543        assert!(!is_leap_year(1999));
544
545        // Century years (divisible by 100 but not 400)
546        assert!(!is_leap_year(1900));
547        assert!(!is_leap_year(2100));
548
549        // Century years divisible by 400
550        assert!(is_leap_year(2000));
551        assert!(is_leap_year(1600));
552    }
553
554    #[test]
555    fn test_days_in_month() {
556        // January (31 days)
557        assert_eq!(days_in_month(2024, 1), 31);
558
559        // February leap year (29 days)
560        assert_eq!(days_in_month(2024, 2), 29);
561
562        // February non-leap year (28 days)
563        assert_eq!(days_in_month(2023, 2), 28);
564
565        // April (30 days)
566        assert_eq!(days_in_month(2024, 4), 30);
567
568        // December (31 days)
569        assert_eq!(days_in_month(2024, 12), 31);
570
571        // Invalid month
572        assert_eq!(days_in_month(2024, 13), 0);
573        assert_eq!(days_in_month(2024, 0), 0);
574    }
575
576    #[test]
577    fn test_setter_interdependency_edge_cases() {
578        // January 31 → February (invalid because Feb max is 28/29)
579        let mut dt = DateTime::new(2023, 1, 31, 0, 0, 0).unwrap();
580        assert_eq!(dt.set_month(2), Err(DateTimeError::InvalidDay));
581
582        // March 31 → April (invalid because April max is 30)
583        let mut dt = DateTime::new(2023, 3, 31, 0, 0, 0).unwrap();
584        assert_eq!(dt.set_month(4), Err(DateTimeError::InvalidDay));
585
586        // Leap year Feb 29 → non-leap year
587        let mut dt = DateTime::new(2024, 2, 29, 0, 0, 0).unwrap();
588        assert_eq!(dt.set_year(2023), Err(DateTimeError::InvalidDay));
589
590        // Non-leap year Feb 28 → leap year (should work)
591        let mut dt = DateTime::new(2023, 2, 28, 0, 0, 0).unwrap();
592        assert!(dt.set_year(2024).is_ok());
593    }
594}