use std::fmt;
use chrono::{Datelike, Duration, NaiveDate, Weekday};
use time::OffsetDateTime;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const REFERENCE_DATE: NaiveDate =
NaiveDate::from_ymd_opt(2019, 1, 3).expect("2019-01-03 should be a valid date");
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CycleValidity {
Valid,
Expired,
Future,
}
impl fmt::Display for CycleValidity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Valid => write!(f, "valid"),
Self::Expired => write!(f, "expired"),
Self::Future => write!(f, "future"),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AiracCycle {
year: u8,
cycle: u8,
}
impl AiracCycle {
pub fn new(year: u8, cycle: u8) -> Self {
Self { year, cycle }
}
pub fn year(&self) -> u8 {
self.year
}
pub fn cycle(&self) -> u8 {
self.cycle
}
pub fn effective_date(&self) -> Option<NaiveDate> {
let year = self.year as u16 + 2000u16; let first_thu = NaiveDate::from_weekday_of_month_opt(year as i32, 1, Weekday::Thu, 1)
.expect("the year should be before before 262143 CE");
let days_since_ref = (first_thu - REFERENCE_DATE).num_days();
let cycle_offset = days_since_ref % 28;
let first_airac_of_year = if cycle_offset == 0 {
first_thu
} else {
first_thu + Duration::days(28 - cycle_offset)
};
let target_date = first_airac_of_year + Duration::days(28 * (self.cycle - 1) as i64);
if target_date.year() as u16 == year
|| (self.cycle == 1 && target_date.year() as u16 == year - 1)
{
Some(target_date)
} else {
None
}
}
pub fn end_date(&self) -> Option<NaiveDate> {
self.effective_date()
.map(|start| start + Duration::days(27)) }
pub fn valid_for_date(&self, date: NaiveDate) -> Option<CycleValidity> {
let start_date = self.effective_date()?;
let end_date = self.end_date()?;
if date < start_date {
Some(CycleValidity::Future)
} else if date > end_date {
Some(CycleValidity::Expired)
} else {
Some(CycleValidity::Valid)
}
}
pub fn now_valid(&self) -> Option<CycleValidity> {
let now = OffsetDateTime::now_utc().date();
let y = now.year();
let m: u8 = now.month().into();
let d = now.day();
let date =
NaiveDate::from_ymd_opt(y, m as u32, d as u32).expect("now should be a valid date");
self.valid_for_date(date)
}
}
impl fmt::Display for AiracCycle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}{:02}", self.year, self.cycle)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effective_dates() {
let cycle = AiracCycle::new(25, 9);
assert_eq!(cycle.effective_date(), NaiveDate::from_ymd_opt(2025, 09, 4));
assert_eq!(cycle.end_date(), NaiveDate::from_ymd_opt(2025, 10, 1));
}
#[test]
fn test_cycle_validity() {
let cycle = AiracCycle::new(25, 9);
let mid_date =
NaiveDate::from_ymd_opt(2025, 9, 18).expect("2025-09-08 should be a valid date");
assert_eq!(cycle.valid_for_date(mid_date), Some(CycleValidity::Valid));
let outdated_date =
NaiveDate::from_ymd_opt(2025, 10, 18).expect("2025-10-08 should be a valid date");
assert_eq!(
cycle.valid_for_date(outdated_date),
Some(CycleValidity::Expired)
);
}
#[test]
fn test_identifier_format() {
let cycle = AiracCycle::new(25, 9);
assert_eq!(cycle.to_string(), "2509");
}
}