use crate::constructed::{BACnetCalendarEntry, BACnetDateRange, BACnetWeekNDay};
use crate::primitives::{Date, Time};
const ANY: u8 = 0xFF;
pub fn is_leap_year(year: u16) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
pub fn days_in_month(year: u16, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SpecificDate {
year: u16,
month: u8,
day: u8,
}
impl SpecificDate {
pub fn new(year: u16, month: u8, day: u8) -> Option<Self> {
let in_range =
(1900..=2154).contains(&year) && (1..=days_in_month(year, month)).contains(&day);
in_range.then_some(Self { year, month, day })
}
pub fn from_date(date: &Date) -> Option<Self> {
if date.year == ANY {
return None;
}
Self::new(1900 + u16::from(date.year), date.month, date.day)
}
pub fn year(self) -> u16 {
self.year
}
pub fn month(self) -> u8 {
self.month
}
pub fn day(self) -> u8 {
self.day
}
pub fn days_in_month(self) -> u8 {
days_in_month(self.year, self.month)
}
pub fn weekday(self) -> u8 {
(self.days_since_1970() + 3).rem_euclid(7) as u8 + 1
}
pub fn days_since_1970(self) -> i64 {
let year = i64::from(self.year) - i64::from(self.month <= 2);
let era = year.div_euclid(400);
let year_of_era = year - era * 400;
let month = i64::from(self.month);
let month_prime = month + if month > 2 { -3 } else { 9 };
let day_of_year = (153 * month_prime + 2) / 5 + i64::from(self.day) - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
pub fn to_date(self) -> Date {
Date {
year: (self.year - 1900) as u8,
month: self.month,
day: self.day,
day_of_week: self.weekday(),
}
}
}
fn month_matches(octet: u8, month: u8) -> bool {
match octet {
ANY => true,
13 => !month.is_multiple_of(2),
14 => month.is_multiple_of(2),
specific => specific == month,
}
}
fn weekday_matches(octet: u8, day: SpecificDate) -> bool {
octet == ANY || octet == day.weekday()
}
fn valid_weekday(octet: u8) -> bool {
octet == ANY || (1..=7).contains(&octet)
}
fn valid_month(octet: u8) -> bool {
octet == ANY || (1..=14).contains(&octet)
}
impl Date {
pub fn is_unspecified(&self) -> bool {
self.encode() == [ANY; 4]
}
pub fn is_valid_pattern(&self) -> bool {
valid_month(self.month)
&& (self.day == ANY || (1..=34).contains(&self.day))
&& valid_weekday(self.day_of_week)
}
pub fn matches(&self, day: SpecificDate) -> bool {
let year = self.year == ANY || 1900 + u16::from(self.year) == day.year();
let day_of_month = match self.day {
ANY => true,
32 => day.day() == day.days_in_month(),
33 => !day.day().is_multiple_of(2),
34 => day.day().is_multiple_of(2),
specific => specific == day.day(),
};
year && month_matches(self.month, day.month())
&& day_of_month
&& weekday_matches(self.day_of_week, day)
}
}
enum Endpoint {
Open,
At(SpecificDate),
Invalid,
}
impl Endpoint {
fn of(date: &Date) -> Self {
if date.is_unspecified() {
return Self::Open;
}
match SpecificDate::from_date(date) {
Some(day) if valid_weekday(date.day_of_week) => Self::At(day),
_ => Self::Invalid,
}
}
}
impl BACnetDateRange {
pub fn is_valid(&self) -> bool {
!matches!(Endpoint::of(&self.start_date), Endpoint::Invalid)
&& !matches!(Endpoint::of(&self.end_date), Endpoint::Invalid)
}
pub fn contains(&self, day: SpecificDate) -> bool {
let after_start = match Endpoint::of(&self.start_date) {
Endpoint::Open => true,
Endpoint::At(start) => start <= day,
Endpoint::Invalid => return false,
};
let before_end = match Endpoint::of(&self.end_date) {
Endpoint::Open => true,
Endpoint::At(end) => day <= end,
Endpoint::Invalid => return false,
};
after_start && before_end
}
}
impl BACnetWeekNDay {
pub fn is_valid(&self) -> bool {
valid_month(self.month)
&& (self.week_of_month == ANY || (1..=9).contains(&self.week_of_month))
&& valid_weekday(self.day_of_week)
}
pub fn matches(&self, day: SpecificDate) -> bool {
let date = day.day();
let last = day.days_in_month();
let week = match self.week_of_month {
ANY => true,
week @ 1..=5 => (date - 1) / 7 + 1 == week,
week @ 6..=9 => {
let end = i16::from(last) - i16::from(week - 6) * 7;
(end - 6..=end).contains(&i16::from(date))
}
_ => false,
};
month_matches(self.month, day.month()) && week && weekday_matches(self.day_of_week, day)
}
}
impl BACnetCalendarEntry {
pub fn is_valid(&self) -> bool {
match self {
Self::Date(date) => date.is_valid_pattern(),
Self::DateRange(range) => range.is_valid(),
Self::WeekNDay(week_n_day) => week_n_day.is_valid(),
}
}
pub fn matches(&self, day: SpecificDate) -> bool {
match self {
Self::Date(date) => date.matches(day),
Self::DateRange(range) => range.contains(day),
Self::WeekNDay(week_n_day) => week_n_day.matches(day),
}
}
}
impl Time {
pub fn is_specific(&self) -> bool {
self.hour <= 23 && self.minute <= 59 && self.second <= 59 && self.hundredths <= 99
}
}
#[cfg(test)]
mod tests;