use crate::error::{PandRSError, Result};
use crate::temporal::{Frequency, Temporal};
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Utc};
#[derive(Debug, Clone)]
pub struct DateRange<T: Temporal> {
start: T,
end: T,
freq: Frequency,
inclusive: bool,
}
impl<T: Temporal> DateRange<T> {
pub fn new(start: T, end: T, freq: Frequency, inclusive: bool) -> Result<Self> {
if start > end {
return Err(PandRSError::Consistency(
"Start date must be earlier than end date".to_string(),
));
}
Ok(DateRange {
start,
end,
freq,
inclusive,
})
}
pub fn generate(&self) -> Result<Vec<T>> {
let mut result = Vec::new();
let mut current = self.start.clone();
result.push(current.clone());
loop {
current = match self.freq {
Frequency::Secondly => current.add(Duration::seconds(1)),
Frequency::Minutely => current.add(Duration::minutes(1)),
Frequency::Hourly => current.add(Duration::hours(1)),
Frequency::Daily => current.add(Duration::days(1)),
Frequency::Weekly => current.add(Duration::weeks(1)),
Frequency::Monthly => Self::add_months(¤t, 1)?,
Frequency::Quarterly => Self::add_months(¤t, 3)?,
Frequency::Yearly => Self::add_years(¤t)?,
Frequency::Custom(duration) => current.add(duration),
};
if self.inclusive {
if current > self.end {
break;
}
} else if current >= self.end {
break;
}
result.push(current.clone());
}
Ok(result)
}
fn add_months(current: &T, months: i64) -> Result<T> {
let naive = current.to_utc().naive_utc();
let (year, month) = normalize_year_month(naive.year(), i64::from(naive.month()) + months);
let day = naive.day().min(days_in_month(year, month));
Self::rebuild(naive, year, month, day)
}
fn add_years(current: &T) -> Result<T> {
let naive = current.to_utc().naive_utc();
let year = naive.year().saturating_add(1);
let month = naive.month();
let day = if month == 2 && naive.day() == 29 && !is_leap_year(year) {
28
} else {
naive.day()
};
Self::rebuild(naive, year, month, day)
}
fn rebuild(naive: NaiveDateTime, year: i32, month: u32, day: u32) -> Result<T> {
let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| {
PandRSError::Consistency(format!(
"Date {year:04}-{month:02}-{day:02} is outside the representable calendar range"
))
})?;
let new_naive = NaiveDateTime::new(date, naive.time());
let new_utc = DateTime::<Utc>::from_naive_utc_and_offset(new_naive, Utc);
T::from_str(&new_utc.to_rfc3339())
}
}
fn days_in_month(year: i32, month: u32) -> u32 {
let (year, month) = normalize_year_month(year, i64::from(month));
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
if is_leap_year(year) {
29
} else {
28
}
}
}
}
fn normalize_year_month(year: i32, month: i64) -> (i32, u32) {
let zero_based = month - 1;
let year_offset = zero_based.div_euclid(12);
let normalized_month = zero_based.rem_euclid(12) + 1;
let normalized_year = i64::from(year)
.saturating_add(year_offset)
.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
(normalized_year, normalized_month as u32)
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn date_range<T: Temporal>(
start: T,
end: T,
freq: Frequency,
inclusive: bool,
) -> Result<Vec<T>> {
DateRange::new(start, end, freq, inclusive)?.generate()
}