From fb63b3fd3d5c69485e7cf05359e0c41e4a668569 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Sun, 11 May 2025 22:39:45 +0100 Subject: [PATCH] refactored bdates to depend on dates util --- src/utils/dateutils/bdates.rs | 1597 +++++---------------------------- 1 file changed, 210 insertions(+), 1387 deletions(-) diff --git a/src/utils/dateutils/bdates.rs b/src/utils/dateutils/bdates.rs index c3818ba..8436c21 100644 --- a/src/utils/dateutils/bdates.rs +++ b/src/utils/dateutils/bdates.rs @@ -5,123 +5,11 @@ use std::hash::Hash; use std::result::Result; use std::str::FromStr; -/// Represents the frequency at which business dates should be generated. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum BDateFreq { - Daily, - WeeklyMonday, - MonthStart, - QuarterStart, - YearStart, - MonthEnd, - QuarterEnd, - WeeklyFriday, - YearEnd, -} +use crate::utils::dateutils::dates::{ + find_next_date, AggregationType, DateFreq, DatesGenerator, DatesList, +}; -/// Indicates whether the first or last date in a periodic group (like month, quarter) -/// is selected for the frequency. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AggregationType { - Start, // Select the first valid business day in the period - End, // Select the last valid business day in the period -} - -impl BDateFreq { - /// Attempts to parse a frequency string into a `BDateFreq` enum. - /// - /// This is a convenience wrapper around `from_str`. - /// - /// # Arguments - /// - /// * `freq` - The frequency string (e.g., "D", "W", "ME"). - /// - /// # Errors - /// - /// Returns an error if the string does not match any known frequency. - pub fn from_string(freq: String) -> Result> { - // Delegate parsing to the FromStr implementation - freq.parse() - } - - /// Returns the canonical string representation of the frequency. - /// - /// This returns the primary code (e.g., "D", "W", "Y", "YE"), not the aliases. - pub fn to_string(&self) -> String { - let r = match self { - BDateFreq::Daily => "D", - BDateFreq::WeeklyMonday => "W", - BDateFreq::MonthStart => "M", - BDateFreq::QuarterStart => "Q", - BDateFreq::YearStart => "Y", - BDateFreq::MonthEnd => "ME", - BDateFreq::QuarterEnd => "QE", - BDateFreq::WeeklyFriday => "WF", - BDateFreq::YearEnd => "YE", - }; - r.to_string() - } - - /// Determines whether the frequency represents a start-of-period or end-of-period aggregation. - pub fn agg_type(&self) -> AggregationType { - match self { - BDateFreq::Daily - | BDateFreq::WeeklyMonday - | BDateFreq::MonthStart - | BDateFreq::QuarterStart - | BDateFreq::YearStart => AggregationType::Start, - - BDateFreq::WeeklyFriday - | BDateFreq::MonthEnd - | BDateFreq::QuarterEnd - | BDateFreq::YearEnd => AggregationType::End, - } - } -} - -// Implement FromStr for BDateFreq to allow parsing directly using `parse()` -impl FromStr for BDateFreq { - type Err = Box; - - /// Attempts to parse a frequency string slice into a `BDateFreq` enum. - /// - /// Supports various frequency codes and common aliases. - /// - /// | Code | Alias | Description | - /// |------|---------|---------------------| - /// | D | | Daily | - /// | W | WS | Weekly Monday | - /// | M | MS | Month Start | - /// | Q | QS | Quarter Start | - /// | Y | A, AS, YS | Year Start | - /// | ME | | Month End | - /// | QE | | Quarter End | - /// | WF | | Weekly Friday | - /// | YE | AE | Year End (Annual) | - /// - /// # Arguments - /// - /// * `freq` - The frequency string slice (e.g., "D", "W", "ME"). - /// - /// # Errors - /// - /// Returns an error if the string does not match any known frequency. - fn from_str(freq: &str) -> Result { - let r = match freq { - "D" => BDateFreq::Daily, - "W" | "WS" => BDateFreq::WeeklyMonday, - "M" | "MS" => BDateFreq::MonthStart, - "Q" | "QS" => BDateFreq::QuarterStart, - "Y" | "A" | "AS" | "YS" => BDateFreq::YearStart, // Support standard aliases for year start - "ME" => BDateFreq::MonthEnd, - "QE" => BDateFreq::QuarterEnd, - "WF" => BDateFreq::WeeklyFriday, - "YE" | "AE" => BDateFreq::YearEnd, // Include 'AE' alias for year end - _ => return Err(format!("Invalid frequency specified: {}", freq).into()), - }; - Ok(r) - } -} +use crate::utils::dateutils::dates; /// Represents a list of business dates generated between a start and end date /// at a specified frequency. Provides methods to retrieve the full list, @@ -130,7 +18,7 @@ impl FromStr for BDateFreq { pub struct BDatesList { start_date_str: String, end_date_str: String, - freq: BDateFreq, + freq: DateFreq, // TODO: cache the generated date list to reduce repeated computation. // Currently, list(), count(), and groups() regenerate the list on every invocation. // cached_list: Option>, @@ -163,12 +51,12 @@ enum GroupKey { /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; -/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name +/// use rustframe::utils::{BDatesList, DateFreq}; // Replace bdates with your actual crate/module name /// /// fn main() -> Result<(), Box> { /// let start_date = "2023-11-01".to_string(); // Wednesday /// let end_date = "2023-11-07".to_string(); // Tuesday -/// let freq = BDateFreq::Daily; +/// let freq = DateFreq::Daily; /// /// let bdates = BDatesList::new(start_date, end_date, freq); /// @@ -191,11 +79,11 @@ enum GroupKey { /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; -/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name +/// use rustframe::utils::{BDatesList, DateFreq}; // Replace bdates with your actual crate/module name /// /// fn main() -> Result<(), Box> { /// let start_date = "2024-02-28".to_string(); // Wednesday -/// let freq = BDateFreq::WeeklyFriday; +/// let freq = DateFreq::WeeklyFriday; /// let n_periods = 3; /// /// let bdates = BDatesList::from_n_periods(start_date, freq, n_periods)?; @@ -221,12 +109,12 @@ enum GroupKey { /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; -/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name +/// use rustframe::utils::{BDatesList, DateFreq}; // Replace bdates with your actual crate/module name /// /// fn main() -> Result<(), Box> { /// let start_date = "2023-11-20".to_string(); // Mon, Week 47 /// let end_date = "2023-12-08".to_string(); // Fri, Week 49 -/// let freq = BDateFreq::WeeklyMonday; +/// let freq = DateFreq::WeeklyMonday; /// /// let bdates = BDatesList::new(start_date, end_date, freq); /// @@ -248,7 +136,7 @@ impl BDatesList { /// * `start_date_str` - The inclusive start date as a string (e.g., "YYYY-MM-DD"). /// * `end_date_str` - The inclusive end date as a string (e.g., "YYYY-MM-DD"). /// * `freq` - The frequency for generating dates. - pub fn new(start_date_str: String, end_date_str: String, freq: BDateFreq) -> Self { + pub fn new(start_date_str: String, end_date_str: String, freq: DateFreq) -> Self { BDatesList { start_date_str, end_date_str, @@ -275,7 +163,7 @@ impl BDatesList { /// * `n_periods` is 0 (as this would result in an empty list and no defined end date). pub fn from_n_periods( start_date_str: String, - freq: BDateFreq, + freq: DateFreq, n_periods: usize, ) -> Result> { if n_periods == 0 { @@ -343,18 +231,18 @@ impl BDatesList { for date in dates { // Derive the appropriate GroupKey for the current date based on the configured frequency. let key = match self.freq { - BDateFreq::Daily => GroupKey::Daily(date), - BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => { + DateFreq::Daily => GroupKey::Daily(date), + DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => { let iso_week = date.iso_week(); GroupKey::Weekly(iso_week.year(), iso_week.week()) } - BDateFreq::MonthStart | BDateFreq::MonthEnd => { + DateFreq::MonthStart | DateFreq::MonthEnd => { GroupKey::Monthly(date.year(), date.month()) } - BDateFreq::QuarterStart | BDateFreq::QuarterEnd => { - GroupKey::Quarterly(date.year(), month_to_quarter(date.month())) + DateFreq::QuarterStart | DateFreq::QuarterEnd => { + GroupKey::Quarterly(date.year(), dates::month_to_quarter(date.month())) } - BDateFreq::YearStart | BDateFreq::YearEnd => GroupKey::Yearly(date.year()), + DateFreq::YearStart | DateFreq::YearEnd => GroupKey::Yearly(date.year()), }; // Append the date to its period group. @@ -404,7 +292,7 @@ impl BDatesList { } /// Returns the frequency enum. - pub fn freq(&self) -> BDateFreq { + pub fn freq(&self) -> DateFreq { self.freq } @@ -430,11 +318,11 @@ impl BDatesList { /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; -/// use rustframe::utils::{BDatesGenerator, BDateFreq}; +/// use rustframe::utils::{BDatesGenerator, DateFreq}; /// /// fn main() -> Result<(), Box> { /// let start = NaiveDate::from_ymd_opt(2023, 12, 28).unwrap(); // Thursday -/// let freq = BDateFreq::MonthEnd; +/// let freq = DateFreq::MonthEnd; /// let n_periods = 4; // Dec '23, Jan '24, Feb '24, Mar '24 /// /// let mut generator = BDatesGenerator::new(start, freq, n_periods)?; @@ -454,11 +342,11 @@ impl BDatesList { /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; -/// use rustframe::utils::{BDatesGenerator, BDateFreq}; // Replace bdates with your actual crate/module name +/// use rustframe::utils::{BDatesGenerator, DateFreq}; // Replace bdates with your actual crate/module name /// /// fn main() -> Result<(), Box> { /// let start = NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(); // Monday -/// let freq = BDateFreq::Daily; +/// let freq = DateFreq::Daily; /// let n_periods = 5; /// /// let generator = BDatesGenerator::new(start, freq, n_periods)?; @@ -478,7 +366,7 @@ impl BDatesList { /// ``` #[derive(Debug, Clone)] pub struct BDatesGenerator { - freq: BDateFreq, + freq: DateFreq, periods_remaining: usize, // Next business date candidate to yield; None when iteration is complete. next_date_candidate: Option, @@ -504,9 +392,10 @@ impl BDatesGenerator { /// but practical usage should be safe. pub fn new( start_date: NaiveDate, - freq: BDateFreq, + freq: DateFreq, n_periods: usize, ) -> Result> { + let start_date = iter_till_bdate(start_date); let first_date = if n_periods > 0 { Some(find_first_bdate_on_or_after(start_date, freq)) } else { @@ -547,553 +436,118 @@ impl Iterator for BDatesGenerator { } } -// Internal helper functions (private implementation) +/// Check if the date is a weekend (Saturday or Sunday). +pub fn is_business_date(date: NaiveDate) -> bool { + match date.weekday() { + Weekday::Sat | Weekday::Sun => false, + _ => true, + } +} -/// Generates the flat list of business dates for the given range and frequency. -/// -/// Filters out weekends and ensures the final list is sorted. This is the core -/// generation logic used by `BDatesList::list` and `BDatesList::groups`. -/// -/// # Arguments (Internal) -/// -/// * `start_date_str` - Inclusive start date string. -/// * `end_date_str` - Inclusive end date string. -/// * `freq` - The frequency. -/// -/// # Errors (Internal) -/// -/// Returns an error if date strings are invalid. -fn get_bdates_list_with_freq( +pub fn find_next_bdate(date: NaiveDate, freq: DateFreq) -> NaiveDate { + let next_date: NaiveDate = find_next_date(date, freq).unwrap(); + let next_date = iter_till_bdate(next_date); + next_date +} + +pub fn find_first_bdate_on_or_after(date: NaiveDate, freq: DateFreq) -> NaiveDate { + // Find the first business date on or after the given date. + let first_date = dates::find_first_date_on_or_after(date, freq).unwrap(); + let first_date = iter_till_bdate_by_freq(first_date, freq); + // let first_date = iter_till_bdate(first_date); + + first_date +} + +/// Iterate forwards or backwards (depending on the frequency) +/// until a business date is found. +fn iter_till_bdate_by_freq(date: NaiveDate, freq: DateFreq) -> NaiveDate { + let agg_type = freq.agg_type(); + let dur = match agg_type { + AggregationType::Start => Duration::days(1), + AggregationType::End => Duration::days(-1), + }; + let mut current_date = date; + while !is_business_date(current_date) { + current_date = current_date + dur; + } + current_date +} + +/// Increment day-by-day until a business date is found. +fn iter_till_bdate(date: NaiveDate) -> NaiveDate { + let mut current_date = date; + while !is_business_date(current_date) { + current_date = current_date + Duration::days(1); + } + current_date +} + +/// Increment day-by-day until a business date is found. +fn iter_reverse_till_bdate(date: NaiveDate) -> NaiveDate { + let mut current_date = date; + while !is_business_date(current_date) { + current_date = current_date - Duration::days(1); + } + current_date +} + +fn crop_to_first_and_last_bdate(dates: &mut Vec) { + if dates.is_empty() { + return; + } + let start_date = dates[0].clone(); + let end_date = dates[dates.len() - 1].clone(); + let start_date = iter_till_bdate(start_date); + let end_date = iter_reverse_till_bdate(end_date); + while dates.first().unwrap() < &start_date { + dates.remove(0); + } + while dates.last().unwrap() > &end_date { + dates.pop(); + } + + if dates.is_empty() { + dates.push(start_date); + } + +} + +/// Helper function to get a list of business dates based on the frequency. +pub fn get_bdates_list_with_freq( start_date_str: &str, end_date_str: &str, - freq: BDateFreq, + freq: DateFreq, ) -> Result, Box> { - // Parse input date strings; propagate parsing errors. + // Generate the list of business dates using the shared logic. + 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")?; + let start_date = iter_till_bdate(start_date); + let end_date = iter_reverse_till_bdate(end_date); - // Return empty list immediately if the date range is invalid. - if start_date > end_date { - return Ok(Vec::new()); + let mut dates = dates::get_dates_list_with_freq_from_naive_date(start_date, end_date, freq)?; + + // crop to first and last business date + crop_to_first_and_last_bdate(&mut dates); + + match freq { + DateFreq::Daily => { + dates.retain(|date| is_business_date(*date)); + } + DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => { + // No logic needed (or possible?) + } + _ => { + dates.iter_mut().for_each(|date| { + *date = iter_till_bdate_by_freq(*date, freq); + }); + } } - // Generate dates according to the requested frequency. - let mut dates = match freq { - BDateFreq::Daily => collect_daily(start_date, end_date), - BDateFreq::WeeklyMonday => collect_weekly(start_date, end_date, Weekday::Mon), - BDateFreq::WeeklyFriday => collect_weekly(start_date, end_date, Weekday::Fri), - BDateFreq::MonthStart => collect_monthly(start_date, end_date, true), - BDateFreq::MonthEnd => collect_monthly(start_date, end_date, false), - BDateFreq::QuarterStart => collect_quarterly(start_date, end_date, true), - BDateFreq::QuarterEnd => collect_quarterly(start_date, end_date, false), - BDateFreq::YearStart => collect_yearly(start_date, end_date, true), - BDateFreq::YearEnd => collect_yearly(start_date, end_date, false), - }; - - // Exclude weekends to ensure only business days remain. - dates.retain(|d| is_weekday(*d)); - - // Guarantee chronological order of the result. - dates.sort(); - Ok(dates) } -/* Low-level date collection routines (private) */ - -/// Returns all weekdays from `start_date` through `end_date`, inclusive. -fn collect_daily(start_date: NaiveDate, end_date: NaiveDate) -> Vec { - let mut result = Vec::new(); - let mut current = start_date; - - // Iterate one day at a time. - while current <= end_date { - if is_weekday(current) { - result.push(current); - } - current = current - .succ_opt() - .expect("Date overflow near end of supported range"); - } - - result -} - -/// Returns each occurrence of `target_weekday` within the date range. -fn collect_weekly( - start_date: NaiveDate, - end_date: NaiveDate, - target_weekday: Weekday, -) -> Vec { - let mut result = Vec::new(); - - // Find the first matching weekday on or after the start date. - let mut current = move_to_weekday_on_or_after(start_date, target_weekday); - - // Step through each week until exceeding the end date. - while current <= end_date { - // Only include if still a weekday. - if is_weekday(current) { - result.push(current); - } - current = current - .checked_add_signed(Duration::days(7)) - .expect("Date overflow when advancing by one week"); - } - - result -} - -/// Returns either the first or last business day of each month in the range. -fn collect_monthly( - start_date: NaiveDate, - end_date: NaiveDate, - want_first_day: bool, -) -> Vec { - let mut result = Vec::new(); - let mut year = start_date.year(); - let mut month = start_date.month(); - - // Advance (year, month) by one month. - let next_month = |(yr, mo): (i32, u32)| { - if mo == 12 { (yr + 1, 1) } else { (yr, mo + 1) } - }; - - // Iterate months from the start date until past the end date. - loop { - // Determine the candidate business date for this month. - let candidate = if want_first_day { - first_business_day_of_month(year, month) - } else { - last_business_day_of_month(year, month) - }; - - // Stop if the candidate is beyond the allowed range. - if candidate > end_date { - break; - } - - // Include candidate if it falls within [start_date, end_date]. - if candidate >= start_date && is_weekday(candidate) { - result.push(candidate); - } - - // If we've processed the end date's month, terminate. - if year > end_date.year() || (year == end_date.year() && month >= end_date.month()) { - break; - } - - // Move to the next month. - let (ny, nm) = next_month((year, month)); - year = ny; - month = nm; - - // Safety guard against unexpected infinite loops. - if year > end_date.year() + 1 { - break; - } - } - - result -} - -/// Return either the first or last business day in each quarter of the range. -fn collect_quarterly( - start_date: NaiveDate, - end_date: NaiveDate, - want_first_day: bool, -) -> Vec { - let mut result = Vec::new(); - - let mut year = start_date.year(); - // Start from the quarter containing the start date. - let mut q = month_to_quarter(start_date.month()); - - // Iterate quarter by quarter until we pass the end date. - loop { - // Compute the candidate date (first or last business day) for the current quarter. - // Use _opt and expect(), expecting valid quarter/year combinations. - let candidate = if want_first_day { - first_business_day_of_quarter(year, q) - } else { - last_business_day_of_quarter(year, q) - }; - - // If the candidate is after the end date, we've gone past the range, so stop. - if candidate > end_date { - break; - } - - // If the candidate is within the specified range [start_date, end_date], add it. - if candidate >= start_date { - // Ensure it's actually a weekday (should be, but adds safety) - if is_weekday(candidate) { - result.push(candidate); - } - } - // Note: We don't break if candidate < start_date because a later quarter - // might be within the range. - - // Check if the current quarter is the last one we should process - let end_q = month_to_quarter(end_date.month()); - if year > end_date.year() || (year == end_date.year() && q >= end_q) { - break; // Stop after processing the end_date's quarter - } - - // Advance to the next quarter. - if q == 4 { - year += 1; - q = 1; - } else { - q += 1; - } - - // Safety break - if year > end_date.year() + 1 { - break; - } - } - - result -} - -/// Return either the first or last business day in each year of the range. -fn collect_yearly( - start_date: NaiveDate, - end_date: NaiveDate, - want_first_day: bool, -) -> Vec { - let mut result = Vec::new(); - // Start from the year of the start date. - let mut year = start_date.year(); - - // Iterate year by year until we pass the end date's year. - while year <= end_date.year() { - // Compute the candidate date (first or last business day) for the current year. - // Use _opt and expect(), expecting valid year. - let candidate = if want_first_day { - first_business_day_of_year(year) - } else { - last_business_day_of_year(year) - }; - - // If the candidate is within the specified range [start_date, end_date], add it. - if candidate >= start_date && candidate <= end_date { - // Ensure it's actually a weekday (should be, but adds safety) - if is_weekday(candidate) { - result.push(candidate); - } - } else if want_first_day && candidate > end_date { - // Optimization: If the *first* bday of the year is already past end_date, - // no subsequent year's first bday will be in range. - // Similar logic applies for last bday if candidate > end_date, but it's less likely to trigger early. - break; - } - // Note: We don't break if candidate < start_date because a later year's candidate - // might be within the range (e.g. start_date 2023-12-15, YE freq, candidate for 2023 is 2023-12-29 (ok), - // candidate for 2024 is 2024-12-31 (could be ok)). - - year += 1; - } - result -} - -/* ---------------------- Core Date Utility Functions (Internal) ---------------------- */ - -/// Checks if a given date is a weekday (Monday-Friday). -fn is_weekday(date: NaiveDate) -> bool { - !matches!(date.weekday(), Weekday::Sat | Weekday::Sun) -} - -/// Given a date and a `target_weekday`, returns the date that is the first -/// `target_weekday` on or after the given date. -fn move_to_weekday_on_or_after(date: NaiveDate, target: Weekday) -> NaiveDate { - let mut current = date; - while current.weekday() != target { - // Use succ_opt() and expect(), assuming valid date and no overflow - current = current - .succ_opt() - .expect("date overflow moving to next weekday"); - } - current -} - -/// Return the earliest business day of the given (year, month). -fn first_business_day_of_month(year: i32, month: u32) -> NaiveDate { - // Start with the 1st of the month. Use _opt and expect(), assuming valid Y/M. - let mut d = NaiveDate::from_ymd_opt(year, month, 1).expect("invalid year-month combination"); - // If it's Sat/Sun, move forward until we find a weekday. - while !is_weekday(d) { - // Use succ_opt() and expect(), assuming valid date and no overflow. - d = d.succ_opt().expect("date overflow finding first bday"); - } - d -} - -/// Returns the last business day (Monday-Friday) of the specified month. -/// -/// Calculates the number of days in the month, then steps backward from the -/// month's last day until a weekday is found. -/// -/// # Panics -/// Panics if date construction or predecessor operations underflow. -fn last_business_day_of_month(year: i32, month: u32) -> NaiveDate { - let last_dom = days_in_month(year, month); - let mut d = - NaiveDate::from_ymd_opt(year, month, last_dom).expect("Invalid year-month-day combination"); - while !is_weekday(d) { - d = d - .pred_opt() - .expect("Date underflow finding last business day"); - } - d -} - -/// Returns the number of days in the specified month, correctly handling leap years. -/// -/// Determines the first day of the next month and subtracts one day. -/// -/// # Panics -/// Panics if date construction or predecessor operations underflow. -fn days_in_month(year: i32, month: u32) -> u32 { - let (ny, nm) = if month == 12 { - (year + 1, 1) - } else { - (year, month + 1) - }; - let first_of_next = - NaiveDate::from_ymd_opt(ny, nm, 1).expect("Invalid next year-month combination"); - let last_of_this = first_of_next - .pred_opt() - .expect("Date underflow computing last day of month"); - last_of_this.day() -} - -/// Maps a month (1-12) to its corresponding quarter (1-4). -/// -/// # Panics -/// Panics if `m` is outside the range 1-12. -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), - } -} - -/// Returns the starting month (1, 4, 7, or 10) for the given quarter (1-4). -/// -/// # Panics -/// Panics if `quarter` is not in 1-4. -fn quarter_start_month(quarter: u32) -> u32 { - match quarter { - 1 => 1, - 2 => 4, - 3 => 7, - 4 => 10, - _ => panic!("Invalid quarter: {}", quarter), - } -} - -/// Returns the first business day (Monday-Friday) of the specified quarter. -/// -/// Delegates to `first_business_day_of_month` using the quarter's start month. -fn first_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate { - let month = quarter_start_month(quarter); - first_business_day_of_month(year, month) -} - -/// Returns the last business day (Monday-Friday) of the specified quarter. -/// -/// Determines the quarter's final month and delegates to -/// `last_business_day_of_month`. -fn last_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate { - let last_month = quarter * 3; - last_business_day_of_month(year, last_month) -} - -/// Returns the first business day (Monday-Friday) of the specified year. -/// -/// Starts at January 1st and advances to the next weekday if needed. -/// -/// # Panics -/// Panics if date construction or successor operations overflow. -fn first_business_day_of_year(year: i32) -> NaiveDate { - let mut d = NaiveDate::from_ymd_opt(year, 1, 1).expect("Invalid year for January 1st"); - while !is_weekday(d) { - d = d - .succ_opt() - .expect("Date overflow finding first business day of year"); - } - d -} - -/// Returns the last business day (Monday-Friday) of the specified year. -/// -/// Starts at December 31st and moves backward to the previous weekday if needed. -/// -/// # Panics -/// Panics if date construction or predecessor operations underflow. -fn last_business_day_of_year(year: i32) -> NaiveDate { - let mut d = NaiveDate::from_ymd_opt(year, 12, 31).expect("Invalid year for December 31st"); - while !is_weekday(d) { - d = d - .pred_opt() - .expect("Date underflow finding last business day of year"); - } - d -} - -/// Finds the first valid business date on or after `start_date` according to `freq`. -/// -/// This may advance across days, weeks, months, quarters, or years depending on `freq`. -/// -/// # Panics -/// Panics on extreme date overflows or underflows. -fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> NaiveDate { - match freq { - BDateFreq::Daily => { - let mut d = start_date; - while !is_weekday(d) { - d = d - .succ_opt() - .expect("Date overflow finding first daily date"); - } - d - } - BDateFreq::WeeklyMonday => move_to_weekday_on_or_after(start_date, Weekday::Mon), - BDateFreq::WeeklyFriday => move_to_weekday_on_or_after(start_date, Weekday::Fri), - BDateFreq::MonthStart => { - let mut candidate = first_business_day_of_month(start_date.year(), start_date.month()); - if candidate < start_date { - let (ny, nm) = if start_date.month() == 12 { - (start_date.year() + 1, 1) - } else { - (start_date.year(), start_date.month() + 1) - }; - candidate = first_business_day_of_month(ny, nm); - } - candidate - } - BDateFreq::MonthEnd => { - let mut candidate = last_business_day_of_month(start_date.year(), start_date.month()); - if candidate < start_date { - let (ny, nm) = if start_date.month() == 12 { - (start_date.year() + 1, 1) - } else { - (start_date.year(), start_date.month() + 1) - }; - candidate = last_business_day_of_month(ny, nm); - } - candidate - } - BDateFreq::QuarterStart => { - let current_q = month_to_quarter(start_date.month()); - let mut candidate = first_business_day_of_quarter(start_date.year(), current_q); - if candidate < start_date { - let (ny, nq) = if current_q == 4 { - (start_date.year() + 1, 1) - } else { - (start_date.year(), current_q + 1) - }; - candidate = first_business_day_of_quarter(ny, nq); - } - candidate - } - BDateFreq::QuarterEnd => { - let current_q = month_to_quarter(start_date.month()); - let mut candidate = last_business_day_of_quarter(start_date.year(), current_q); - if candidate < start_date { - let (ny, nq) = if current_q == 4 { - (start_date.year() + 1, 1) - } else { - (start_date.year(), current_q + 1) - }; - candidate = last_business_day_of_quarter(ny, nq); - } - candidate - } - BDateFreq::YearStart => { - let mut candidate = first_business_day_of_year(start_date.year()); - if candidate < start_date { - candidate = first_business_day_of_year(start_date.year() + 1); - } - candidate - } - BDateFreq::YearEnd => { - let mut candidate = last_business_day_of_year(start_date.year()); - if candidate < start_date { - candidate = last_business_day_of_year(start_date.year() + 1); - } - candidate - } - } -} - -/// Finds the next business date after `current_date` according to `freq`. -/// -/// Assumes `current_date` was previously generated. Advances by days, weeks, -/// months, quarters, or years as specified. -/// -/// # Panics -/// Panics on extreme date overflows or underflows. -fn find_next_bdate(current_date: NaiveDate, freq: BDateFreq) -> NaiveDate { - match freq { - BDateFreq::Daily => { - let mut next_day = current_date - .succ_opt() - .expect("Date overflow finding next daily date"); - while !is_weekday(next_day) { - next_day = next_day - .succ_opt() - .expect("Date overflow finding next daily weekday"); - } - next_day - } - BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => current_date - .checked_add_signed(Duration::days(7)) - .expect("Date overflow adding one week"), - BDateFreq::MonthStart => { - let (ny, nm) = if current_date.month() == 12 { - (current_date.year() + 1, 1) - } else { - (current_date.year(), current_date.month() + 1) - }; - first_business_day_of_month(ny, nm) - } - BDateFreq::MonthEnd => { - let (ny, nm) = if current_date.month() == 12 { - (current_date.year() + 1, 1) - } else { - (current_date.year(), current_date.month() + 1) - }; - last_business_day_of_month(ny, nm) - } - BDateFreq::QuarterStart => { - let current_q = month_to_quarter(current_date.month()); - let (ny, nq) = if current_q == 4 { - (current_date.year() + 1, 1) - } else { - (current_date.year(), current_q + 1) - }; - first_business_day_of_quarter(ny, nq) - } - BDateFreq::QuarterEnd => { - let current_q = month_to_quarter(current_date.month()); - let (ny, nq) = if current_q == 4 { - (current_date.year() + 1, 1) - } else { - (current_date.year(), current_q + 1) - }; - last_business_day_of_quarter(ny, nq) - } - BDateFreq::YearStart => first_business_day_of_year(current_date.year() + 1), - BDateFreq::YearEnd => last_business_day_of_year(current_date.year() + 1), - } -} - // --- Example Usage and Tests --- #[cfg(test)] @@ -1106,78 +560,78 @@ mod tests { NaiveDate::from_ymd_opt(year, month, day).expect("Invalid date in test setup") } - // --- BDateFreq Tests --- + // --- DateFreq Tests --- #[test] - fn test_bdatefreq_from_str() -> Result<(), Box> { - assert_eq!(BDateFreq::from_str("D")?, BDateFreq::Daily); - assert_eq!("D".parse::()?, BDateFreq::Daily); // Test FromStr impl - assert_eq!(BDateFreq::from_str("W")?, BDateFreq::WeeklyMonday); - assert_eq!(BDateFreq::from_str("M")?, BDateFreq::MonthStart); - assert_eq!(BDateFreq::from_str("Q")?, BDateFreq::QuarterStart); + fn test_date_freq_from_str() -> Result<(), Box> { + assert_eq!(DateFreq::from_str("D")?, DateFreq::Daily); + assert_eq!("D".parse::()?, DateFreq::Daily); // Test FromStr impl + assert_eq!(DateFreq::from_str("W")?, DateFreq::WeeklyMonday); + assert_eq!(DateFreq::from_str("M")?, DateFreq::MonthStart); + assert_eq!(DateFreq::from_str("Q")?, DateFreq::QuarterStart); // Test YearStart codes and aliases (Y, A, AS, YS) - assert_eq!(BDateFreq::from_str("Y")?, BDateFreq::YearStart); - assert_eq!(BDateFreq::from_str("A")?, BDateFreq::YearStart); - assert_eq!(BDateFreq::from_str("AS")?, BDateFreq::YearStart); - assert_eq!(BDateFreq::from_str("YS")?, BDateFreq::YearStart); - assert_eq!("Y".parse::()?, BDateFreq::YearStart); // Test FromStr impl + 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!("Y".parse::()?, DateFreq::YearStart); // Test FromStr impl - assert_eq!(BDateFreq::from_str("ME")?, BDateFreq::MonthEnd); - assert_eq!(BDateFreq::from_str("QE")?, BDateFreq::QuarterEnd); - assert_eq!(BDateFreq::from_str("WF")?, BDateFreq::WeeklyFriday); - assert_eq!("WF".parse::()?, BDateFreq::WeeklyFriday); // Test FromStr impl + 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::WeeklyFriday); // Test FromStr impl // Test YearEnd codes and aliases (YE, AE) - assert_eq!(BDateFreq::from_str("YE")?, BDateFreq::YearEnd); - assert_eq!(BDateFreq::from_str("AE")?, BDateFreq::YearEnd); + assert_eq!(DateFreq::from_str("YE")?, DateFreq::YearEnd); + assert_eq!(DateFreq::from_str("AE")?, DateFreq::YearEnd); // Test aliases for other frequencies - assert_eq!(BDateFreq::from_str("WS")?, BDateFreq::WeeklyMonday); - assert_eq!(BDateFreq::from_str("MS")?, BDateFreq::MonthStart); - assert_eq!(BDateFreq::from_str("QS")?, BDateFreq::QuarterStart); + assert_eq!(DateFreq::from_str("WS")?, DateFreq::WeeklyMonday); + assert_eq!(DateFreq::from_str("MS")?, DateFreq::MonthStart); + assert_eq!(DateFreq::from_str("QS")?, DateFreq::QuarterStart); // Test invalid string - assert!(BDateFreq::from_str("INVALID").is_err()); - assert!("INVALID".parse::().is_err()); // Test FromStr impl - let err = BDateFreq::from_str("INVALID").unwrap_err(); + assert!(DateFreq::from_str("INVALID").is_err()); + assert!("INVALID".parse::().is_err()); // Test FromStr impl + let err = DateFreq::from_str("INVALID").unwrap_err(); assert_eq!(err.to_string(), "Invalid frequency specified: INVALID"); Ok(()) } #[test] - fn test_bdatefreq_to_string() { - assert_eq!(BDateFreq::Daily.to_string(), "D"); - assert_eq!(BDateFreq::WeeklyMonday.to_string(), "W"); - assert_eq!(BDateFreq::MonthStart.to_string(), "M"); - assert_eq!(BDateFreq::QuarterStart.to_string(), "Q"); - assert_eq!(BDateFreq::YearStart.to_string(), "Y"); // Assert "Y" - assert_eq!(BDateFreq::MonthEnd.to_string(), "ME"); - assert_eq!(BDateFreq::QuarterEnd.to_string(), "QE"); - assert_eq!(BDateFreq::WeeklyFriday.to_string(), "WF"); - assert_eq!(BDateFreq::YearEnd.to_string(), "YE"); + fn test_date_freq_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 "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_bdatefreq_from_string() -> Result<(), Box> { - assert_eq!(BDateFreq::from_string("D".to_string())?, BDateFreq::Daily); - assert!(BDateFreq::from_string("INVALID".to_string()).is_err()); + fn test_date_freq_from_string() -> Result<(), Box> { + assert_eq!(DateFreq::from_string("D".to_string())?, DateFreq::Daily); + assert!(DateFreq::from_string("INVALID".to_string()).is_err()); Ok(()) } #[test] - fn test_bdatefreq_agg_type() { - assert_eq!(BDateFreq::Daily.agg_type(), AggregationType::Start); - assert_eq!(BDateFreq::WeeklyMonday.agg_type(), AggregationType::Start); - assert_eq!(BDateFreq::MonthStart.agg_type(), AggregationType::Start); - assert_eq!(BDateFreq::QuarterStart.agg_type(), AggregationType::Start); - assert_eq!(BDateFreq::YearStart.agg_type(), AggregationType::Start); + fn test_date_freq_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!(BDateFreq::WeeklyFriday.agg_type(), AggregationType::End); - assert_eq!(BDateFreq::MonthEnd.agg_type(), AggregationType::End); - assert_eq!(BDateFreq::QuarterEnd.agg_type(), AggregationType::End); - assert_eq!(BDateFreq::YearEnd.agg_type(), AggregationType::End); + 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); } // --- BDatesList Property Tests --- @@ -1186,7 +640,7 @@ mod tests { fn test_bdates_list_properties_new() -> Result<(), Box> { let start_str = "2023-01-01".to_string(); let end_str = "2023-12-31".to_string(); - let freq = BDateFreq::QuarterEnd; + let freq = DateFreq::QuarterEnd; let dates_list = BDatesList::new(start_str.clone(), end_str.clone(), freq); // check start_date_str @@ -1208,7 +662,7 @@ mod tests { #[test] fn test_bdates_list_properties_from_n_periods() -> Result<(), Box> { let start_str = "2023-01-01".to_string(); // Sunday - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 5; // Expect: Jan 2, 3, 4, 5, 6 let dates_list = BDatesList::from_n_periods(start_str.clone(), freq, n_periods)?; @@ -1244,7 +698,7 @@ mod tests { #[test] fn test_bdates_list_from_n_periods_zero_periods() { let start_str = "2023-01-01".to_string(); - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 0; let result = BDatesList::from_n_periods(start_str.clone(), freq, n_periods); assert!(result.is_err()); @@ -1257,17 +711,15 @@ mod tests { #[test] fn test_bdates_list_from_n_periods_invalid_start_date() { let start_str = "invalid-date".to_string(); - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 5; let result = BDatesList::from_n_periods(start_str.clone(), freq, n_periods); assert!(result.is_err()); // Error comes from NaiveDate::parse_from_str - assert!( - result - .unwrap_err() - .to_string() - .contains("input contains invalid characters") - ); + assert!(result + .unwrap_err() + .to_string() + .contains("input contains invalid characters")); } #[test] @@ -1275,7 +727,7 @@ mod tests { let dates_list_start_invalid = BDatesList::new( "invalid-date".to_string(), "2023-12-31".to_string(), - BDateFreq::Daily, + DateFreq::Daily, ); assert!(dates_list_start_invalid.list().is_err()); assert!(dates_list_start_invalid.count().is_err()); @@ -1286,7 +738,7 @@ mod tests { let dates_list_end_invalid = BDatesList::new( "2023-01-01".to_string(), "invalid-date".to_string(), - BDateFreq::Daily, + DateFreq::Daily, ); assert!(dates_list_end_invalid.list().is_err()); assert!(dates_list_end_invalid.count().is_err()); @@ -1303,7 +755,7 @@ mod tests { let dates_list = BDatesList::new( "2023-01-01".to_string(), "2023-12-31".to_string(), - BDateFreq::QuarterEnd, + DateFreq::QuarterEnd, ); let list = dates_list.list()?; @@ -1328,7 +780,7 @@ mod tests { let dates_list = BDatesList::new( "2023-10-30".to_string(), // Monday (Week 44) "2023-11-12".to_string(), // Sunday (Week 45 ends, Week 46 starts) - BDateFreq::WeeklyMonday, + DateFreq::WeeklyMonday, ); let list = dates_list.list()?; @@ -1348,7 +800,7 @@ mod tests { let dates_list = BDatesList::new( "2023-11-01".to_string(), // Wednesday "2023-11-05".to_string(), // Sunday - BDateFreq::Daily, + DateFreq::Daily, ); let list = dates_list.list()?; @@ -1368,7 +820,7 @@ mod tests { let dates_list = BDatesList::new( "2023-12-31".to_string(), "2023-01-01".to_string(), // End date before start date - BDateFreq::Daily, + DateFreq::Daily, ); let list = dates_list.list()?; assert!(list.is_empty()); @@ -1383,14 +835,14 @@ mod tests { let dates_list = BDatesList::new( "2023-01-01".to_string(), "2023-12-31".to_string(), - BDateFreq::MonthEnd, + DateFreq::MonthEnd, ); assert_eq!(dates_list.count()?, 12); // 12 month ends in 2023 let dates_list_weekly = BDatesList::new( "2023-11-01".to_string(), // Wed "2023-11-30".to_string(), // Thu - BDateFreq::WeeklyFriday, + DateFreq::WeeklyFriday, ); // Fridays in range: 2023-11-03, 2023-11-10, 2023-11-17, 2023-11-24 assert_eq!(dates_list_weekly.count()?, 4); @@ -1404,7 +856,7 @@ mod tests { let dates_list = BDatesList::new( "2023-06-01".to_string(), "2025-06-01".to_string(), - BDateFreq::YearStart, + DateFreq::YearStart, ); // Year starts >= 2023-06-01 and <= 2025-06-01: // 2023-01-02 (Mon, Jan 1st is Sun) -> Excluded (< 2023-06-01) @@ -1422,7 +874,7 @@ mod tests { let dates_list = BDatesList::new( "2023-11-15".to_string(), // Mid-Nov "2024-02-15".to_string(), // Mid-Feb - BDateFreq::MonthStart, + DateFreq::MonthStart, ); // Month starts >= 2023-11-15 and <= 2024-02-15: // 2023-11-01 (Wed) -> Excluded (< 2023-11-15) @@ -1445,7 +897,7 @@ mod tests { let dates_list = BDatesList::new( "2023-11-01".to_string(), // Wed (Week 44) "2023-11-14".to_string(), // Tue (Week 46 starts on Mon 13th) - BDateFreq::WeeklyFriday, + DateFreq::WeeklyFriday, ); // Fridays >= 2023-11-01 and <= 2023-11-14: // 2023-11-03 (Week 44) -> Included @@ -1468,7 +920,7 @@ mod tests { let dates_list = BDatesList::new( "2023-10-15".to_string(), // Mid-October "2024-01-15".to_string(), // Mid-January next year - BDateFreq::MonthEnd, + DateFreq::MonthEnd, ); let groups = dates_list.groups()?; @@ -1494,7 +946,7 @@ mod tests { let dates_list = BDatesList::new( "2023-11-01".to_string(), // Wed "2023-11-05".to_string(), // Sun - BDateFreq::Daily, + DateFreq::Daily, ); let groups = dates_list.groups()?; @@ -1515,7 +967,7 @@ mod tests { let dates_list = BDatesList::new( "2023-11-01".to_string(), // Wed (ISO Week 44) "2023-11-15".to_string(), // Wed (ISO Week 46) - BDateFreq::WeeklyFriday, + DateFreq::WeeklyFriday, ); let groups = dates_list.groups()?; @@ -1539,7 +991,7 @@ mod tests { let dates_list = BDatesList::new( "2023-08-01".to_string(), // Start date after Q3 2023 start business day "2024-05-01".to_string(), // End date after Q2 2024 start business day - BDateFreq::QuarterStart, + DateFreq::QuarterStart, ); let groups = dates_list.groups()?; @@ -1568,7 +1020,7 @@ mod tests { let dates_list = BDatesList::new( "2022-01-01".to_string(), "2024-03-31".to_string(), // End date is Q1 2024 - BDateFreq::YearEnd, + DateFreq::YearEnd, ); let groups = dates_list.groups()?; @@ -1594,7 +1046,7 @@ mod tests { let dates_list = BDatesList::new( "2023-12-31".to_string(), "2023-01-01".to_string(), // End date before start date - BDateFreq::Daily, + DateFreq::Daily, ); let groups = dates_list.groups()?; assert!(groups.is_empty()); @@ -1602,641 +1054,12 @@ mod tests { Ok(()) } - // --- Tests for internal helper functions --- - - #[test] - /// Tests the `is_weekday` function for all days of the week. - fn test_is_weekday() { - assert!(is_weekday(date(2023, 11, 6))); // Mon - assert!(is_weekday(date(2023, 11, 7))); // Tue - assert!(is_weekday(date(2023, 11, 8))); // Wed - assert!(is_weekday(date(2023, 11, 9))); // Thu - assert!(is_weekday(date(2023, 11, 10))); // Fri - assert!(!is_weekday(date(2023, 11, 11))); // Sat - assert!(!is_weekday(date(2023, 11, 12))); // Sun - } - - #[test] - /// Tests the `move_to_weekday_on_or_after` function. - fn test_move_to_weekday_on_or_after() { - // Already the target weekday - assert_eq!( - move_to_weekday_on_or_after(date(2023, 11, 6), Weekday::Mon), - date(2023, 11, 6) - ); - // Target weekday is later in the week - assert_eq!( - move_to_weekday_on_or_after(date(2023, 11, 8), Weekday::Fri), - date(2023, 11, 10) - ); - // Target weekday is next week - assert_eq!( - move_to_weekday_on_or_after(date(2023, 11, 11), Weekday::Mon), - date(2023, 11, 13) - ); // Sat to next Mon - assert_eq!( - move_to_weekday_on_or_after(date(2023, 11, 10), Weekday::Mon), - date(2023, 11, 13) - ); // Fri to next Mon - } - - #[test] - /// Tests `first_business_day_of_month` including weekend starts. - fn test_first_business_day_of_month() { - // Month starts on a weekday - assert_eq!(first_business_day_of_month(2023, 11), date(2023, 11, 1)); // Nov 1st 2023 is Wed - // Month starts on a Sunday, 1st business day is Monday - assert_eq!(first_business_day_of_month(2023, 10), date(2023, 10, 2)); // Oct 1st 2023 is Sun - // Month starts on a Saturday, 1st business day is Monday - assert_eq!(first_business_day_of_month(2022, 10), date(2022, 10, 3)); // Oct 1st 2022 is Sat - } - - #[test] - /// Tests `last_business_day_of_month` including weekend ends. - fn test_last_business_day_of_month() { - // Month ends on a weekday - assert_eq!(last_business_day_of_month(2023, 11), date(2023, 11, 30)); // Nov 30th 2023 is Thu - // Month ends on a Sunday, last business day is Friday - assert_eq!(last_business_day_of_month(2023, 12), date(2023, 12, 29)); // Dec 31st 2023 is Sun - // Month ends on a Saturday, last business day is Friday - assert_eq!(last_business_day_of_month(2022, 12), date(2022, 12, 30)); // Dec 31st 2022 is Sat - // Month ends on Friday - assert_eq!(last_business_day_of_month(2023, 3), date(2023, 3, 31)); // Mar 31st 2023 is Fri - } - - #[test] - /// Tests `days_in_month` including leap years and different month lengths. - fn test_days_in_month() { - assert_eq!(days_in_month(2023, 1), 31); // Jan (31) - assert_eq!(days_in_month(2023, 2), 28); // Feb (28, non-leap) - assert_eq!(days_in_month(2024, 2), 29); // Feb (29, leap) - assert_eq!(days_in_month(2023, 4), 30); // Apr (30) - assert_eq!(days_in_month(2023, 12), 31); // Dec (31) - } - - #[test] - /// Tests the `month_to_quarter` mapping. - fn test_month_to_quarter() { - assert_eq!(month_to_quarter(1), 1); - assert_eq!(month_to_quarter(2), 1); - assert_eq!(month_to_quarter(3), 1); - assert_eq!(month_to_quarter(4), 2); - assert_eq!(month_to_quarter(5), 2); - assert_eq!(month_to_quarter(6), 2); - assert_eq!(month_to_quarter(7), 3); - assert_eq!(month_to_quarter(8), 3); - assert_eq!(month_to_quarter(9), 3); - assert_eq!(month_to_quarter(10), 4); - assert_eq!(month_to_quarter(11), 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] - /// Tests `first_business_day_of_quarter` including weekend starts. - fn test_first_business_day_of_quarter() { - // Q1 2023: Jan 1st 2023 is Sun, 1st bday is Mon Jan 2nd - assert_eq!(first_business_day_of_quarter(2023, 1), date(2023, 1, 2)); - // Q2 2023: Apr 1st 2023 is Sat, 1st bday is Mon Apr 3rd - assert_eq!(first_business_day_of_quarter(2023, 2), date(2023, 4, 3)); - // Q3 2023: Jul 1st 2023 is Sat, 1st bday is Mon Jul 3rd - assert_eq!(first_business_day_of_quarter(2023, 3), date(2023, 7, 3)); - // Q4 2023: Oct 1st 2023 is Sun, 1st bday is Mon Oct 2nd - assert_eq!(first_business_day_of_quarter(2023, 4), date(2023, 10, 2)); - // Q1 2024: Jan 1st 2024 is Mon, 1st bday is Mon Jan 1st - assert_eq!(first_business_day_of_quarter(2024, 1), date(2024, 1, 1)); - } - - #[test] - /// Tests `last_business_day_of_quarter` including weekend ends. - fn test_last_business_day_of_quarter() { - // Q1 2023: Ends Mar 31st (Fri), last bday is Mar 31st - assert_eq!(last_business_day_of_quarter(2023, 1), date(2023, 3, 31)); - // Q2 2023: Ends Jun 30th (Fri), last bday is Jun 30th - assert_eq!(last_business_day_of_quarter(2023, 2), date(2023, 6, 30)); - // Q3 2023: Ends Sep 30th (Sat), last bday is Sep 29th (Fri) - assert_eq!(last_business_day_of_quarter(2023, 3), date(2023, 9, 29)); - // Q4 2023: Ends Dec 31st (Sun), last bday is Dec 29th (Fri) - assert_eq!(last_business_day_of_quarter(2023, 4), date(2023, 12, 29)); - } - - #[test] - /// Tests `first_business_day_of_year` including weekend starts. - fn test_first_business_day_of_year() { - // 2023: Jan 1st is Sun, 1st bday is Jan 2nd (Mon) - assert_eq!(first_business_day_of_year(2023), date(2023, 1, 2)); - // 2024: Jan 1st is Mon, 1st bday is Jan 1st (Mon) - assert_eq!(first_business_day_of_year(2024), date(2024, 1, 1)); - // 2022: Jan 1st is Sat, 1st bday is Jan 3rd (Mon) - assert_eq!(first_business_day_of_year(2022), date(2022, 1, 3)); - } - - #[test] - /// Tests `last_business_day_of_year` including weekend ends. - fn test_last_business_day_of_year() { - // 2023: Dec 31st is Sun, last bday is Dec 29th (Fri) - assert_eq!(last_business_day_of_year(2023), date(2023, 12, 29)); - // 2024: Dec 31st is Tue, last bday is Dec 31st (Tue) - assert_eq!(last_business_day_of_year(2024), date(2024, 12, 31)); - // 2022: Dec 31st is Sat, last bday is Dec 30th (Fri) - assert_eq!(last_business_day_of_year(2022), date(2022, 12, 30)); - } - - // Test `collect_daily` edge cases - #[test] - fn test_collect_daily_single_day_range() { - // Single weekday - let start = date(2023, 11, 8); // Wed - assert_eq!(collect_daily(start, start), vec![start]); - // Single weekend day - should be empty - let start = date(2023, 11, 11); // Sat - assert_eq!(collect_daily(start, start), vec![]); - } - - #[test] - fn test_collect_daily_range_spanning_weekend() { - let start = date(2023, 11, 10); // Fri - let end = date(2023, 11, 13); // Mon - // Fri, Sat(skipped), Sun(skipped), Mon - assert_eq!( - collect_daily(start, end), - vec![date(2023, 11, 10), date(2023, 11, 13)] - ); - } - - // Test `collect_weekly` edge cases - #[test] - fn test_collect_weekly_start_is_target() { - let start = date(2023, 11, 13); // Mon - let end = date(2023, 11, 20); // Mon - // Start date is already the target weekday - assert_eq!( - collect_weekly(start, end, Weekday::Mon), - vec![date(2023, 11, 13), date(2023, 11, 20)] - ); - } - - #[test] - fn test_collect_weekly_end_before_target() { - let start = date(2023, 11, 13); // Mon - let end = date(2023, 11, 16); // Thu - // Target Friday is after the end date - assert_eq!(collect_weekly(start, end, Weekday::Fri), vec![]); - } - - #[test] - fn test_collect_weekly_single_week() { - let start = date(2023, 11, 8); // Wed - let end = date(2023, 11, 14); // Tue - // Only one Monday (Nov 13) and one Friday (Nov 10) in this range - assert_eq!( - collect_weekly(start, end, Weekday::Mon), - vec![date(2023, 11, 13)] - ); - assert_eq!( - collect_weekly(start, end, Weekday::Fri), - vec![date(2023, 11, 10)] - ); - } - - // Test `collect_monthly` edge cases - #[test] - fn test_collect_monthly_range_starts_mid_month_ends_mid_month() { - let start = date(2023, 10, 15); // Mid Oct - let end = date(2024, 1, 15); // Mid Jan - // Month starts >= start_date AND <= end_date: Nov 2023, Dec 2023, Jan 2024 - assert_eq!( - collect_monthly(start, end, true), - vec![date(2023, 11, 1), date(2023, 12, 1), date(2024, 1, 1)] - ); - // Month ends >= start_date AND <= end_date: Oct 2023, Nov 2023, Dec 2023 - // Last business day of Oct 2023 is Oct 31st, which is after Oct 15th start. - // Last business day of Jan 2024 is Jan 31st, which is after Jan 15th end. - assert_eq!( - collect_monthly(start, end, false), - vec![date(2023, 10, 31), date(2023, 11, 30), date(2023, 12, 29)] - ); - } - - #[test] - fn test_collect_monthly_single_month() { - let start = date(2023, 11, 1); // Nov 1st (Wed) - let end = date(2023, 11, 30); // Nov 30th (Thu) - // Range covers exactly one month, start and end dates are the start/end business days - assert_eq!(collect_monthly(start, end, true), vec![date(2023, 11, 1)]); - assert_eq!(collect_monthly(start, end, false), vec![date(2023, 11, 30)]); - } - - #[test] - fn test_collect_monthly_range_short() { - let start = date(2023, 11, 15); // Mid Nov - let end = date(2023, 11, 20); // Mid Nov - // No month starts or ends are within this short range. - assert_eq!(collect_monthly(start, end, true), vec![]); - assert_eq!(collect_monthly(start, end, false), vec![]); - } - - #[test] - fn test_collect_monthly_full_year_start() { - let start = date(2023, 1, 1); - let end = date(2023, 12, 31); - let expected: Vec = (1..=12) - .map(|m| first_business_day_of_month(2023, m)) - .collect(); - assert_eq!(collect_monthly(start, end, true), expected); - } - - #[test] - fn test_collect_monthly_full_year_end() { - let start = date(2023, 1, 1); - let end = date(2023, 12, 31); - let expected: Vec = (1..=12) - .map(|m| last_business_day_of_month(2023, m)) - .collect(); - assert_eq!(collect_monthly(start, end, false), expected); - } - - // Test `collect_quarterly` edge cases - #[test] - fn test_collect_quarterly_range_starts_mid_quarter_ends_mid_quarter() { - let start = date(2023, 8, 15); // Mid Q3 2023 - let end = date(2024, 2, 15); // Mid Q1 2024 - // Q starts >= start_date AND <= end_date: Q4 2023, Q1 2024 - // Q3 2023 start bday (Jul 3rd) < start_date (Aug 15th) -> Excluded - // Q4 2023 start bday (Oct 2nd) >= start_date (Aug 15th) -> Included - // Q1 2024 start bday (Jan 1st) >= start_date (Aug 15th) AND <= end_date -> Included - // Q2 2024 start bday (Apr 1st) > end_date (Feb 15th) -> Excluded - assert_eq!( - collect_quarterly(start, end, true), - vec![date(2023, 10, 2), date(2024, 1, 1)] - ); - // Q ends >= start_date AND <= end_date: Q3 2023, Q4 2023 - // Q3 2023 end bday (Sep 29th) >= start_date (Aug 15th) -> Included - // Q4 2023 end bday (Dec 29th) >= start_date (Aug 15th) -> Included - // Q1 2024 end bday (Mar 29th) > end_date (Feb 15th) -> Excluded - assert_eq!( - collect_quarterly(start, end, false), - vec![date(2023, 9, 29), date(2023, 12, 29)] - ); - } - - #[test] - fn test_collect_quarterly_single_quarter() { - let start = date(2023, 4, 3); // Apr 3rd (Q2 start bday) - let end = date(2023, 6, 30); // Jun 30th (Q2 end bday) - // Range covers exactly one quarter - assert_eq!(collect_quarterly(start, end, true), vec![date(2023, 4, 3)]); - assert_eq!( - collect_quarterly(start, end, false), - vec![date(2023, 6, 30)] - ); - } - - #[test] - fn test_collect_quarterly_range_short() { - let start = date(2023, 5, 15); // Mid Q2 - let end = date(2023, 6, 15); // Mid Q2 - // No quarter starts or ends are within this short range. - assert_eq!(collect_quarterly(start, end, true), vec![]); - assert_eq!(collect_quarterly(start, end, false), vec![]); - } - - #[test] - fn test_collect_quarterly_full_year_start() { - let start = date(2023, 1, 1); - let end = date(2023, 12, 31); - // Q1: Jan 2, Q2: Apr 3, Q3: Jul 3, Q4: Oct 2 - assert_eq!( - collect_quarterly(start, end, true), - vec![ - date(2023, 1, 2), - date(2023, 4, 3), - date(2023, 7, 3), - date(2023, 10, 2) - ] - ); - } - - #[test] - fn test_collect_quarterly_full_year_end() { - let start = date(2023, 1, 1); - let end = date(2023, 12, 31); - // Q1: Mar 31, Q2: Jun 30, Q3: Sep 29, Q4: Dec 29 - assert_eq!( - collect_quarterly(start, end, false), - vec![ - date(2023, 3, 31), - date(2023, 6, 30), - date(2023, 9, 29), - date(2023, 12, 29) - ] - ); - } - - // Test `collect_yearly` edge cases - #[test] - fn test_collect_yearly_range_starts_mid_year_ends_mid_year() -> Result<(), Box> { - let start = date(2023, 6, 1); // Mid 2023 - let end = date(2024, 6, 1); // Mid 2024 - // Year starts >= start_date AND <= end_date: 2024 - // 2023 start bday (Jan 2nd) < start_date (Jun 1st) -> Excluded - // 2024 start bday (Jan 1st) >= start_date (Jun 1st) AND <= end_date -> Included - // 2025 start bday (Jan 1st) > end_date (Jun 1st) -> Excluded - assert_eq!(collect_yearly(start, end, true), vec![date(2024, 1, 1)]); - // Year ends >= start_date AND <= end_date: 2023 - // 2023 end bday (Dec 29th) >= start_date (Jun 1st) -> Included - // 2024 end bday (Dec 31st) > end_date (Jun 1st) -> Excluded <-- Correction: Original thought was wrong - assert_eq!(collect_yearly(start, end, false), vec![date(2023, 12, 29)]); - Ok(()) - } - - #[test] - fn test_collect_yearly_single_year() { - let start = date(2024, 1, 1); // 2024 start bday - let end = date(2024, 12, 31); // 2024 end bday - // Range covers exactly one year - assert_eq!(collect_yearly(start, end, true), vec![date(2024, 1, 1)]); - assert_eq!(collect_yearly(start, end, false), vec![date(2024, 12, 31)]); - } - - #[test] - fn test_collect_yearly_range_short() { - let start = date(2023, 5, 15); // Mid 2023 - let end = date(2023, 6, 15); // Mid 2023 - // No year starts or ends are within this short range. - assert_eq!(collect_yearly(start, end, true), vec![]); - assert_eq!(collect_yearly(start, end, false), vec![]); - } - - #[test] - fn test_collect_yearly_full_years() { - let start = date(2022, 1, 1); - let end = date(2024, 12, 31); - // Year starts - assert_eq!( - collect_yearly(start, end, true), - vec![date(2022, 1, 3), date(2023, 1, 2), date(2024, 1, 1)] - ); - // Year ends - assert_eq!( - collect_yearly(start, end, false), - vec![date(2022, 12, 30), date(2023, 12, 29), date(2024, 12, 31)] - ); - } - - // --- Tests for Generator Helper Functions --- - - #[test] - fn test_find_first_bdate_on_or_after() { - // Daily - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 8), BDateFreq::Daily), - date(2023, 11, 8) - ); // Wed -> Wed - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 11), BDateFreq::Daily), - date(2023, 11, 13) - ); // Sat -> Mon - - // Weekly Mon - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 8), BDateFreq::WeeklyMonday), - date(2023, 11, 13) - ); // Wed -> Next Mon - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 13), BDateFreq::WeeklyMonday), - date(2023, 11, 13) - ); // Mon -> Mon - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 12), BDateFreq::WeeklyMonday), - date(2023, 11, 13) - ); // Sun -> Mon - - // Weekly Fri - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 8), BDateFreq::WeeklyFriday), - date(2023, 11, 10) - ); // Wed -> Fri - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 10), BDateFreq::WeeklyFriday), - date(2023, 11, 10) - ); // Fri -> Fri - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 11), BDateFreq::WeeklyFriday), - date(2023, 11, 17) - ); // Sat -> Next Fri - - // Month Start - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 1), BDateFreq::MonthStart), - date(2023, 11, 1) - ); // Nov 1 (Wed) -> Nov 1 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 10, 15), BDateFreq::MonthStart), - date(2023, 11, 1) - ); // Mid Oct -> Nov 1 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 15), BDateFreq::MonthStart), - date(2024, 1, 1) - ); // Mid Dec -> Jan 1 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 10, 1), BDateFreq::MonthStart), - date(2023, 10, 2) - ); // Oct 1 (Sun) -> Oct 2 - - // Month End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 30), BDateFreq::MonthEnd), - date(2023, 11, 30) - ); // Nov 30 (Thu) -> Nov 30 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 15), BDateFreq::MonthEnd), - date(2023, 11, 30) - ); // Mid Nov -> Nov 30 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 30), BDateFreq::MonthEnd), - date(2024, 1, 31) - ); // Dec 30 (Sat) -> Jan 31 (Dec end was 29th, which is < 30th) - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 29), BDateFreq::MonthEnd), - date(2023, 12, 29) - ); // Dec 29 (Fri) -> Dec 29 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 9, 30), BDateFreq::MonthEnd), - date(2023, 10, 31) - ); // Sep 30 (Sat) -> Oct 31 (Sep end was 29th, < 30th) - - // Quarter Start - assert_eq!( - find_first_bdate_on_or_after(date(2023, 10, 2), BDateFreq::QuarterStart), - date(2023, 10, 2) - ); // Q4 Start (Mon) -> Q4 Start - assert_eq!( - find_first_bdate_on_or_after(date(2023, 8, 15), BDateFreq::QuarterStart), - date(2023, 10, 2) - ); // Mid Q3 -> Q4 Start - assert_eq!( - find_first_bdate_on_or_after(date(2023, 11, 15), BDateFreq::QuarterStart), - date(2024, 1, 1) - ); // Mid Q4 -> Q1 Start - assert_eq!( - find_first_bdate_on_or_after(date(2023, 1, 1), BDateFreq::QuarterStart), - date(2023, 1, 2) - ); // Jan 1 (Sun) -> Jan 2 (Mon) - - // Quarter End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 9, 29), BDateFreq::QuarterEnd), - date(2023, 9, 29) - ); // Q3 End (Fri) -> Q3 End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 8, 15), BDateFreq::QuarterEnd), - date(2023, 9, 29) - ); // Mid Q3 -> Q3 End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 10, 15), BDateFreq::QuarterEnd), - date(2023, 12, 29) - ); // Mid Q4 -> Q4 End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 30), BDateFreq::QuarterEnd), - date(2024, 3, 29) - ); // Dec 30 (Sat) -> Q1 End (Q4 end was 29th, < 30th) - - // Year Start - assert_eq!( - find_first_bdate_on_or_after(date(2024, 1, 1), BDateFreq::YearStart), - date(2024, 1, 1) - ); // Jan 1 (Mon) -> Jan 1 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 6, 15), BDateFreq::YearStart), - date(2024, 1, 1) - ); // Mid 2023 -> Jan 1 2024 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 1, 1), BDateFreq::YearStart), - date(2023, 1, 2) - ); // Jan 1 (Sun) -> Jan 2 - - // Year End - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 29), BDateFreq::YearEnd), - date(2023, 12, 29) - ); // Dec 29 (Fri) -> Dec 29 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 6, 15), BDateFreq::YearEnd), - date(2023, 12, 29) - ); // Mid 2023 -> Dec 29 2023 - assert_eq!( - find_first_bdate_on_or_after(date(2023, 12, 30), BDateFreq::YearEnd), - date(2024, 12, 31) - ); // Dec 30 (Sat) -> Dec 31 2024 (2023 end was 29th, < 30th) - } - - #[test] - fn test_find_next_bdate() { - // Daily - assert_eq!( - find_next_bdate(date(2023, 11, 8), BDateFreq::Daily), - date(2023, 11, 9) - ); // Wed -> Thu - assert_eq!( - find_next_bdate(date(2023, 11, 10), BDateFreq::Daily), - date(2023, 11, 13) - ); // Fri -> Mon - - // Weekly Mon - assert_eq!( - find_next_bdate(date(2023, 11, 13), BDateFreq::WeeklyMonday), - date(2023, 11, 20) - ); // Mon -> Next Mon - - // Weekly Fri - assert_eq!( - find_next_bdate(date(2023, 11, 10), BDateFreq::WeeklyFriday), - date(2023, 11, 17) - ); // Fri -> Next Fri - - // Month Start - assert_eq!( - find_next_bdate(date(2023, 11, 1), BDateFreq::MonthStart), - date(2023, 12, 1) - ); // Nov 1 -> Dec 1 - assert_eq!( - find_next_bdate(date(2023, 12, 1), BDateFreq::MonthStart), - date(2024, 1, 1) - ); // Dec 1 -> Jan 1 - - // Month End - assert_eq!( - find_next_bdate(date(2023, 10, 31), BDateFreq::MonthEnd), - date(2023, 11, 30) - ); // Oct 31 -> Nov 30 - assert_eq!( - find_next_bdate(date(2023, 11, 30), BDateFreq::MonthEnd), - date(2023, 12, 29) - ); // Nov 30 -> Dec 29 - assert_eq!( - find_next_bdate(date(2023, 12, 29), BDateFreq::MonthEnd), - date(2024, 1, 31) - ); // Dec 29 -> Jan 31 - - // Quarter Start - assert_eq!( - find_next_bdate(date(2023, 10, 2), BDateFreq::QuarterStart), - date(2024, 1, 1) - ); // Q4 Start -> Q1 Start - assert_eq!( - find_next_bdate(date(2024, 1, 1), BDateFreq::QuarterStart), - date(2024, 4, 1) - ); // Q1 Start -> Q2 Start - - // Quarter End - assert_eq!( - find_next_bdate(date(2023, 9, 29), BDateFreq::QuarterEnd), - date(2023, 12, 29) - ); // Q3 End -> Q4 End - assert_eq!( - find_next_bdate(date(2023, 12, 29), BDateFreq::QuarterEnd), - date(2024, 3, 29) - ); // Q4 End -> Q1 End (Mar 31 2024 is Sun) - - // Year Start - assert_eq!( - find_next_bdate(date(2023, 1, 2), BDateFreq::YearStart), - date(2024, 1, 1) - ); // 2023 Start -> 2024 Start - assert_eq!( - find_next_bdate(date(2024, 1, 1), BDateFreq::YearStart), - date(2025, 1, 1) - ); // 2024 Start -> 2025 Start - - // Year End - assert_eq!( - find_next_bdate(date(2022, 12, 30), BDateFreq::YearEnd), - date(2023, 12, 29) - ); // 2022 End -> 2023 End - assert_eq!( - find_next_bdate(date(2023, 12, 29), BDateFreq::YearEnd), - date(2024, 12, 31) - ); // 2023 End -> 2024 End - } - // --- Tests for BDatesGenerator --- #[test] fn test_generator_new_zero_periods() -> Result<(), Box> { let start_date = date(2023, 1, 1); - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 0; let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; assert_eq!(generator.next(), None); // Should be immediately exhausted @@ -2246,7 +1069,7 @@ mod tests { #[test] fn test_generator_daily() -> Result<(), Box> { let start_date = date(2023, 11, 10); // Friday - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 4; let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2269,7 +1092,7 @@ mod tests { #[test] fn test_generator_weekly_monday() -> Result<(), Box> { let start_date = date(2023, 11, 8); // Wednesday - let freq = BDateFreq::WeeklyMonday; + let freq = DateFreq::WeeklyMonday; let n_periods = 3; let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2284,7 +1107,7 @@ mod tests { #[test] fn test_generator_weekly_friday() -> Result<(), Box> { let start_date = date(2023, 11, 11); // Saturday - let freq = BDateFreq::WeeklyFriday; + let freq = DateFreq::WeeklyFriday; let n_periods = 3; let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2299,7 +1122,7 @@ mod tests { #[test] fn test_generator_month_start() -> Result<(), Box> { let start_date = date(2023, 10, 15); // Mid-Oct - let freq = BDateFreq::MonthStart; + let freq = DateFreq::MonthStart; let n_periods = 4; // Nov, Dec, Jan, Feb let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2315,7 +1138,7 @@ mod tests { #[test] fn test_generator_month_end() -> Result<(), Box> { let start_date = date(2023, 9, 30); // Sep 30 (Sat) - let freq = BDateFreq::MonthEnd; + let freq = DateFreq::MonthEnd; let n_periods = 4; // Oct, Nov, Dec, Jan let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2331,7 +1154,7 @@ mod tests { #[test] fn test_generator_quarter_start() -> Result<(), Box> { let start_date = date(2023, 8, 1); // Mid-Q3 - let freq = BDateFreq::QuarterStart; + let freq = DateFreq::QuarterStart; let n_periods = 3; // Q4'23, Q1'24, Q2'24 let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2346,7 +1169,7 @@ mod tests { #[test] fn test_generator_quarter_end() -> Result<(), Box> { let start_date = date(2023, 11, 1); // Mid-Q4 - let freq = BDateFreq::QuarterEnd; + let freq = DateFreq::QuarterEnd; let n_periods = 3; // Q4'23, Q1'24, Q2'24 let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2361,7 +1184,7 @@ mod tests { #[test] fn test_generator_year_start() -> Result<(), Box> { let start_date = date(2023, 1, 1); // Jan 1 (Sun) - let freq = BDateFreq::YearStart; + let freq = DateFreq::YearStart; let n_periods = 3; // 2023, 2024, 2025 let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2376,7 +1199,7 @@ mod tests { #[test] fn test_generator_year_end() -> Result<(), Box> { let start_date = date(2022, 12, 31); // Dec 31 (Sat) - let freq = BDateFreq::YearEnd; + let freq = DateFreq::YearEnd; let n_periods = 3; // 2023, 2024, 2025 let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?; @@ -2391,7 +1214,7 @@ mod tests { #[test] fn test_generator_collect() -> Result<(), Box> { let start_date = date(2023, 11, 10); // Friday - let freq = BDateFreq::Daily; + let freq = DateFreq::Daily; let n_periods = 4; let generator = BDatesGenerator::new(start_date, freq, n_periods)?; // Use non-mut binding for collect let dates: Vec = generator.collect();