1use bacnet_types::primitives::{Date, Time};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct ClockFrame {
8 pub local_date: Date,
10 pub local_time: Time,
12 pub utc_offset: i16,
14 pub daylight_savings_status: bool,
16}
17
18impl ClockFrame {
19 pub fn day_of_week_bit(self) -> Option<u8> {
22 (1..=7)
23 .contains(&self.local_date.day_of_week)
24 .then(|| 1 << (self.local_date.day_of_week - 1))
25 }
26
27 pub fn is_valid_actual_datetime(self) -> bool {
30 let Some(year) = self.local_date.actual_year() else {
31 return false;
32 };
33 if !(1..=12).contains(&self.local_date.month)
34 || !(1..=7).contains(&self.local_date.day_of_week)
35 {
36 return false;
37 }
38
39 let max_day = days_in_month(year, self.local_date.month);
40 if self.local_date.day == 0 || self.local_date.day > max_day {
41 return false;
42 }
43
44 let days = days_from_civil(
45 i64::from(year),
46 i64::from(self.local_date.month),
47 i64::from(self.local_date.day),
48 );
49 let expected_day_of_week = (days + 3).rem_euclid(7) as u8 + 1;
50 if self.local_date.day_of_week != expected_day_of_week {
51 return false;
52 }
53
54 self.local_time.hour <= 23
55 && self.local_time.minute <= 59
56 && self.local_time.second <= 59
57 && self.local_time.hundredths <= 99
58 }
59}
60
61fn days_in_month(year: u16, month: u8) -> u8 {
62 match month {
63 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
64 4 | 6 | 9 | 11 => 30,
65 2 if is_leap_year(year) => 29,
66 2 => 28,
67 _ => 0,
68 }
69}
70
71fn is_leap_year(year: u16) -> bool {
72 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
73}
74
75fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
76 let year = year - i64::from(month <= 2);
77 let era = year.div_euclid(400);
78 let year_of_era = year - era * 400;
79 let month_prime = month + if month > 2 { -3 } else { 9 };
80 let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
81 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
82 era * 146_097 + day_of_era - 719_468
83}
84
85pub trait ClockReader: Send + Sync {
90 fn read_clock(&self) -> Option<ClockFrame>;
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn leap_day_frame() -> ClockFrame {
99 ClockFrame {
100 local_date: Date {
101 year: 124,
102 month: 2,
103 day: 29,
104 day_of_week: 4,
105 },
106 local_time: Time {
107 hour: 23,
108 minute: 59,
109 second: 59,
110 hundredths: 99,
111 },
112 utc_offset: 0,
113 daylight_savings_status: false,
114 }
115 }
116
117 #[test]
118 fn actual_datetime_validation_checks_calendar_weekday_and_time() {
119 let valid = leap_day_frame();
120 assert!(valid.is_valid_actual_datetime());
121
122 for invalid in [
123 ClockFrame {
124 local_date: Date {
125 day: 30,
126 ..valid.local_date
127 },
128 ..valid
129 },
130 ClockFrame {
131 local_date: Date {
132 day_of_week: 5,
133 ..valid.local_date
134 },
135 ..valid
136 },
137 ClockFrame {
138 local_time: Time {
139 hour: Time::UNSPECIFIED,
140 ..valid.local_time
141 },
142 ..valid
143 },
144 ] {
145 assert!(!invalid.is_valid_actual_datetime());
146 }
147 }
148}