use chrono::{Datelike, Duration, NaiveDate, Weekday};
use std::collections::HashMap;
use std::error::Error;
use std::hash::Hash;
use std::result::Result;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DateFreq {
Daily, WeeklyMonday, WeeklyFriday, MonthStart, MonthEnd, QuarterStart, QuarterEnd, YearStart, YearEnd, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregationType {
Start, End, }
impl DateFreq {
pub fn from_string(freq: String) -> Result<Self, Box<dyn Error>> {
freq.parse()
}
pub fn to_string(&self) -> String {
let r = match self {
DateFreq::Daily => "D",
DateFreq::WeeklyMonday => "W",
DateFreq::MonthStart => "M",
DateFreq::QuarterStart => "Q",
DateFreq::YearStart => "Y",
DateFreq::MonthEnd => "ME",
DateFreq::QuarterEnd => "QE",
DateFreq::WeeklyFriday => "WF",
DateFreq::YearEnd => "YE",
};
r.to_string()
}
pub fn agg_type(&self) -> AggregationType {
match self {
DateFreq::Daily
| DateFreq::WeeklyMonday
| DateFreq::MonthStart
| DateFreq::QuarterStart
| DateFreq::YearStart => AggregationType::Start,
DateFreq::WeeklyFriday
| DateFreq::MonthEnd
| DateFreq::QuarterEnd
| DateFreq::YearEnd => AggregationType::End,
}
}
}
impl FromStr for DateFreq {
type Err = Box<dyn Error>;
fn from_str(freq: &str) -> Result<Self, Self::Err> {
let r = match freq {
"D" => DateFreq::Daily,
"W" | "WS" => DateFreq::WeeklyMonday,
"M" | "MS" => DateFreq::MonthStart,
"Q" | "QS" => DateFreq::QuarterStart,
"Y" | "A" | "AS" | "YS" => DateFreq::YearStart,
"ME" => DateFreq::MonthEnd,
"QE" => DateFreq::QuarterEnd,
"WF" => DateFreq::WeeklyFriday,
"YE" | "AE" => DateFreq::YearEnd,
_ => return Err(format!("Invalid frequency specified: {}", freq).into()),
};
Ok(r)
}
}
#[derive(Debug, Clone)]
pub struct DatesList {
start_date_str: String,
end_date_str: String,
freq: DateFreq,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum GroupKey {
Daily(NaiveDate), Weekly(i32, u32), Monthly(i32, u32), Quarterly(i32, u32), Yearly(i32), }
impl DatesList {
pub fn new(start_date_str: String, end_date_str: String, freq: DateFreq) -> Self {
DatesList {
start_date_str,
end_date_str,
freq,
}
}
pub fn from_n_periods(
start_date_str: String,
freq: DateFreq,
n_periods: usize,
) -> Result<Self, Box<dyn Error>> {
if n_periods == 0 {
return Err("n_periods must be greater than 0".into());
}
let start_date = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d")?;
let generator = DatesGenerator::new(start_date, freq, n_periods)?;
let dates: Vec<NaiveDate> = generator.collect();
let last_date = dates
.last()
.ok_or("Generator failed to produce dates even though n_periods > 0")?;
let end_date_str = last_date.format("%Y-%m-%d").to_string();
Ok(DatesList {
start_date_str, end_date_str,
freq,
})
}
pub fn list(&self) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
get_dates_list_with_freq(&self.start_date_str, &self.end_date_str, self.freq)
}
pub fn count(&self) -> Result<usize, Box<dyn Error>> {
self.list().map(|list| list.len())
}
pub fn groups(&self) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error>> {
let dates = self.list()?;
group_dates_helper(dates, self.freq)
}
pub fn start_date(&self) -> Result<NaiveDate, Box<dyn Error>> {
NaiveDate::parse_from_str(&self.start_date_str, "%Y-%m-%d").map_err(|e| e.into())
}
pub fn start_date_str(&self) -> &str {
&self.start_date_str
}
pub fn end_date(&self) -> Result<NaiveDate, Box<dyn Error>> {
NaiveDate::parse_from_str(&self.end_date_str, "%Y-%m-%d").map_err(|e| e.into())
}
pub fn end_date_str(&self) -> &str {
&self.end_date_str
}
pub fn freq(&self) -> DateFreq {
self.freq
}
pub fn freq_str(&self) -> String {
self.freq.to_string()
}
}
#[derive(Debug, Clone)]
pub struct DatesGenerator {
pub freq: DateFreq,
pub periods_remaining: usize,
pub next_date_candidate: Option<NaiveDate>,
}
impl DatesGenerator {
pub fn new(
start_date: NaiveDate,
freq: DateFreq,
n_periods: usize,
) -> Result<Self, Box<dyn Error>> {
let first_date = if n_periods > 0 {
Some(find_first_date_on_or_after(start_date, freq)?)
} else {
None };
Ok(DatesGenerator {
freq,
periods_remaining: n_periods,
next_date_candidate: first_date,
})
}
}
impl Iterator for DatesGenerator {
type Item = NaiveDate;
fn next(&mut self) -> Option<Self::Item> {
match self.next_date_candidate {
Some(current_date) if self.periods_remaining > 0 => {
self.next_date_candidate = find_next_date(current_date, self.freq).ok();
self.periods_remaining -= 1;
Some(current_date)
}
_ => {
self.periods_remaining = 0; self.next_date_candidate = None;
None
}
}
}
}
pub fn group_dates_helper(
dates: Vec<NaiveDate>,
freq: DateFreq,
) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error + 'static>> {
let mut groups: HashMap<GroupKey, Vec<NaiveDate>> = HashMap::new();
for date in dates {
let key = match freq {
DateFreq::Daily => GroupKey::Daily(date),
DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => {
let iso_week = date.iso_week();
GroupKey::Weekly(iso_week.year(), iso_week.week())
}
DateFreq::MonthStart | DateFreq::MonthEnd => {
GroupKey::Monthly(date.year(), date.month())
}
DateFreq::QuarterStart | DateFreq::QuarterEnd => {
GroupKey::Quarterly(date.year(), month_to_quarter(date.month()))
}
DateFreq::YearStart | DateFreq::YearEnd => GroupKey::Yearly(date.year()),
};
groups.entry(key).or_insert_with(Vec::new).push(date);
}
let mut sorted_groups: Vec<(GroupKey, Vec<NaiveDate>)> = groups.into_iter().collect();
sorted_groups.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
let result_groups = sorted_groups.into_iter().map(|(_, dates)| dates).collect();
Ok(result_groups)
}
pub fn get_dates_list_with_freq(
start_date_str: &str,
end_date_str: &str,
freq: DateFreq,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")?;
let end_date = NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")?;
if start_date > end_date {
return Ok(Vec::new());
}
let dates = match freq {
DateFreq::Daily => collect_daily(start_date, end_date)?,
DateFreq::WeeklyMonday => collect_weekly(start_date, end_date, Weekday::Mon)?,
DateFreq::WeeklyFriday => collect_weekly(start_date, end_date, Weekday::Fri)?,
DateFreq::MonthStart => {
collect_monthly(start_date, end_date, true)?
}
DateFreq::MonthEnd => {
collect_monthly(start_date, end_date, false)?
}
DateFreq::QuarterStart => {
collect_quarterly(start_date, end_date, true)?
}
DateFreq::QuarterEnd => {
collect_quarterly(start_date, end_date, false)?
}
DateFreq::YearStart => collect_yearly(start_date, end_date, true)?,
DateFreq::YearEnd => collect_yearly(start_date, end_date, false)?,
};
Ok(dates)
}
fn collect_daily(
start_date: NaiveDate,
end_date: NaiveDate,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let mut result = Vec::new();
let mut current = start_date;
while current <= end_date {
result.push(current);
current = current
.succ_opt()
.ok_or("Date overflow near end of supported range")?;
}
Ok(result)
}
fn collect_weekly(
start_date: NaiveDate,
end_date: NaiveDate,
target_weekday: Weekday,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let mut result = Vec::new();
let mut current = move_to_day_of_week_on_or_after(start_date, target_weekday)?;
while current <= end_date {
result.push(current);
current = current
.checked_add_signed(Duration::days(7))
.ok_or("Date overflow adding 7 days")?;
}
Ok(result)
}
fn collect_monthly(
start_date: NaiveDate,
end_date: NaiveDate,
want_first_day: bool,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let mut result = Vec::new();
let mut year = start_date.year();
let mut month = start_date.month();
let next_month = |(yr, mo): (i32, u32)| -> (i32, u32) {
if mo == 12 {
(yr + 1, 1)
} else {
(yr, mo + 1)
}
};
loop {
let candidate = if want_first_day {
first_day_of_month(year, month)?
} else {
last_day_of_month(year, month)?
};
if candidate > end_date {
break;
}
if candidate >= start_date {
result.push(candidate);
}
if year > end_date.year() || (year == end_date.year() && month >= end_date.month()) {
break;
}
let (ny, nm) = next_month((year, month));
year = ny;
month = nm;
if year > end_date.year() + 2 {
return Err("Loop seems to exceed reasonable year range in collect_monthly".into());
}
}
Ok(result)
}
fn collect_quarterly(
start_date: NaiveDate,
end_date: NaiveDate,
want_first_day: bool,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let mut result = Vec::new();
let mut year = start_date.year();
let mut q = month_to_quarter(start_date.month());
loop {
let candidate = if want_first_day {
first_day_of_quarter(year, q)?
} else {
last_day_of_quarter(year, q)?
};
if candidate > end_date {
break;
}
if candidate >= start_date {
result.push(candidate);
}
let end_q = month_to_quarter(end_date.month());
if year > end_date.year() || (year == end_date.year() && q >= end_q) {
break;
}
if q == 4 {
year += 1;
q = 1;
} else {
q += 1;
}
if year > end_date.year() + 2 {
return Err("Loop seems to exceed reasonable year range in collect_quarterly".into());
}
}
Ok(result)
}
pub fn get_dates_list_with_freq_from_naive_date(
start_date: NaiveDate,
end_date: NaiveDate,
freq: DateFreq,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
get_dates_list_with_freq(
&start_date.format("%Y-%m-%d").to_string(),
&end_date.format("%Y-%m-%d").to_string(),
freq,
)
}
fn collect_yearly(
start_date: NaiveDate,
end_date: NaiveDate,
want_first_day: bool,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
let mut result = Vec::new();
let mut year = start_date.year();
while year <= end_date.year() {
let candidate = if want_first_day {
first_day_of_year(year)?
} else {
last_day_of_year(year)?
};
if candidate >= start_date && candidate <= end_date {
result.push(candidate);
} else if candidate > end_date {
break;
}
year = year.checked_add(1).ok_or("Year overflow")?;
}
Ok(result)
}
fn move_to_day_of_week_on_or_after(
date: NaiveDate,
target: Weekday,
) -> Result<NaiveDate, Box<dyn Error>> {
let mut current = date;
while current.weekday() != target {
current = current
.succ_opt()
.ok_or("Date overflow moving to next weekday")?;
}
Ok(current)
}
fn first_day_of_month(year: i32, month: u32) -> Result<NaiveDate, Box<dyn Error>> {
if !(1..=12).contains(&month) {
return Err(format!("Invalid month: {}", month).into());
}
NaiveDate::from_ymd_opt(year, month, 1)
.ok_or_else(|| format!("Invalid year-month combination: {}-{}", year, month).into())
}
fn days_in_month(year: i32, month: u32) -> Result<u32, Box<dyn Error>> {
if !(1..=12).contains(&month) {
return Err(format!("Invalid month: {}", month).into());
}
let (ny, nm) = if month == 12 {
(
year.checked_add(1)
.ok_or("Year overflow calculating next month")?,
1,
)
} else {
(year, month + 1)
};
let first_of_next = first_day_of_month(ny, nm)?;
let last_of_this = first_of_next
.pred_opt()
.ok_or("Date underflow calculating last day of month")?;
Ok(last_of_this.day())
}
fn last_day_of_month(year: i32, month: u32) -> Result<NaiveDate, Box<dyn Error>> {
let last_dom = days_in_month(year, month)?;
NaiveDate::from_ymd_opt(year, month, last_dom)
.ok_or_else(|| format!("Invalid year-month-day: {}-{}-{}", year, month, last_dom).into())
}
pub fn month_to_quarter(m: u32) -> u32 {
match m {
1..=3 => 1,
4..=6 => 2,
7..=9 => 3,
10..=12 => 4,
_ => panic!("Invalid month: {}", m), }
}
fn quarter_start_month(quarter: u32) -> Result<u32, Box<dyn Error>> {
match quarter {
1 => Ok(1), 2 => Ok(4), 3 => Ok(7), 4 => Ok(10), _ => Err(format!("invalid quarter: {}", quarter).into()), }
}
fn first_day_of_quarter(year: i32, quarter: u32) -> Result<NaiveDate, Box<dyn Error>> {
let month = quarter_start_month(quarter)?;
first_day_of_month(year, month)
}
fn quarter_end_month(quarter: u32) -> Result<u32, Box<dyn Error>> {
match quarter {
1 => Ok(3), 2 => Ok(6), 3 => Ok(9), 4 => Ok(12), _ => Err(format!("invalid quarter: {}", quarter).into()), }
}
fn last_day_of_quarter(year: i32, quarter: u32) -> Result<NaiveDate, Box<dyn Error>> {
let month = quarter_end_month(quarter)?;
last_day_of_month(year, month)
}
fn first_day_of_year(year: i32) -> Result<NaiveDate, Box<dyn Error>> {
NaiveDate::from_ymd_opt(year, 1, 1)
.ok_or_else(|| format!("Invalid year for Jan 1st: {}", year).into())
}
fn last_day_of_year(year: i32) -> Result<NaiveDate, Box<dyn Error>> {
NaiveDate::from_ymd_opt(year, 12, 31)
.ok_or_else(|| format!("Invalid year for Dec 31st: {}", year).into())
}
fn get_first_date_helper(freq: DateFreq) -> fn(i32, u32) -> Result<NaiveDate, Box<dyn Error>> {
if matches!(
freq,
DateFreq::Daily | DateFreq::WeeklyMonday | DateFreq::WeeklyFriday
) {
panic!("Daily, WeeklyMonday, and WeeklyFriday frequencies are not supported here");
}
match freq {
DateFreq::MonthStart => first_day_of_month,
DateFreq::MonthEnd => last_day_of_month,
DateFreq::QuarterStart => first_day_of_quarter,
DateFreq::QuarterEnd => last_day_of_quarter,
DateFreq::YearStart => |year, _| first_day_of_year(year),
DateFreq::YearEnd => |year, _| last_day_of_year(year),
_ => unreachable!(),
}
}
pub fn find_first_date_on_or_after(
start_date: NaiveDate,
freq: DateFreq,
) -> Result<NaiveDate, Box<dyn Error>> {
match freq {
DateFreq::Daily => Ok(start_date), DateFreq::WeeklyMonday => move_to_day_of_week_on_or_after(start_date, Weekday::Mon),
DateFreq::WeeklyFriday => move_to_day_of_week_on_or_after(start_date, Weekday::Fri),
DateFreq::MonthStart | DateFreq::MonthEnd => {
let get_cand_func = get_first_date_helper(freq);
let mut candidate = get_cand_func(start_date.year(), start_date.month())?;
if candidate < start_date {
let (next_y, next_m) = if start_date.month() == 12 {
(start_date.year().checked_add(1).ok_or("Year overflow")?, 1)
} else {
(start_date.year(), start_date.month() + 1)
};
candidate = get_cand_func(next_y, next_m)?;
}
Ok(candidate)
}
DateFreq::QuarterStart | DateFreq::QuarterEnd => {
let current_q = month_to_quarter(start_date.month());
let get_cand_func = get_first_date_helper(freq);
let mut candidate = get_cand_func(start_date.year(), current_q)?;
if candidate < start_date {
let (next_y, next_q) = if current_q == 4 {
(start_date.year().checked_add(1).ok_or("Year overflow")?, 1)
} else {
(start_date.year(), current_q + 1)
};
candidate = get_cand_func(next_y, next_q)?;
}
Ok(candidate)
}
DateFreq::YearStart | DateFreq::YearEnd => {
let get_cand_func = get_first_date_helper(freq);
let mut candidate = get_cand_func(start_date.year(), 0)?;
if candidate < start_date {
candidate =
get_cand_func(start_date.year().checked_add(1).ok_or("Year overflow")?, 0)?;
}
Ok(candidate)
}
}
}
pub fn find_next_date(
current_date: NaiveDate,
freq: DateFreq,
) -> Result<NaiveDate, Box<dyn Error>> {
match freq {
DateFreq::Daily => current_date
.succ_opt()
.ok_or_else(|| "Date overflow finding next daily".into()),
DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => current_date
.checked_add_signed(Duration::days(7))
.ok_or_else(|| "Date overflow adding 7 days".into()),
DateFreq::MonthStart | DateFreq::MonthEnd => {
let get_cand_func = get_first_date_helper(freq);
let (next_y, next_m) = if current_date.month() == 12 {
(
current_date.year().checked_add(1).ok_or("Year overflow")?,
1,
)
} else {
(current_date.year(), current_date.month() + 1)
};
get_cand_func(next_y, next_m)
}
DateFreq::QuarterStart | DateFreq::QuarterEnd => {
let current_q = month_to_quarter(current_date.month());
let get_cand_func = get_first_date_helper(freq);
let (next_y, next_q) = if current_q == 4 {
(
current_date.year().checked_add(1).ok_or("Year overflow")?,
1,
)
} else {
(current_date.year(), current_q + 1)
};
get_cand_func(next_y, next_q)
}
DateFreq::YearStart | DateFreq::YearEnd => {
let get_cand_func = get_first_date_helper(freq);
get_cand_func(
current_date.year().checked_add(1).ok_or("Year overflow")?,
0,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Duration, NaiveDate, Weekday};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("Invalid date in test setup")
}
#[test]
fn test_datefreq_from_str() -> Result<(), Box<dyn Error>> {
assert_eq!(DateFreq::from_str("D")?, DateFreq::Daily);
assert_eq!("D".parse::<DateFreq>()?, DateFreq::Daily);
assert_eq!(DateFreq::from_str("W")?, DateFreq::WeeklyMonday);
assert_eq!(DateFreq::from_str("WS")?, DateFreq::WeeklyMonday);
assert_eq!(DateFreq::from_str("M")?, DateFreq::MonthStart);
assert_eq!(DateFreq::from_str("MS")?, DateFreq::MonthStart);
assert_eq!(DateFreq::from_str("Q")?, DateFreq::QuarterStart);
assert_eq!(DateFreq::from_str("QS")?, DateFreq::QuarterStart);
assert_eq!(DateFreq::from_str("Y")?, DateFreq::YearStart);
assert_eq!(DateFreq::from_str("A")?, DateFreq::YearStart);
assert_eq!(DateFreq::from_str("AS")?, DateFreq::YearStart);
assert_eq!(DateFreq::from_str("YS")?, DateFreq::YearStart);
assert_eq!(DateFreq::from_str("ME")?, DateFreq::MonthEnd);
assert_eq!(DateFreq::from_str("QE")?, DateFreq::QuarterEnd);
assert_eq!(DateFreq::from_str("WF")?, DateFreq::WeeklyFriday);
assert_eq!("WF".parse::<DateFreq>()?, DateFreq::WeeklyFriday);
assert_eq!(DateFreq::from_str("YE")?, DateFreq::YearEnd);
assert_eq!(DateFreq::from_str("AE")?, DateFreq::YearEnd);
assert!(DateFreq::from_str("INVALID").is_err());
assert!("INVALID".parse::<DateFreq>().is_err());
let err = DateFreq::from_str("INVALID").unwrap_err();
assert_eq!(err.to_string(), "Invalid frequency specified: INVALID");
Ok(())
}
#[test]
fn test_datefreq_to_string() {
assert_eq!(DateFreq::Daily.to_string(), "D");
assert_eq!(DateFreq::WeeklyMonday.to_string(), "W");
assert_eq!(DateFreq::MonthStart.to_string(), "M");
assert_eq!(DateFreq::QuarterStart.to_string(), "Q");
assert_eq!(DateFreq::YearStart.to_string(), "Y");
assert_eq!(DateFreq::MonthEnd.to_string(), "ME");
assert_eq!(DateFreq::QuarterEnd.to_string(), "QE");
assert_eq!(DateFreq::WeeklyFriday.to_string(), "WF");
assert_eq!(DateFreq::YearEnd.to_string(), "YE");
}
#[test]
fn test_datefreq_from_string() -> Result<(), Box<dyn Error>> {
assert_eq!(DateFreq::from_string("D".to_string())?, DateFreq::Daily);
assert!(DateFreq::from_string("INVALID".to_string()).is_err());
Ok(())
}
#[test]
fn test_datefreq_agg_type() {
assert_eq!(DateFreq::Daily.agg_type(), AggregationType::Start);
assert_eq!(DateFreq::WeeklyMonday.agg_type(), AggregationType::Start);
assert_eq!(DateFreq::MonthStart.agg_type(), AggregationType::Start);
assert_eq!(DateFreq::QuarterStart.agg_type(), AggregationType::Start);
assert_eq!(DateFreq::YearStart.agg_type(), AggregationType::Start);
assert_eq!(DateFreq::WeeklyFriday.agg_type(), AggregationType::End);
assert_eq!(DateFreq::MonthEnd.agg_type(), AggregationType::End);
assert_eq!(DateFreq::QuarterEnd.agg_type(), AggregationType::End);
assert_eq!(DateFreq::YearEnd.agg_type(), AggregationType::End);
}
#[test]
fn test_dates_list_properties_new() -> Result<(), Box<dyn Error>> {
let start_str = "2023-01-01".to_string();
let end_str = "2023-12-31".to_string();
let freq = DateFreq::QuarterEnd;
let dates_list = DatesList::new(start_str.clone(), end_str.clone(), freq);
assert_eq!(dates_list.start_date_str(), start_str);
assert_eq!(dates_list.end_date_str(), end_str);
assert_eq!(dates_list.freq(), freq);
assert_eq!(dates_list.freq_str(), "QE");
assert_eq!(dates_list.start_date()?, date(2023, 1, 1));
assert_eq!(dates_list.end_date()?, date(2023, 12, 31));
Ok(())
}
#[test]
fn test_dates_list_properties_from_n_periods() -> Result<(), Box<dyn Error>> {
let start_str = "2023-01-01".to_string(); let freq = DateFreq::Daily;
let n_periods = 5; let dates_list = DatesList::from_n_periods(start_str.clone(), freq, n_periods)?;
assert_eq!(dates_list.start_date_str(), start_str);
assert_eq!(dates_list.end_date_str(), "2023-01-05");
assert_eq!(dates_list.freq(), freq);
assert_eq!(dates_list.freq_str(), "D");
assert_eq!(dates_list.start_date()?, date(2023, 1, 1));
assert_eq!(dates_list.end_date()?, date(2023, 1, 5));
assert_eq!(
dates_list.list()?,
vec![
date(2023, 1, 1),
date(2023, 1, 2),
date(2023, 1, 3),
date(2023, 1, 4),
date(2023, 1, 5)
]
);
assert_eq!(dates_list.count()?, 5);
Ok(())
}
#[test]
fn test_dates_list_from_n_periods_zero_periods() {
let start_str = "2023-01-01".to_string();
let freq = DateFreq::Daily;
let n_periods = 0;
let result = DatesList::from_n_periods(start_str.clone(), freq, n_periods);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"n_periods must be greater than 0"
);
}
#[test]
fn test_dates_list_from_n_periods_invalid_start_date() {
let start_str = "invalid-date".to_string();
let freq = DateFreq::Daily;
let n_periods = 5;
let result = DatesList::from_n_periods(start_str.clone(), freq, n_periods);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("input contains invalid characters") );
}
#[test]
fn test_dates_list_invalid_date_string_new() {
let list_start_invalid = DatesList::new(
"invalid-date".to_string(),
"2023-12-31".to_string(),
DateFreq::Daily,
);
assert!(list_start_invalid.list().is_err());
assert!(list_start_invalid.count().is_err());
assert!(list_start_invalid.groups().is_err());
assert!(list_start_invalid.start_date().is_err());
assert!(list_start_invalid.end_date().is_ok());
let list_end_invalid = DatesList::new(
"2023-01-01".to_string(),
"invalid-date".to_string(),
DateFreq::Daily,
);
assert!(list_end_invalid.list().is_err());
assert!(list_end_invalid.count().is_err());
assert!(list_end_invalid.groups().is_err());
assert!(list_end_invalid.start_date().is_ok()); assert!(list_end_invalid.end_date().is_err());
}
#[test]
fn test_dates_list_daily_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-11-01".to_string(), "2023-11-05".to_string(), DateFreq::Daily,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 5);
assert_eq!(
list,
vec![
date(2023, 11, 1),
date(2023, 11, 2),
date(2023, 11, 3),
date(2023, 11, 4),
date(2023, 11, 5)
]
);
assert_eq!(dates_list.count()?, 5);
Ok(())
}
#[test]
fn test_dates_list_weekly_monday_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-10-30".to_string(), "2023-11-13".to_string(), DateFreq::WeeklyMonday,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 3);
assert_eq!(
list,
vec![date(2023, 10, 30), date(2023, 11, 6), date(2023, 11, 13)]
);
assert_eq!(dates_list.count()?, 3);
Ok(())
}
#[test]
fn test_dates_list_weekly_friday_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-11-01".to_string(), "2023-11-17".to_string(), DateFreq::WeeklyFriday,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 3);
assert_eq!(
list,
vec![date(2023, 11, 3), date(2023, 11, 10), date(2023, 11, 17)]
);
assert_eq!(dates_list.count()?, 3);
Ok(())
}
#[test]
fn test_dates_list_month_start_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-11-15".to_string(), "2024-02-01".to_string(), DateFreq::MonthStart,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 3);
assert_eq!(
list,
vec![date(2023, 12, 1), date(2024, 1, 1), date(2024, 2, 1)]
);
assert_eq!(dates_list.count()?, 3);
Ok(())
}
#[test]
fn test_dates_list_month_end_list_leap() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2024-01-15".to_string(), "2024-03-31".to_string(), DateFreq::MonthEnd,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 3);
assert_eq!(
list,
vec![date(2024, 1, 31), date(2024, 2, 29), date(2024, 3, 31)]
);
assert_eq!(dates_list.count()?, 3);
Ok(())
}
#[test]
fn test_dates_list_quarter_start_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-08-01".to_string(), "2024-04-01".to_string(), DateFreq::QuarterStart,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 3);
assert_eq!(
list,
vec![date(2023, 10, 1), date(2024, 1, 1), date(2024, 4, 1)]
);
assert_eq!(dates_list.count()?, 3);
Ok(())
}
#[test]
fn test_dates_list_quarter_end_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-03-31".to_string(), "2023-12-31".to_string(), DateFreq::QuarterEnd,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 4);
assert_eq!(
list,
vec![
date(2023, 3, 31),
date(2023, 6, 30),
date(2023, 9, 30),
date(2023, 12, 31)
]
);
assert_eq!(dates_list.count()?, 4);
Ok(())
}
#[test]
fn test_dates_list_year_start_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-06-01".to_string(), "2025-01-01".to_string(), DateFreq::YearStart,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 2);
assert_eq!(list, vec![date(2024, 1, 1), date(2025, 1, 1)]);
assert_eq!(dates_list.count()?, 2);
Ok(())
}
#[test]
fn test_dates_list_year_end_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2022-01-01".to_string(), "2024-03-31".to_string(), DateFreq::YearEnd,
);
let list = dates_list.list()?;
assert_eq!(list.len(), 2);
assert_eq!(list, vec![date(2022, 12, 31), date(2023, 12, 31)]);
assert_eq!(dates_list.count()?, 2);
Ok(())
}
#[test]
fn test_dates_list_empty_range_list() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-12-31".to_string(),
"2023-01-01".to_string(), DateFreq::Daily,
);
let list = dates_list.list()?;
assert!(list.is_empty());
assert_eq!(dates_list.count()?, 0);
Ok(())
}
#[test]
fn test_dates_list_groups_monthly_end() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-10-15".to_string(), "2024-01-15".to_string(), DateFreq::MonthEnd,
);
let groups = dates_list.groups()?;
assert_eq!(groups.len(), 3);
assert_eq!(groups[0], vec![date(2023, 10, 31)]);
assert_eq!(groups[1], vec![date(2023, 11, 30)]);
assert_eq!(groups[2], vec![date(2023, 12, 31)]);
Ok(())
}
#[test]
fn test_dates_list_groups_daily() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-11-01".to_string(), "2023-11-03".to_string(), DateFreq::Daily,
);
let groups = dates_list.groups()?;
assert_eq!(groups.len(), 3);
assert_eq!(groups[0], vec![date(2023, 11, 1)]);
assert_eq!(groups[1], vec![date(2023, 11, 2)]);
assert_eq!(groups[2], vec![date(2023, 11, 3)]);
Ok(())
}
#[test]
fn test_dates_list_groups_weekly_friday() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-11-01".to_string(), "2023-11-15".to_string(), DateFreq::WeeklyFriday,
);
let groups = dates_list.groups()?;
assert_eq!(groups.len(), 2);
assert_eq!(groups[0], vec![date(2023, 11, 3)]);
assert_eq!(groups[1], vec![date(2023, 11, 10)]);
Ok(())
}
#[test]
fn test_dates_list_groups_quarterly_start() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-08-01".to_string(), "2024-05-01".to_string(), DateFreq::QuarterStart,
);
let groups = dates_list.groups()?;
assert_eq!(groups.len(), 3);
assert_eq!(groups[0], vec![date(2023, 10, 1)]);
assert_eq!(groups[1], vec![date(2024, 1, 1)]);
assert_eq!(groups[2], vec![date(2024, 4, 1)]);
Ok(())
}
#[test]
fn test_dates_list_groups_yearly_end() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2022-01-01".to_string(), "2024-03-31".to_string(), DateFreq::YearEnd,
);
let groups = dates_list.groups()?;
assert_eq!(groups.len(), 2);
assert_eq!(groups[0], vec![date(2022, 12, 31)]);
assert_eq!(groups[1], vec![date(2023, 12, 31)]);
Ok(())
}
#[test]
fn test_dates_list_groups_empty_range() -> Result<(), Box<dyn Error>> {
let dates_list = DatesList::new(
"2023-12-31".to_string(),
"2023-01-01".to_string(), DateFreq::Daily,
);
let groups = dates_list.groups()?;
assert!(groups.is_empty());
Ok(())
}
#[test]
fn test_move_to_day_of_week_on_or_after() -> Result<(), Box<dyn Error>> {
assert_eq!(
move_to_day_of_week_on_or_after(date(2023, 11, 6), Weekday::Mon)?,
date(2023, 11, 6)
);
assert_eq!(
move_to_day_of_week_on_or_after(date(2023, 11, 8), Weekday::Fri)?,
date(2023, 11, 10)
);
assert_eq!(
move_to_day_of_week_on_or_after(date(2023, 11, 11), Weekday::Mon)?, date(2023, 11, 13)
);
assert_eq!(
move_to_day_of_week_on_or_after(date(2023, 11, 10), Weekday::Mon)?, date(2023, 11, 13)
);
let near_max = NaiveDate::MAX - Duration::days(7);
assert!(move_to_day_of_week_on_or_after(near_max, Weekday::Sun).is_ok());
if NaiveDate::MAX.weekday() != Weekday::Sun {
assert!(move_to_day_of_week_on_or_after(NaiveDate::MAX, Weekday::Sun).is_err());
} else {
assert!(move_to_day_of_week_on_or_after(NaiveDate::MAX, Weekday::Sun).is_ok());
let day_before = NaiveDate::MAX - Duration::days(1);
let target_day_after = NaiveDate::MAX.weekday().succ(); assert!(move_to_day_of_week_on_or_after(day_before, target_day_after).is_err());
}
Ok(())
}
#[test]
fn test_first_day_of_month() -> Result<(), Box<dyn Error>> {
assert_eq!(first_day_of_month(2023, 11)?, date(2023, 11, 1));
assert_eq!(first_day_of_month(2024, 2)?, date(2024, 2, 1));
assert!(first_day_of_month(2023, 0).is_err()); assert!(first_day_of_month(2023, 13).is_err()); Ok(())
}
#[test]
fn test_days_in_month() -> Result<(), Box<dyn Error>> {
assert_eq!(days_in_month(2023, 1)?, 31);
assert_eq!(days_in_month(2023, 2)?, 28);
assert_eq!(days_in_month(2024, 2)?, 29);
assert_eq!(days_in_month(2023, 4)?, 30);
assert_eq!(days_in_month(2023, 12)?, 31);
assert!(days_in_month(2023, 0).is_err());
assert!(days_in_month(2023, 13).is_err());
assert!(days_in_month(NaiveDate::MAX.year(), 12).is_err());
Ok(())
}
#[test]
fn test_last_day_of_month() -> Result<(), Box<dyn Error>> {
assert_eq!(last_day_of_month(2023, 11)?, date(2023, 11, 30));
assert_eq!(last_day_of_month(2024, 2)?, date(2024, 2, 29)); assert_eq!(last_day_of_month(2023, 12)?, date(2023, 12, 31));
assert!(last_day_of_month(2023, 0).is_err());
assert!(last_day_of_month(2023, 13).is_err());
assert!(last_day_of_month(NaiveDate::MAX.year(), 12).is_err());
Ok(())
}
#[test]
fn test_month_to_quarter() {
assert_eq!(month_to_quarter(1), 1);
assert_eq!(month_to_quarter(3), 1);
assert_eq!(month_to_quarter(4), 2);
assert_eq!(month_to_quarter(6), 2);
assert_eq!(month_to_quarter(7), 3);
assert_eq!(month_to_quarter(9), 3);
assert_eq!(month_to_quarter(10), 4);
assert_eq!(month_to_quarter(12), 4);
}
#[test]
#[should_panic(expected = "Invalid month: 0")]
fn test_month_to_quarter_invalid_low() {
month_to_quarter(0);
}
#[test]
#[should_panic(expected = "Invalid month: 13")]
fn test_month_to_quarter_invalid_high() {
month_to_quarter(13);
}
#[test]
fn test_quarter_start_month() {
assert_eq!(quarter_start_month(1).unwrap(), 1);
assert_eq!(quarter_start_month(2).unwrap(), 4);
assert_eq!(quarter_start_month(3).unwrap(), 7);
assert_eq!(quarter_start_month(4).unwrap(), 10);
assert!(quarter_start_month(0).is_err());
assert!(quarter_start_month(5).is_err());
}
#[test]
fn test_first_day_of_quarter() -> Result<(), Box<dyn Error>> {
assert_eq!(first_day_of_quarter(2023, 1)?, date(2023, 1, 1));
assert_eq!(first_day_of_quarter(2023, 2)?, date(2023, 4, 1));
assert_eq!(first_day_of_quarter(2023, 3)?, date(2023, 7, 1));
assert_eq!(first_day_of_quarter(2023, 4)?, date(2023, 10, 1));
assert!(first_day_of_quarter(2023, 5).is_err());
Ok(())
}
#[test]
fn test_quarter_end_month() {
assert_eq!(quarter_end_month(1).unwrap(), 3);
assert_eq!(quarter_end_month(2).unwrap(), 6);
assert_eq!(quarter_end_month(3).unwrap(), 9);
assert_eq!(quarter_end_month(4).unwrap(), 12);
assert!(quarter_end_month(0).is_err());
assert!(quarter_end_month(5).is_err());
}
#[test]
fn test_last_day_of_quarter() -> Result<(), Box<dyn Error>> {
assert_eq!(last_day_of_quarter(2023, 1)?, date(2023, 3, 31));
assert_eq!(last_day_of_quarter(2023, 2)?, date(2023, 6, 30));
assert_eq!(last_day_of_quarter(2023, 3)?, date(2023, 9, 30));
assert_eq!(last_day_of_quarter(2023, 4)?, date(2023, 12, 31));
assert_eq!(last_day_of_quarter(2024, 1)?, date(2024, 3, 31));
assert!(last_day_of_quarter(2023, 5).is_err());
assert!(last_day_of_quarter(NaiveDate::MAX.year(), 4).is_err());
Ok(())
}
#[test]
fn test_first_day_of_year() -> Result<(), Box<dyn Error>> {
assert_eq!(first_day_of_year(2023)?, date(2023, 1, 1));
assert_eq!(first_day_of_year(2024)?, date(2024, 1, 1));
assert!(first_day_of_year(NaiveDate::MAX.year()).is_ok());
Ok(())
}
#[test]
fn test_last_day_of_year() -> Result<(), Box<dyn Error>> {
assert_eq!(last_day_of_year(2023)?, date(2023, 12, 31));
assert_eq!(last_day_of_year(2024)?, date(2024, 12, 31));
assert_eq!(last_day_of_year(NaiveDate::MAX.year())?, NaiveDate::MAX);
Ok(())
}
#[test]
fn test_find_first_date_on_or_after() -> Result<(), Box<dyn Error>> {
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 8), DateFreq::Daily)?,
date(2023, 11, 8)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 11), DateFreq::Daily)?,
date(2023, 11, 11)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 8), DateFreq::WeeklyMonday)?,
date(2023, 11, 13)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 13), DateFreq::WeeklyMonday)?,
date(2023, 11, 13)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 12), DateFreq::WeeklyMonday)?,
date(2023, 11, 13)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 8), DateFreq::WeeklyFriday)?,
date(2023, 11, 10)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 10), DateFreq::WeeklyFriday)?,
date(2023, 11, 10)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 11), DateFreq::WeeklyFriday)?,
date(2023, 11, 17)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 1), DateFreq::MonthStart)?,
date(2023, 11, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 10, 15), DateFreq::MonthStart)?,
date(2023, 11, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 12, 15), DateFreq::MonthStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 10, 1), DateFreq::MonthStart)?,
date(2023, 10, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 30), DateFreq::MonthEnd)?,
date(2023, 11, 30)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 15), DateFreq::MonthEnd)?,
date(2023, 11, 30)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 12, 31), DateFreq::MonthEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_first_date_on_or_after(date(2024, 2, 15), DateFreq::MonthEnd)?,
date(2024, 2, 29)
);
assert_eq!(
find_first_date_on_or_after(date(2024, 2, 29), DateFreq::MonthEnd)?,
date(2024, 2, 29)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 10, 1), DateFreq::QuarterStart)?,
date(2023, 10, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 8, 15), DateFreq::QuarterStart)?,
date(2023, 10, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 11, 15), DateFreq::QuarterStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 1, 1), DateFreq::QuarterStart)?,
date(2023, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 9, 30), DateFreq::QuarterEnd)?,
date(2023, 9, 30)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 8, 15), DateFreq::QuarterEnd)?,
date(2023, 9, 30)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 10, 15), DateFreq::QuarterEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 12, 31), DateFreq::QuarterEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_first_date_on_or_after(date(2024, 1, 1), DateFreq::YearStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 6, 15), DateFreq::YearStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 1, 1), DateFreq::YearStart)?,
date(2023, 1, 1)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 12, 31), DateFreq::YearEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_first_date_on_or_after(date(2023, 6, 15), DateFreq::YearEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_first_date_on_or_after(date(2022, 12, 31), DateFreq::YearEnd)?,
date(2022, 12, 31)
);
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::Daily).is_ok());
if NaiveDate::MAX.weekday() != Weekday::Mon {
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::WeeklyMonday).is_err());
} else {
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::WeeklyMonday).is_ok());
}
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::MonthStart).is_err());
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::MonthEnd).is_err());
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::QuarterStart).is_err());
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::QuarterEnd).is_err());
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::YearStart).is_err());
assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::YearEnd).is_ok());
Ok(())
}
#[test]
fn test_find_next_date() -> Result<(), Box<dyn Error>> {
assert_eq!(
find_next_date(date(2023, 11, 8), DateFreq::Daily)?,
date(2023, 11, 9)
);
assert_eq!(
find_next_date(date(2023, 11, 10), DateFreq::Daily)?,
date(2023, 11, 11)
);
assert_eq!(
find_next_date(date(2023, 11, 13), DateFreq::WeeklyMonday)?,
date(2023, 11, 20)
);
assert_eq!(
find_next_date(date(2023, 11, 10), DateFreq::WeeklyFriday)?,
date(2023, 11, 17)
);
assert_eq!(
find_next_date(date(2023, 11, 1), DateFreq::MonthStart)?,
date(2023, 12, 1)
);
assert_eq!(
find_next_date(date(2023, 12, 1), DateFreq::MonthStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_next_date(date(2023, 10, 31), DateFreq::MonthEnd)?,
date(2023, 11, 30)
);
assert_eq!(
find_next_date(date(2024, 1, 31), DateFreq::MonthEnd)?,
date(2024, 2, 29)
); assert_eq!(
find_next_date(date(2024, 2, 29), DateFreq::MonthEnd)?,
date(2024, 3, 31)
);
assert_eq!(
find_next_date(date(2023, 10, 1), DateFreq::QuarterStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_next_date(date(2024, 1, 1), DateFreq::QuarterStart)?,
date(2024, 4, 1)
);
assert_eq!(
find_next_date(date(2023, 9, 30), DateFreq::QuarterEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_next_date(date(2023, 12, 31), DateFreq::QuarterEnd)?,
date(2024, 3, 31)
);
assert_eq!(
find_next_date(date(2023, 1, 1), DateFreq::YearStart)?,
date(2024, 1, 1)
);
assert_eq!(
find_next_date(date(2024, 1, 1), DateFreq::YearStart)?,
date(2025, 1, 1)
);
assert_eq!(
find_next_date(date(2022, 12, 31), DateFreq::YearEnd)?,
date(2023, 12, 31)
);
assert_eq!(
find_next_date(date(2023, 12, 31), DateFreq::YearEnd)?,
date(2024, 12, 31)
);
assert!(find_next_date(NaiveDate::MAX, DateFreq::Daily).is_err());
assert!(
find_next_date(NaiveDate::MAX - Duration::days(6), DateFreq::WeeklyMonday).is_err()
);
assert!(find_next_date(date(NaiveDate::MAX.year(), 12, 1), DateFreq::MonthStart).is_err());
let nov_end_max_year = last_day_of_month(NaiveDate::MAX.year(), 11)?;
assert!(find_next_date(nov_end_max_year, DateFreq::MonthEnd).is_err());
assert!(find_next_date(NaiveDate::MAX, DateFreq::MonthEnd).is_err());
assert!(find_next_date(
first_day_of_quarter(NaiveDate::MAX.year(), 4)?,
DateFreq::QuarterStart
)
.is_err());
let q3_end_max_year = last_day_of_quarter(NaiveDate::MAX.year(), 3)?;
assert!(find_next_date(q3_end_max_year, DateFreq::QuarterEnd).is_err());
assert!(find_next_date(NaiveDate::MAX, DateFreq::QuarterEnd).is_err());
assert!(find_next_date(
first_day_of_year(NaiveDate::MAX.year())?,
DateFreq::YearStart
)
.is_err());
assert!(find_next_date(
last_day_of_year(NaiveDate::MAX.year() - 1)?,
DateFreq::YearEnd
)
.is_ok());
assert!(
find_next_date(last_day_of_year(NaiveDate::MAX.year())?, DateFreq::YearEnd).is_err()
);
Ok(())
}
#[test]
fn test_generator_new_zero_periods() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 1, 1);
let freq = DateFreq::Daily;
let n_periods = 0;
let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), None); Ok(())
}
#[test]
fn test_generator_new_fail_find_first() -> Result<(), Box<dyn Error>> {
let start_date = NaiveDate::MAX;
let freq = DateFreq::WeeklyMonday;
let n_periods = 1;
let result = DatesGenerator::new(start_date, freq, n_periods);
if NaiveDate::MAX.weekday() != Weekday::Mon {
assert!(result.is_err());
} else {
assert!(result.is_ok());
}
Ok(())
}
#[test]
fn test_generator_daily() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 11, 10); let freq = DateFreq::Daily;
let n_periods = 4;
let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 11, 10))); assert_eq!(generator.next(), Some(date(2023, 11, 11))); assert_eq!(generator.next(), Some(date(2023, 11, 12))); assert_eq!(generator.next(), Some(date(2023, 11, 13))); assert_eq!(generator.next(), None);
let generator_collect = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(
generator_collect.collect::<Vec<_>>(),
vec![
date(2023, 11, 10),
date(2023, 11, 11),
date(2023, 11, 12),
date(2023, 11, 13)
]
);
Ok(())
}
#[test]
fn test_generator_weekly_monday() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 11, 8); let freq = DateFreq::WeeklyMonday;
let n_periods = 3;
let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 11, 13)));
assert_eq!(generator.next(), Some(date(2023, 11, 20)));
assert_eq!(generator.next(), Some(date(2023, 11, 27)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_weekly_friday() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 11, 11); let freq = DateFreq::WeeklyFriday;
let n_periods = 3;
let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 11, 17)));
assert_eq!(generator.next(), Some(date(2023, 11, 24)));
assert_eq!(generator.next(), Some(date(2023, 12, 1)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_month_start() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 10, 15); let freq = DateFreq::MonthStart;
let n_periods = 4; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 11, 1)));
assert_eq!(generator.next(), Some(date(2023, 12, 1)));
assert_eq!(generator.next(), Some(date(2024, 1, 1)));
assert_eq!(generator.next(), Some(date(2024, 2, 1)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_month_end_leap() -> Result<(), Box<dyn Error>> {
let start_date = date(2024, 1, 31); let freq = DateFreq::MonthEnd;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2024, 1, 31)));
assert_eq!(generator.next(), Some(date(2024, 2, 29)));
assert_eq!(generator.next(), Some(date(2024, 3, 31)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_quarter_start() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 8, 1); let freq = DateFreq::QuarterStart;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 10, 1)));
assert_eq!(generator.next(), Some(date(2024, 1, 1)));
assert_eq!(generator.next(), Some(date(2024, 4, 1)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_quarter_end() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 11, 1); let freq = DateFreq::QuarterEnd;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 12, 31)));
assert_eq!(generator.next(), Some(date(2024, 3, 31)));
assert_eq!(generator.next(), Some(date(2024, 6, 30)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_year_start() -> Result<(), Box<dyn Error>> {
let start_date = date(2023, 1, 1); let freq = DateFreq::YearStart;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2023, 1, 1)));
assert_eq!(generator.next(), Some(date(2024, 1, 1)));
assert_eq!(generator.next(), Some(date(2025, 1, 1)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_year_end() -> Result<(), Box<dyn Error>> {
let start_date = date(2022, 12, 31); let freq = DateFreq::YearEnd;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(date(2022, 12, 31)));
assert_eq!(generator.next(), Some(date(2023, 12, 31)));
assert_eq!(generator.next(), Some(date(2024, 12, 31)));
assert_eq!(generator.next(), None);
Ok(())
}
#[test]
fn test_generator_stops_after_error_finding_next() -> Result<(), Box<dyn Error>> {
let start_year = NaiveDate::MAX.year();
let start_date = last_day_of_year(start_year - 1)?; let freq = DateFreq::YearEnd;
let n_periods = 3; let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
assert_eq!(generator.next(), Some(start_date));
assert_eq!(generator.next(), Some(last_day_of_year(start_year)?));
assert_eq!(generator.next(), None);
assert_eq!(generator.periods_remaining, 0);
assert!(generator.next_date_candidate.is_none());
assert_eq!(generator.next(), None);
assert_eq!(generator.periods_remaining, 0);
Ok(())
}
}