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; /// Represents the frequency at which calendar dates should be generated. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DateFreq { Daily, // Every calendar day WeeklyMonday, // Every Monday WeeklyFriday, // Every Friday MonthStart, // First calendar day of the month MonthEnd, // Last calendar day of the month QuarterStart, // First calendar day of the quarter QuarterEnd, // Last calendar day of the quarter YearStart, // First calendar day of the year (Jan 1st) YearEnd, // Last calendar day of the year (Dec 31st) } /// 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, // Indicates picking the first calendar date in a group's period. End, // Indicates picking the last calendar day in a group's period. } impl DateFreq { /// Attempts to parse a frequency string into a `DateFreq` 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> { 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 { 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() } /// Determines whether the frequency represents a start-of-period or end-of-period aggregation. 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, } } } // Implement FromStr for DateFreq to allow parsing directly using `parse()` impl FromStr for DateFreq { type Err = Box; /// Attempts to parse a frequency string slice into a `DateFreq` enum. /// /// Supports various frequency codes and common aliases. /// /// | Code | Alias | Description | /// |------|---------|--------------------------| /// | D | | Daily (every day) | /// | W | WS | Weekly Monday | /// | M | MS | Month Start (1st) | /// | Q | QS | Quarter Start (1st) | /// | Y | A, AS, YS | Year Start (Jan 1st) | /// | ME | | Month End (Last day) | /// | QE | | Quarter End (Last day) | /// | WF | | Weekly Friday | /// | YE | AE | Year End (Dec 31st) | /// /// # 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" => 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) } } /// Represents a list of calendar dates generated between a start and end date /// at a specified frequency. Provides methods to retrieve the full list, /// count, or dates grouped by period. #[derive(Debug, Clone)] pub struct DatesList { start_date_str: String, end_date_str: String, freq: DateFreq, } // Helper enum to represent the key for grouping dates into periods. // Deriving traits for comparison and hashing allows using it as a HashMap key // and for sorting groups chronologically. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] enum GroupKey { Daily(NaiveDate), // Group by the specific date (for Daily frequency) Weekly(i32, u32), // Group by year and ISO week number Monthly(i32, u32), // Group by year and month (1-12) Quarterly(i32, u32), // Group by year and quarter (1-4) Yearly(i32), // Group by year } /// Represents a collection of calendar dates generated according to specific rules. /// /// It can be defined either by a start and end date range or by a start date /// and a fixed number of periods. It provides methods to retrieve the dates /// as a flat list, count them, or group them by their natural period /// (e.g., month, quarter). /// /// This struct handles all calendar dates, including weekends. /// /// ## Examples /// /// **1. Using `DatesList::new` (Start and End Date):** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// # use rustframe::utils::{DatesList, DateFreq}; // Assuming the crate/module is named 'dates' /// /// # fn main() -> Result<(), Box> { /// let start_date = "2023-11-01".to_string(); // Wednesday /// let end_date = "2023-11-07".to_string(); // Tuesday /// let freq = DateFreq::Daily; /// /// let dates_list = DatesList::new(start_date, end_date, freq); /// /// let expected_dates = vec![ /// NaiveDate::from_ymd_opt(2023, 11, 1).unwrap(), // Wed /// NaiveDate::from_ymd_opt(2023, 11, 2).unwrap(), // Thu /// NaiveDate::from_ymd_opt(2023, 11, 3).unwrap(), // Fri /// NaiveDate::from_ymd_opt(2023, 11, 4).unwrap(), // Sat /// NaiveDate::from_ymd_opt(2023, 11, 5).unwrap(), // Sun /// NaiveDate::from_ymd_opt(2023, 11, 6).unwrap(), // Mon /// NaiveDate::from_ymd_opt(2023, 11, 7).unwrap(), // Tue /// ]; /// /// assert_eq!(dates_list.list()?, expected_dates); /// assert_eq!(dates_list.count()?, 7); /// # Ok(()) /// # } /// ``` /// /// **2. Using `DatesList::from_n_periods` (Start Date and Count):** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// use rustframe::utils::{DatesList, DateFreq}; /// /// fn main() -> Result<(), Box> { /// let start_date = "2024-02-28".to_string(); // Wednesday /// let freq = DateFreq::WeeklyFriday; /// let n_periods = 3; /// /// let dates_list = DatesList::from_n_periods(start_date, freq, n_periods)?; /// /// // The first Friday on or after 2024-02-28 is Mar 1. /// // The next two Fridays are Mar 8 and Mar 15. /// let expected_dates = vec![ /// NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(), /// NaiveDate::from_ymd_opt(2024, 3, 8).unwrap(), /// NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(), /// ]; /// /// assert_eq!(dates_list.list()?, expected_dates); /// assert_eq!(dates_list.count()?, 3); /// assert_eq!(dates_list.start_date_str(), "2024-02-28"); // Keeps original start string /// assert_eq!(dates_list.end_date_str(), "2024-03-15"); // End date is the last generated date /// # Ok(()) /// # } /// ``` /// /// **3. Using `groups()`:** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// use rustframe::utils::{DatesList, DateFreq}; /// /// 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 = DateFreq::MonthEnd; // Find month-ends /// /// let dates_list = DatesList::new(start_date, end_date, freq); /// /// // Month ends >= Nov 20 and <= Dec 08: Nov 30 /// let groups = dates_list.groups()?; /// /// assert_eq!(groups.len(), 1); // Only November's end date falls in the range /// assert_eq!(groups[0], vec![NaiveDate::from_ymd_opt(2023, 11, 30).unwrap()]); // Nov 2023 group /// Ok(()) /// } /// ``` impl DatesList { /// Creates a new `DatesList` instance defined by a start and end date. /// /// # Arguments /// /// * `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: DateFreq) -> Self { DatesList { start_date_str, end_date_str, freq, } } /// Creates a new `DatesList` instance defined by a start date, frequency, /// and the number of periods (dates) to generate. /// /// This calculates the required dates using a `DatesGenerator` and determines /// the effective end date based on the last generated date. /// /// # Arguments /// /// * `start_date_str` - The start date as a string (e.g., "YYYY-MM-DD"). The first generated date will be on or after this date. /// * `freq` - The frequency for generating dates. /// * `n_periods` - The exact number of dates to generate according to the frequency. /// /// # Errors /// /// Returns an error if: /// * `start_date_str` cannot be parsed. /// * `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: DateFreq, n_periods: usize, ) -> Result> { 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")?; // Use the generator to find all the dates let generator = DatesGenerator::new(start_date, freq, n_periods)?; let dates: Vec = generator.collect(); // Should always have at least one date if n_periods > 0 and generator construction succeeded 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, // Keep the original start date string end_date_str, freq, }) } /// Returns the flat list of dates within the specified range and frequency. /// /// The list is guaranteed to be sorted chronologically. /// /// # Errors /// /// Returns an error if the start or end date strings cannot be parsed. pub fn list(&self) -> Result, Box> { // Delegate the core logic to the internal helper function get_dates_list_with_freq(&self.start_date_str, &self.end_date_str, self.freq) } /// Returns the count of dates within the specified range and frequency. /// /// # Errors /// /// Returns an error if the start or end date strings cannot be parsed (as it /// calls `list` internally). pub fn count(&self) -> Result> { self.list().map(|list| list.len()) } /// Returns a list of date lists, where each inner list contains dates /// belonging to the same period (determined by frequency). /// /// The outer list (groups) is sorted by period chronologically, and the /// inner lists (dates within groups) are also sorted chronologically. /// /// For `Daily` frequency, each date forms its own group. For `Weekly` /// frequencies, grouping is by ISO week number. For `Monthly`, `Quarterly`, /// and `Yearly` frequencies, grouping is by the respective period. /// /// # Errors /// /// Returns an error if the start or end date strings cannot be parsed. pub fn groups(&self) -> Result>, Box> { let dates = self.list()?; group_dates_helper(dates, self.freq) } /// Returns the start date parsed as a `NaiveDate`. /// /// # Errors /// /// Returns an error if the start date string is not in "YYYY-MM-DD" format. pub fn start_date(&self) -> Result> { NaiveDate::parse_from_str(&self.start_date_str, "%Y-%m-%d").map_err(|e| e.into()) } /// Returns the start date string. pub fn start_date_str(&self) -> &str { &self.start_date_str } /// Returns the end date parsed as a `NaiveDate`. /// /// # Errors /// /// Returns an error if the end date string is not in "YYYY-MM-DD" format. pub fn end_date(&self) -> Result> { NaiveDate::parse_from_str(&self.end_date_str, "%Y-%m-%d").map_err(|e| e.into()) } /// Returns the end date string. pub fn end_date_str(&self) -> &str { &self.end_date_str } /// Returns the frequency enum. pub fn freq(&self) -> DateFreq { self.freq } /// Returns the canonical string representation of the frequency. pub fn freq_str(&self) -> String { self.freq.to_string() } } /// An iterator that generates a sequence of calendar dates based on a start date, /// frequency, and a specified number of periods. /// /// This implements the `Iterator` trait, allowing generation of dates one by one. /// It's useful when you need to process dates lazily or only need a fixed number /// starting from a specific point, without necessarily defining an end date beforehand. /// # Examples /// /// **1. Basic Iteration (Month End):** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// use rustframe::utils::{DatesGenerator, DateFreq}; /// /// fn main() -> Result<(), Box> { /// let start = NaiveDate::from_ymd_opt(2023, 12, 28).unwrap(); // Thursday /// let freq = DateFreq::MonthEnd; /// let n_periods = 4; // Dec '23, Jan '24, Feb '24, Mar '24 /// /// let mut generator = DatesGenerator::new(start, freq, n_periods)?; /// /// // First month-end on or after 2023-12-28 is 2023-12-31 /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap())); /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 31).unwrap())); /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 2, 29).unwrap())); // Leap year /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 3, 31).unwrap())); /// assert_eq!(generator.next(), None); // Exhausted /// Ok(()) /// } /// ``` /// /// **2. Collecting into a Vec (Daily):** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// use rustframe::utils::{DatesGenerator, DateFreq}; /// /// fn main() -> Result<(), Box> { /// let start = NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(); // Monday /// let freq = DateFreq::Daily; /// let n_periods = 5; /// /// let generator = DatesGenerator::new(start, freq, n_periods)?; /// let dates: Vec = generator.collect(); /// /// let expected_dates = vec![ /// NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(), // Mon /// NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(), // Tue /// NaiveDate::from_ymd_opt(2024, 5, 1).unwrap(), // Wed /// NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(), // Thu /// NaiveDate::from_ymd_opt(2024, 5, 3).unwrap(), // Fri /// ]; /// /// assert_eq!(dates, expected_dates); /// Ok(()) /// } /// ``` /// /// **3. Starting on the Exact Day (Weekly Monday):** /// /// ```rust /// use chrono::NaiveDate; /// use std::error::Error; /// use rustframe::utils::{DatesGenerator, DateFreq}; /// /// fn main() -> Result<(), Box> { /// let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); // Monday /// let freq = DateFreq::WeeklyMonday; /// let n_periods = 3; /// /// let mut generator = DatesGenerator::new(start, freq, n_periods)?; /// /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())); /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 8).unwrap())); /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())); /// assert_eq!(generator.next(), None); /// Ok(()) /// } /// ``` #[derive(Debug, Clone)] pub struct DatesGenerator { pub freq: DateFreq, pub periods_remaining: usize, // Stores the *next* date to be yielded by the iterator. pub next_date_candidate: Option, } impl DatesGenerator { /// Creates a new `DatesGenerator`. /// /// It calculates the first valid date based on the `start_date` and `freq`, /// which will be the first item yielded by the iterator. /// /// # Arguments /// /// * `start_date` - The date from which to start searching for the first valid date. /// * `freq` - The frequency for generating dates. /// * `n_periods` - The total number of dates to generate. /// /// # Errors /// /// Returns an error if initial date calculation fails (e.g., due to overflow, though unlikely). pub fn new( start_date: NaiveDate, freq: DateFreq, n_periods: usize, ) -> Result> { let first_date = if n_periods > 0 { Some(find_first_date_on_or_after(start_date, freq)?) } else { None // No dates to generate if n_periods is 0 }; Ok(DatesGenerator { freq, periods_remaining: n_periods, next_date_candidate: first_date, }) } } impl Iterator for DatesGenerator { type Item = NaiveDate; /// Returns the next date in the sequence, or `None` if `n_periods` /// dates have already been generated. fn next(&mut self) -> Option { match self.next_date_candidate { Some(current_date) if self.periods_remaining > 0 => { // Prepare the *next* candidate for the subsequent call // We calculate the next date *before* decrementing periods_remaining // If find_next_date fails, we treat it as the end of the sequence. self.next_date_candidate = find_next_date(current_date, self.freq).ok(); // Decrement the count *after* potentially getting the next date self.periods_remaining -= 1; // Return the stored current date Some(current_date) } _ => { // Exhausted or no initial date self.periods_remaining = 0; // Ensure it's 0 self.next_date_candidate = None; None } } } } // Internal helper functions pub fn group_dates_helper( dates: Vec, freq: DateFreq, ) -> Result>, Box> { let mut groups: HashMap> = 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)> = groups.into_iter().collect(); sorted_groups.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); // Dates within groups are already sorted because they came from the sorted `self.list()`. let result_groups = sorted_groups.into_iter().map(|(_, dates)| dates).collect(); Ok(result_groups) } /// Generates the flat list of dates for the given range and frequency. /// Assumes the `collect_*` functions return sorted dates. pub fn get_dates_list_with_freq( start_date_str: &str, end_date_str: &str, freq: DateFreq, ) -> Result, Box> { 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, /*want_first_day=*/ true)? } DateFreq::MonthEnd => { collect_monthly(start_date, end_date, /*want_first_day=*/ false)? } DateFreq::QuarterStart => { collect_quarterly(start_date, end_date, /*want_first_day=*/ true)? } DateFreq::QuarterEnd => { collect_quarterly(start_date, end_date, /*want_first_day=*/ false)? } DateFreq::YearStart => collect_yearly(start_date, end_date, /*want_first_day=*/ true)?, DateFreq::YearEnd => collect_yearly(start_date, end_date, /*want_first_day=*/ false)?, }; // The collect_* functions should now generate sorted dates directly. Ok(dates) } // Low-Level Date Collection Functions (Internal) // These functions generate dates within a *range* [start_date, end_date] /// Returns all calendar days day-by-day within the range. fn collect_daily( start_date: NaiveDate, end_date: NaiveDate, ) -> Result, Box> { 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) } /// Returns the specified `target_weekday` in each week within the range. fn collect_weekly( start_date: NaiveDate, end_date: NaiveDate, target_weekday: Weekday, ) -> Result, Box> { 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) } /// Returns either the first or last calendar day in each month of the range. fn collect_monthly( start_date: NaiveDate, end_date: NaiveDate, want_first_day: bool, ) -> Result, Box> { 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; // Safety check for potential infinite loop, though unlikely with valid date logic if year > end_date.year() + 2 { return Err("Loop seems to exceed reasonable year range in collect_monthly".into()); } } Ok(result) } /// Return either the first or last calendar day in each quarter of the range. fn collect_quarterly( start_date: NaiveDate, end_date: NaiveDate, want_first_day: bool, ) -> Result, Box> { 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; } // Safety check if year > end_date.year() + 2 { return Err("Loop seems to exceed reasonable year range in collect_quarterly".into()); } } Ok(result) } /// Returns a list of dates between the given start and end dates, inclusive, /// at the specified frequency. /// This function is a convenience wrapper around `get_dates_list_with_freq`. pub fn get_dates_list_with_freq_from_naive_date( start_date: NaiveDate, end_date: NaiveDate, freq: DateFreq, ) -> Result, Box> { get_dates_list_with_freq( &start_date.format("%Y-%m-%d").to_string(), &end_date.format("%Y-%m-%d").to_string(), freq, ) } /// Return either the first or last calendar day in each year of the range. fn collect_yearly( start_date: NaiveDate, end_date: NaiveDate, want_first_day: bool, ) -> Result, Box> { 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 { // Optimization: If the candidate date is already past the end_date, // no subsequent year's candidate will be in range. break; } year = year.checked_add(1).ok_or("Year overflow")?; } Ok(result) } /// 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_day_of_week_on_or_after( date: NaiveDate, target: Weekday, ) -> Result> { let mut current = date; while current.weekday() != target { current = current .succ_opt() .ok_or("Date overflow moving to next weekday")?; } Ok(current) } /// Return the first calendar day of the given (year, month). fn first_day_of_month(year: i32, month: u32) -> Result> { 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()) } /// Returns the number of days in a given month and year. fn days_in_month(year: i32, month: u32) -> Result> { 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) }; // Use first_day_of_month which handles ymd creation errors 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()) } /// Return the last calendar day of the given (year, month). fn last_day_of_month(year: i32, month: u32) -> Result> { // days_in_month now validates month and handles overflow 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()) } /// Converts a month number (1-12) to a quarter number (1-4). /// Panics if month is invalid (should not happen with valid NaiveDate). 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), // Should only happen with programmer error } } /// Returns the 1st day of the month that starts a given quarter. fn quarter_start_month(quarter: u32) -> Result> { match quarter { 1 => Ok(1), // Jan 2 => Ok(4), // Apr 3 => Ok(7), // Jul 4 => Ok(10), // Oct _ => Err(format!("invalid quarter: {}", quarter).into()), // Return Err instead of panic } } /// Return the first calendar day in the given (year, quarter). fn first_day_of_quarter(year: i32, quarter: u32) -> Result> { // Propagate error from quarter_start_month let month = quarter_start_month(quarter)?; first_day_of_month(year, month) } /// Returns the last day of the month that ends a given quarter. fn quarter_end_month(quarter: u32) -> Result> { match quarter { 1 => Ok(3), // Mar 2 => Ok(6), // Jun 3 => Ok(9), // Sep 4 => Ok(12), // Dec _ => Err(format!("invalid quarter: {}", quarter).into()), // Return Err instead of panic } } /// Return the last calendar day in the given (year, quarter). fn last_day_of_quarter(year: i32, quarter: u32) -> Result> { // Propagate error from quarter_end_month let month = quarter_end_month(quarter)?; last_day_of_month(year, month) } /// Returns the first calendar day (Jan 1st) of the given year. fn first_day_of_year(year: i32) -> Result> { NaiveDate::from_ymd_opt(year, 1, 1) .ok_or_else(|| format!("Invalid year for Jan 1st: {}", year).into()) } /// Returns the last calendar day (Dec 31st) of the given year. fn last_day_of_year(year: i32) -> Result> { NaiveDate::from_ymd_opt(year, 12, 31) .ok_or_else(|| format!("Invalid year for Dec 31st: {}", year).into()) } // --- Generator Helper Functions --- fn get_first_date_helper(freq: DateFreq) -> fn(i32, u32) -> Result> { 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!(), } } /// Finds the *first* valid date according to the frequency, /// starting the search *on or after* the given `start_date`. pub fn find_first_date_on_or_after( start_date: NaiveDate, freq: DateFreq, ) -> Result> { match freq { DateFreq::Daily => Ok(start_date), // The first daily date is the start date itself 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 mut candidate = first_day_of_month(start_date.year(), start_date.month())?; 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) } } } /// Finds the *next* valid date according to the frequency, /// given the `current_date` (which is assumed to be a valid date previously generated). pub fn find_next_date( current_date: NaiveDate, freq: DateFreq, ) -> Result> { 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, ) } } } // --- Tests --- #[cfg(test)] mod tests { use super::*; use chrono::{Duration, NaiveDate, Weekday}; // Make sure Duration is imported // Helper to create a NaiveDate for tests, expecting valid dates. fn date(year: i32, month: u32, day: u32) -> NaiveDate { NaiveDate::from_ymd_opt(year, month, day).expect("Invalid date in test setup") } // --- DateFreq Tests --- #[test] fn test_datefreq_from_str() -> Result<(), Box> { assert_eq!(DateFreq::from_str("D")?, DateFreq::Daily); assert_eq!("D".parse::()?, 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::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::().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> { 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); } // --- DatesList Property Tests --- #[test] fn test_dates_list_properties_new() -> Result<(), Box> { 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> { let start_str = "2023-01-01".to_string(); // Sunday let freq = DateFreq::Daily; let n_periods = 5; // Expect: Jan 1, 2, 3, 4, 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_dates_list_from_n_periods_fail_get_last removed as it was flawed #[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") // Error from NaiveDate::parse_from_str ); } #[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()); // End date is valid 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()); // Start date is valid assert!(list_end_invalid.end_date().is_err()); } // --- DatesList Core Logic Tests (list, count) --- #[test] fn test_dates_list_daily_list() -> Result<(), Box> { let dates_list = DatesList::new( "2023-11-01".to_string(), // Wed "2023-11-05".to_string(), // Sun 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> { let dates_list = DatesList::new( "2023-10-30".to_string(), // Mon "2023-11-13".to_string(), // Mon DateFreq::WeeklyMonday, ); let list = dates_list.list()?; // Mondays in range: Oct 30, Nov 6, Nov 13 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> { let dates_list = DatesList::new( "2023-11-01".to_string(), // Wed "2023-11-17".to_string(), // Fri DateFreq::WeeklyFriday, ); let list = dates_list.list()?; // Fridays in range: Nov 3, Nov 10, Nov 17 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> { let dates_list = DatesList::new( "2023-11-15".to_string(), // Mid-Nov "2024-02-01".to_string(), // Feb 1st DateFreq::MonthStart, ); let list = dates_list.list()?; // Month starts >= Nov 15 and <= Feb 1: Dec 1, Jan 1, Feb 1 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> { let dates_list = DatesList::new( "2024-01-15".to_string(), // Mid-Jan "2024-03-31".to_string(), // Mar 31st DateFreq::MonthEnd, ); let list = dates_list.list()?; // Month ends >= Jan 15 and <= Mar 31: Jan 31, Feb 29 (leap), Mar 31 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> { let dates_list = DatesList::new( "2023-08-01".to_string(), // Mid Q3 "2024-04-01".to_string(), // Start Q2 DateFreq::QuarterStart, ); let list = dates_list.list()?; // Quarter starts >= Aug 1 '23 and <= Apr 1 '24: Oct 1 '23, Jan 1 '24, Apr 1 '24 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> { let dates_list = DatesList::new( "2023-03-31".to_string(), // End Q1 "2023-12-31".to_string(), // End Q4 DateFreq::QuarterEnd, ); let list = dates_list.list()?; // Quarter ends >= Mar 31 and <= Dec 31: Mar 31, Jun 30, Sep 30, Dec 31 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> { let dates_list = DatesList::new( "2023-06-01".to_string(), // Mid 2023 "2025-01-01".to_string(), // Start 2025 DateFreq::YearStart, ); let list = dates_list.list()?; // Year starts >= Jun 1 '23 and <= Jan 1 '25: Jan 1 '24, Jan 1 '25 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> { let dates_list = DatesList::new( "2022-01-01".to_string(), // Start 2022 "2024-03-31".to_string(), // Q1 2024 DateFreq::YearEnd, ); let list = dates_list.list()?; // Year ends >= Jan 1 '22 and <= Mar 31 '24: Dec 31 '22, Dec 31 '23 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> { let dates_list = DatesList::new( "2023-12-31".to_string(), "2023-01-01".to_string(), // End date before start date DateFreq::Daily, ); let list = dates_list.list()?; assert!(list.is_empty()); assert_eq!(dates_list.count()?, 0); Ok(()) } // --- Tests for groups() method --- #[test] fn test_dates_list_groups_monthly_end() -> Result<(), Box> { let dates_list = DatesList::new( "2023-10-15".to_string(), // Mid-Oct "2024-01-15".to_string(), // Mid-Jan DateFreq::MonthEnd, ); let groups = dates_list.groups()?; // Month Ends >= Oct 15 '23 and <= Jan 15 '24: Oct 31, Nov 30, Dec 31 assert_eq!(groups.len(), 3); // Key order: Monthly(2023, 10), Monthly(2023, 11), Monthly(2023, 12) 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> { let dates_list = DatesList::new( "2023-11-01".to_string(), // Wed "2023-11-03".to_string(), // Fri DateFreq::Daily, ); let groups = dates_list.groups()?; // Dates: Nov 1, Nov 2, Nov 3. Each gets own group. assert_eq!(groups.len(), 3); // Key order: Daily(2023-11-01), Daily(2023-11-02), Daily(2023-11-03) 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> { let dates_list = DatesList::new( "2023-11-01".to_string(), // Wed (ISO Week 44) "2023-11-15".to_string(), // Wed (ISO Week 46) DateFreq::WeeklyFriday, ); let groups = dates_list.groups()?; // Fridays in range: Nov 3 (W44), Nov 10 (W45) assert_eq!(groups.len(), 2); // Key order: Weekly(2023, 44), Weekly(2023, 45) 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> { let dates_list = DatesList::new( "2023-08-01".to_string(), // Start Q3 "2024-05-01".to_string(), // Start Q2 DateFreq::QuarterStart, ); let groups = dates_list.groups()?; // Quarter starts >= Aug 1 '23 and <= May 1 '24: Oct 1 '23, Jan 1 '24, Apr 1 '24 assert_eq!(groups.len(), 3); // Key order: Quarterly(2023, 4), Quarterly(2024, 1), Quarterly(2024, 2) 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> { let dates_list = DatesList::new( "2022-01-01".to_string(), // Start 2022 "2024-03-31".to_string(), // Q1 2024 DateFreq::YearEnd, ); let groups = dates_list.groups()?; // Year ends >= Jan 1 '22 and <= Mar 31 '24: Dec 31 '22, Dec 31 '23 assert_eq!(groups.len(), 2); // Key order: Yearly(2022), Yearly(2023) 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> { let dates_list = DatesList::new( "2023-12-31".to_string(), "2023-01-01".to_string(), // End < Start 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> { 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)?, // Sat -> Mon date(2023, 11, 13) ); assert_eq!( move_to_day_of_week_on_or_after(date(2023, 11, 10), Weekday::Mon)?, // Fri -> Mon date(2023, 11, 13) ); // Test near max date (ensure it doesn't panic easily, though overflow is possible) // MAX - 7 days guarantees we have room to move forward let near_max = NaiveDate::MAX - Duration::days(7); assert!(move_to_day_of_week_on_or_after(near_max, Weekday::Sun).is_ok()); // Test overflow case - starting at MAX, moving forward fails if MAX is not target if NaiveDate::MAX.weekday() != Weekday::Sun { assert!(move_to_day_of_week_on_or_after(NaiveDate::MAX, Weekday::Sun).is_err()); } else { // If MAX is the target, it should succeed assert!(move_to_day_of_week_on_or_after(NaiveDate::MAX, Weekday::Sun).is_ok()); // And trying to move *past* it should fail let day_before = NaiveDate::MAX - Duration::days(1); let target_day_after = NaiveDate::MAX.weekday().succ(); // Day after MAX's weekday assert!(move_to_day_of_week_on_or_after(day_before, target_day_after).is_err()); // Moving past MAX fails } Ok(()) } #[test] fn test_first_day_of_month() -> Result<(), Box> { 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()); // Invalid month 0 assert!(first_day_of_month(2023, 13).is_err()); // Invalid month 13 Ok(()) } #[test] fn test_days_in_month() -> Result<(), Box> { assert_eq!(days_in_month(2023, 1)?, 31); assert_eq!(days_in_month(2023, 2)?, 28); // Leap assert_eq!(days_in_month(2024, 2)?, 29); assert_eq!(days_in_month(2023, 4)?, 30); assert_eq!(days_in_month(2023, 12)?, 31); // Invalid month 0 assert!(days_in_month(2023, 0).is_err()); // Invalid month 13 // Test near max date year overflow - Use MAX.year() 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> { assert_eq!(last_day_of_month(2023, 11)?, date(2023, 11, 30)); assert_eq!(last_day_of_month(2024, 2)?, date(2024, 2, 29)); // Leap assert_eq!(last_day_of_month(2023, 12)?, date(2023, 12, 31)); // Invalid month 0 assert!(last_day_of_month(2023, 0).is_err()); // Invalid month 13 // Test near max date year overflow - use MAX.year() 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> { 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)); // Invalid quarter 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> { 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)); // Leap year doesn't affect March end assert_eq!(last_day_of_quarter(2024, 1)?, date(2024, 3, 31)); // Invalid quarter // Test overflow propagation - use MAX.year() 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> { assert_eq!(first_day_of_year(2023)?, date(2023, 1, 1)); assert_eq!(first_day_of_year(2024)?, date(2024, 1, 1)); // Test MAX year - should be ok for Jan 1 assert!(first_day_of_year(NaiveDate::MAX.year()).is_ok()); Ok(()) } #[test] fn test_last_day_of_year() -> Result<(), Box> { assert_eq!(last_day_of_year(2023)?, date(2023, 12, 31)); // Leap year doesn't affect Dec 31st existence // Test MAX year - should be okay since MAX is Dec 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> { // Daily assert_eq!( find_first_date_on_or_after(date(2023, 11, 8), DateFreq::Daily)?, date(2023, 11, 8) ); // Sat -> Sat assert_eq!( find_first_date_on_or_after(date(2023, 11, 11), DateFreq::Daily)?, date(2023, 11, 11) ); // Weekly Mon 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) ); // Sun -> Mon assert_eq!( find_first_date_on_or_after(date(2023, 11, 12), DateFreq::WeeklyMonday)?, date(2023, 11, 13) ); // Weekly Fri 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) ); // Sat -> Next Fri // Month Start 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) ); // Oct 1 -> Oct 1 assert_eq!( find_first_date_on_or_after(date(2023, 10, 1), DateFreq::MonthStart)?, date(2023, 10, 1) ); // Month End 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) ); // Dec 31 -> Dec 31 assert_eq!( find_first_date_on_or_after(date(2023, 12, 31), DateFreq::MonthEnd)?, date(2023, 12, 31) ); // Mid Feb (Leap) -> Feb 29 assert_eq!( find_first_date_on_or_after(date(2024, 2, 15), DateFreq::MonthEnd)?, date(2024, 2, 29) ); // Feb 29 -> Feb 29 assert_eq!( find_first_date_on_or_after(date(2024, 2, 29), DateFreq::MonthEnd)?, date(2024, 2, 29) ); // Quarter Start 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) ); // Quarter End 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) ); // Year Start 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) ); // Year End 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) ); // --- Test Overflow Cases near MAX --- assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::Daily).is_ok()); // Daily starting at MAX is MAX // Weekly: depends if MAX is the target day. If not, succ() fails. 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()); } // Month Start: MAX is Dec 31. find_first for MonthStart at MAX tries month=12, candidate=Dec 1. candidate < MAX is true. // Tries next month: Jan (MAX_YEAR+1), which fails in checked_add. assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::MonthStart).is_err()); // Month End: MAX is Dec 31. find_first for MonthEnd at MAX tries month=12, calls last_day_of_month(MAX_YEAR, 12). // last_day_of_month -> days_in_month -> first_day_of_month(MAX_YEAR+1, 1) -> fails. assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::MonthEnd).is_err()); // Quarter Start: MAX is Dec 31 (Q4). Tries Q4 start (Oct 1). candidate < MAX is true. Tries next Q (Q1 MAX+1), fails. assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::QuarterStart).is_err()); // Quarter End: MAX is Dec 31 (Q4). Tries Q4 end (Dec 31). Calls last_day_of_quarter(MAX_YEAR, 4). // last_day_of_quarter -> last_day_of_month(MAX_YEAR, 12) -> fails. assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::QuarterEnd).is_err()); // Year Start: MAX is Dec 31. Tries YearStart(MAX_YEAR) (Jan 1). candidate < MAX is true. Tries next year (MAX_YEAR+1), fails. assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::YearStart).is_err()); // Year End: MAX is Dec 31. Tries YearEnd(MAX_YEAR). Calls last_day_of_year(MAX_YEAR). Returns Ok(MAX). candidate < MAX is false. Returns Ok(MAX). assert!(find_first_date_on_or_after(NaiveDate::MAX, DateFreq::YearEnd).is_ok()); Ok(()) } #[test] fn test_find_next_date() -> Result<(), Box> { // Daily 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) ); // Fri -> Sat // Weekly Mon assert_eq!( find_next_date(date(2023, 11, 13), DateFreq::WeeklyMonday)?, date(2023, 11, 20) ); // Weekly Fri assert_eq!( find_next_date(date(2023, 11, 10), DateFreq::WeeklyFriday)?, date(2023, 11, 17) ); // Month Start 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) ); // Month End 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) ); // Jan -> Feb (Leap) assert_eq!( find_next_date(date(2024, 2, 29), DateFreq::MonthEnd)?, date(2024, 3, 31) ); // Feb -> Mar // Quarter Start 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) ); // Quarter End 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) ); // Year Start 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) ); // Year End 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) ); // --- Test Overflow Cases near MAX --- assert!(find_next_date(NaiveDate::MAX, DateFreq::Daily).is_err()); assert!( find_next_date(NaiveDate::MAX - Duration::days(6), DateFreq::WeeklyMonday).is_err() ); // Test finding next month start after Dec MAX_YEAR -> Jan (MAX_YEAR+1) (fail) assert!(find_next_date(date(NaiveDate::MAX.year(), 12, 1), DateFreq::MonthStart).is_err()); // Test finding next month end after Nov MAX_YEAR -> Dec MAX_YEAR (fails because last_day_of_month(MAX, 12) fails) 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()); // Test finding next month end after Dec MAX_YEAR -> Jan (MAX_YEAR+1) (fail) // The call last_day_of_month(MAX_YEAR + 1, 1) fails assert!(find_next_date(NaiveDate::MAX, DateFreq::MonthEnd).is_err()); // Test finding next quarter start after Q4 MAX_YEAR -> Q1 (MAX_YEAR+1) (fail) assert!(find_next_date( first_day_of_quarter(NaiveDate::MAX.year(), 4)?, DateFreq::QuarterStart ) .is_err()); // Test finding next quarter end after Q3 MAX_YEAR -> Q4 MAX_YEAR (fails because last_day_of_quarter(MAX, 4) fails) 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()); // Test finding next quarter end after Q4 MAX_YEAR -> Q1 (MAX_YEAR+1) (fail) // The call last_day_of_quarter(MAX_YEAR + 1, 1) fails assert!(find_next_date(NaiveDate::MAX, DateFreq::QuarterEnd).is_err()); // Test finding next year start after Jan 1 MAX_YEAR -> Jan 1 (MAX_YEAR+1) (fail) assert!(find_next_date( first_day_of_year(NaiveDate::MAX.year())?, DateFreq::YearStart ) .is_err()); // Test finding next year end after Dec 31 (MAX_YEAR-1) -> Dec 31 MAX_YEAR (ok) assert!(find_next_date( last_day_of_year(NaiveDate::MAX.year() - 1)?, DateFreq::YearEnd ) .is_ok()); // Test finding next year end after Dec 31 MAX_YEAR -> Dec 31 (MAX_YEAR+1) (fail) assert!( find_next_date(last_day_of_year(NaiveDate::MAX.year())?, DateFreq::YearEnd).is_err() ); // Fails calculating MAX_YEAR+1 Ok(()) } // --- Tests for DatesGenerator --- #[test] fn test_generator_new_zero_periods() -> Result<(), Box> { 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); // Immediately exhausted Ok(()) } #[test] fn test_generator_new_fail_find_first() -> Result<(), Box> { let start_date = NaiveDate::MAX; // Use a frequency that requires finding the *next* day if MAX isn't the target. let freq = DateFreq::WeeklyMonday; let n_periods = 1; let result = DatesGenerator::new(start_date, freq, n_periods); // This fails if MAX is not a Monday, because find_first tries MAX.succ_opt() if NaiveDate::MAX.weekday() != Weekday::Mon { assert!(result.is_err()); } else { // If MAX *is* a Monday, new() succeeds. assert!(result.is_ok()); } Ok(()) } #[test] fn test_generator_daily() -> Result<(), Box> { let start_date = date(2023, 11, 10); // Fri 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))); // Fri assert_eq!(generator.next(), Some(date(2023, 11, 11))); // Sat assert_eq!(generator.next(), Some(date(2023, 11, 12))); // Sun assert_eq!(generator.next(), Some(date(2023, 11, 13))); // Mon assert_eq!(generator.next(), None); // Exhausted // Test collecting let generator_collect = DatesGenerator::new(start_date, freq, n_periods)?; assert_eq!( generator_collect.collect::>(), 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> { let start_date = date(2023, 11, 8); // Wed 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> { let start_date = date(2023, 11, 11); // Sat 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> { let start_date = date(2023, 10, 15); // Mid-Oct let freq = DateFreq::MonthStart; let n_periods = 4; // Nov, Dec, Jan, Feb 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> { let start_date = date(2024, 1, 31); // Jan 31 let freq = DateFreq::MonthEnd; let n_periods = 3; // Jan, Feb (leap), Mar let mut generator = DatesGenerator::new(start_date, freq, n_periods)?; // find_first for Jan 31 returns Jan 31 assert_eq!(generator.next(), Some(date(2024, 1, 31))); // find_next finds Feb 29 assert_eq!(generator.next(), Some(date(2024, 2, 29))); // find_next finds Mar 31 assert_eq!(generator.next(), Some(date(2024, 3, 31))); assert_eq!(generator.next(), None); Ok(()) } #[test] fn test_generator_quarter_start() -> Result<(), Box> { let start_date = date(2023, 8, 1); // Mid-Q3 let freq = DateFreq::QuarterStart; let n_periods = 3; // Q4'23, Q1'24, Q2'24 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> { let start_date = date(2023, 11, 1); // Mid-Q4 let freq = DateFreq::QuarterEnd; let n_periods = 3; // Q4'23, Q1'24, Q2'24 let mut generator = DatesGenerator::new(start_date, freq, n_periods)?; // find_first for Nov 1 (Q4) returns Dec 31 (Q4 end) assert_eq!(generator.next(), Some(date(2023, 12, 31))); // find_next finds Mar 31 (Q1 end) assert_eq!(generator.next(), Some(date(2024, 3, 31))); // find_next finds Jun 30 (Q2 end) assert_eq!(generator.next(), Some(date(2024, 6, 30))); assert_eq!(generator.next(), None); Ok(()) } #[test] fn test_generator_year_start() -> Result<(), Box> { let start_date = date(2023, 1, 1); // Jan 1 let freq = DateFreq::YearStart; let n_periods = 3; // 2023, 2024, 2025 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> { let start_date = date(2022, 12, 31); // Dec 31 let freq = DateFreq::YearEnd; let n_periods = 3; // 2022, 2023, 2024 let mut generator = DatesGenerator::new(start_date, freq, n_periods)?; // find_first for Dec 31 '22 returns Dec 31 '22 assert_eq!(generator.next(), Some(date(2022, 12, 31))); // find_next finds Dec 31 '23 assert_eq!(generator.next(), Some(date(2023, 12, 31))); // find_next finds Dec 31 '24 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> { let start_year = NaiveDate::MAX.year(); let start_date = last_day_of_year(start_year - 1)?; // Dec 31 of year before MAX let freq = DateFreq::YearEnd; let n_periods = 3; // Try for YE(MAX-1), YE(MAX), YE(MAX+1) - last should fail let mut generator = DatesGenerator::new(start_date, freq, n_periods)?; // find_first returns start_date (YE MAX-1) assert_eq!(generator.next(), Some(start_date)); // find_next finds YE(MAX) assert_eq!(generator.next(), Some(last_day_of_year(start_year)?)); // Should be MAX // find_next tries YE(MAX+1) - this call to find_next_date fails internally assert_eq!(generator.next(), None); // Returns None because internal find_next_date failed // State after the *first* None is returned: // Corrected assertion assert_eq!(generator.periods_remaining, 0); assert!(generator.next_date_candidate.is_none()); // Calling next() again should also return None assert_eq!(generator.next(), None); assert_eq!(generator.periods_remaining, 0); Ok(()) } } // end mod tests