rustframe/utils/dateutils/
bdates.rs

1//! This module provides functionality for generating and manipulating business dates.
2//! It includes the `BDatesList`, which emulates a `DateList` structure and its properties.
3//! It uses `DateList` and `DateListGenerator`, adjusting the output to work on business dates.
4
5use chrono::{Datelike, Duration, NaiveDate, Weekday};
6use std::error::Error;
7use std::result::Result;
8
9use crate::utils::dateutils::dates::{find_next_date, AggregationType, DateFreq, DatesGenerator};
10
11use crate::utils::dateutils::dates;
12
13/// Type alias for `DateFreq` to represent business date frequency.
14pub type BDateFreq = DateFreq;
15
16/// Represents a list of business dates generated between a start and end date
17/// at a specified frequency. Provides methods to retrieve the full list,
18/// count, or dates grouped by period.
19#[derive(Debug, Clone)]
20pub struct BDatesList {
21    start_date_str: String,
22    end_date_str: String,
23    freq: DateFreq,
24}
25
26/// Represents a collection of business dates generated according to specific rules.
27///
28/// It can be defined either by a start and end date range or by a start date
29/// and a fixed number of periods. It provides methods to retrieve the dates
30/// as a flat list, count them, or group them by their natural period
31/// (e.g., month, quarter).
32///
33/// Business days are typically Monday to Friday. Weekend dates are skipped or
34/// adjusted depending on the frequency rules.
35///
36/// # Examples
37///
38/// **1. Using `new` (Start and End Date):**
39///
40/// ```rust
41/// use chrono::NaiveDate;
42/// use std::error::Error;
43/// use rustframe::utils::{BDatesList, DateFreq};
44///
45/// fn main() -> Result<(), Box<dyn Error>> {
46///     let start_date = "2023-11-01".to_string(); // Wednesday
47///     let end_date = "2023-11-07".to_string();   // Tuesday
48///     let freq = DateFreq::Daily;
49///
50///     let bdates = BDatesList::new(start_date, end_date, freq);
51///
52///     let expected_dates = vec![
53///         NaiveDate::from_ymd_opt(2023, 11, 1).unwrap(), // Wed
54///         NaiveDate::from_ymd_opt(2023, 11, 2).unwrap(), // Thu
55///         NaiveDate::from_ymd_opt(2023, 11, 3).unwrap(), // Fri
56///         NaiveDate::from_ymd_opt(2023, 11, 6).unwrap(), // Mon
57///         NaiveDate::from_ymd_opt(2023, 11, 7).unwrap(), // Tue
58///     ];
59///
60///     assert_eq!(bdates.list()?, expected_dates);
61///     assert_eq!(bdates.count()?, 5);
62///     Ok(())
63/// }
64/// ```
65///
66/// **2. Using `from_n_periods` (Start Date and Count):**
67///
68/// ```rust
69/// use chrono::NaiveDate;
70/// use std::error::Error;
71/// use rustframe::utils::{BDatesList, DateFreq};
72///
73/// fn main() -> Result<(), Box<dyn Error>> {
74///     let start_date = "2024-02-28".to_string(); // Wednesday
75///     let freq = DateFreq::WeeklyFriday;
76///     let n_periods = 3;
77///
78///     let bdates = BDatesList::from_n_periods(start_date, freq, n_periods)?;
79///
80///     // The first Friday on or after 2024-02-28 is Mar 1.
81///     // The next two Fridays are Mar 8 and Mar 15.
82///     let expected_dates = vec![
83///         NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
84///         NaiveDate::from_ymd_opt(2024, 3, 8).unwrap(),
85///         NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
86///     ];
87///
88///     assert_eq!(bdates.list()?, expected_dates);
89///     assert_eq!(bdates.count()?, 3);
90///     assert_eq!(bdates.start_date_str(), "2024-02-28"); // Keeps original start string
91///     assert_eq!(bdates.end_date_str(), "2024-03-15");   // End date is the last generated date
92///     Ok(())
93/// }
94/// ```
95///
96/// **3. Using `groups()`:**
97///
98/// ```rust
99/// use chrono::NaiveDate;
100/// use std::error::Error;
101/// use rustframe::utils::{BDatesList, DateFreq};
102///
103/// fn main() -> Result<(), Box<dyn Error>> {
104///     let start_date = "2023-11-20".to_string(); // Mon, Week 47
105///     let end_date = "2023-12-08".to_string();   // Fri, Week 49
106///     let freq = DateFreq::WeeklyMonday;
107///
108///     let bdates = BDatesList::new(start_date, end_date, freq);
109///
110///     // Mondays in range: Nov 20, Nov 27, Dec 4
111///     let groups = bdates.groups()?;
112///
113///     assert_eq!(groups.len(), 3); // One group per week containing a Monday
114///     assert_eq!(groups[0], vec![NaiveDate::from_ymd_opt(2023, 11, 20).unwrap()]); // Week 47
115///     assert_eq!(groups[1], vec![NaiveDate::from_ymd_opt(2023, 11, 27).unwrap()]); // Week 48
116///     assert_eq!(groups[2], vec![NaiveDate::from_ymd_opt(2023, 12, 4).unwrap()]);  // Week 49
117///     Ok(())
118/// }
119/// ```
120impl BDatesList {
121    /// Creates a new `BDatesList` instance defined by a start and end date.
122    ///
123    /// # Arguments
124    ///
125    /// * `start_date_str` - The inclusive start date as a string (e.g., "YYYY-MM-DD").
126    /// * `end_date_str` - The inclusive end date as a string (e.g., "YYYY-MM-DD").
127    /// * `freq` - The frequency for generating dates.
128    pub fn new(start_date_str: String, end_date_str: String, freq: DateFreq) -> Self {
129        BDatesList {
130            start_date_str,
131            end_date_str,
132            freq,
133        }
134    }
135
136    /// Creates a new `BDatesList` instance defined by a start date, frequency,
137    /// and the number of periods (dates) to generate.
138    ///
139    /// This calculates the required dates using a `BDatesGenerator` and determines
140    /// the effective end date based on the last generated date.
141    ///
142    /// # Arguments
143    ///
144    /// * `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.
145    /// * `freq` - The frequency for generating dates.
146    /// * `n_periods` - The exact number of business dates to generate according to the frequency.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if:
151    /// * `start_date_str` cannot be parsed.
152    /// * `n_periods` is 0 (as this would result in an empty list and no defined end date).
153    pub fn from_n_periods(
154        start_date_str: String,
155        freq: DateFreq,
156        n_periods: usize,
157    ) -> Result<Self, Box<dyn Error>> {
158        if n_periods == 0 {
159            return Err("n_periods must be greater than 0".into());
160        }
161
162        let start_date = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d")?;
163
164        // Instantiate the date generator to compute the sequence of business dates.
165        let generator = BDatesGenerator::new(start_date, freq, n_periods)?;
166        let dates: Vec<NaiveDate> = generator.collect();
167
168        // Confirm that the generator returned at least one date when n_periods > 0.
169        let last_date = dates
170            .last()
171            .ok_or("Generator failed to produce dates for the specified periods")?;
172
173        let end_date_str = last_date.format("%Y-%m-%d").to_string();
174
175        Ok(BDatesList {
176            start_date_str,
177            end_date_str,
178            freq,
179        })
180    }
181
182    /// Returns the flat list of business dates within the specified range and frequency.
183    ///
184    /// The list is guaranteed to be sorted chronologically.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if the start or end date strings cannot be parsed.
189    pub fn list(&self) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
190        // Retrieve the list of business dates via the shared helper function.
191        get_bdates_list_with_freq(&self.start_date_str, &self.end_date_str, self.freq)
192    }
193
194    /// Returns the count of business dates within the specified range and frequency.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the start or end date strings cannot be parsed.
199    pub fn count(&self) -> Result<usize, Box<dyn Error>> {
200        // Compute the total number of business dates by invoking `list()` and returning its length.
201        self.list().map(|list| list.len())
202    }
203
204    /// Returns a list of date lists, where each inner list contains dates
205    /// belonging to the same period (determined by frequency).
206    ///
207    /// The outer list (groups) is sorted chronologically by period, and the
208    /// inner lists (dates within each period) are also sorted.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the start or end date strings cannot be parsed.
213    pub fn groups(&self) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error>> {
214        let dates = self.list()?;
215        dates::group_dates_helper(dates, self.freq)
216    }
217
218    /// Returns the start date parsed as a `NaiveDate`.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the start date string is not in "YYYY-MM-DD" format.
223    pub fn start_date(&self) -> Result<NaiveDate, Box<dyn Error>> {
224        NaiveDate::parse_from_str(&self.start_date_str, "%Y-%m-%d").map_err(|e| e.into())
225    }
226
227    /// Returns the start date string.
228    pub fn start_date_str(&self) -> &str {
229        &self.start_date_str
230    }
231
232    /// Returns the end date parsed as a `NaiveDate`.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if the end date string is not in "YYYY-MM-DD" format.
237    pub fn end_date(&self) -> Result<NaiveDate, Box<dyn Error>> {
238        NaiveDate::parse_from_str(&self.end_date_str, "%Y-%m-%d").map_err(|e| e.into())
239    }
240
241    /// Returns the end date string.
242    pub fn end_date_str(&self) -> &str {
243        &self.end_date_str
244    }
245
246    /// Returns the frequency enum.
247    pub fn freq(&self) -> DateFreq {
248        self.freq
249    }
250
251    /// Returns the canonical string representation of the frequency.
252    pub fn freq_str(&self) -> String {
253        self.freq.to_string()
254    }
255}
256
257// Business date iterator: generates a sequence of business dates for a given frequency and period count.
258
259/// An iterator that generates a sequence of business dates based on a start date,
260/// frequency, and a specified number of periods.
261///
262/// This implements the `Iterator` trait, allowing generation of dates one by one.
263/// It's useful when you need to process dates lazily or only need a fixed number
264/// starting from a specific point, without necessarily defining an end date beforehand.
265///
266/// # Examples
267///
268/// **1. Basic Iteration:**
269///
270/// ```rust
271/// use chrono::NaiveDate;
272/// use std::error::Error;
273/// use rustframe::utils::{BDatesGenerator, DateFreq};
274///
275/// fn main() -> Result<(), Box<dyn Error>> {
276///     let start = NaiveDate::from_ymd_opt(2023, 12, 28).unwrap(); // Thursday
277///     let freq = DateFreq::MonthEnd;
278///     let n_periods = 4; // Dec '23, Jan '24, Feb '24, Mar '24
279///
280///     let mut generator = BDatesGenerator::new(start, freq, n_periods)?;
281///
282///     // First month-end on or after 2023-12-28 is 2023-12-29
283///     assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2023, 12, 29).unwrap()));
284///     assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 31).unwrap()));
285///     assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 2, 29).unwrap())); // Leap year
286///     assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 3, 29).unwrap())); // Mar 31 is Sun
287///     assert_eq!(generator.next(), None); // Exhausted
288///     Ok(())
289/// }
290/// ```
291///
292/// **2. Collecting into a Vec:**
293///
294/// ```rust
295/// use chrono::NaiveDate;
296/// use std::error::Error;
297/// use rustframe::utils::{BDatesGenerator, DateFreq};
298///
299/// fn main() -> Result<(), Box<dyn Error>> {
300///     let start = NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(); // Monday
301///     let freq = DateFreq::Daily;
302///     let n_periods = 5;
303///
304///     let generator = BDatesGenerator::new(start, freq, n_periods)?;
305///     let dates: Vec<NaiveDate> = generator.collect();
306///
307///     let expected_dates = vec![
308///         NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(), // Mon
309///         NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(), // Tue
310///         NaiveDate::from_ymd_opt(2024, 5, 1).unwrap(),  // Wed
311///         NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(),  // Thu
312///         NaiveDate::from_ymd_opt(2024, 5, 3).unwrap(),  // Fri
313///     ];
314///
315///     assert_eq!(dates, expected_dates);
316///     Ok(())
317/// }
318/// ```
319#[derive(Debug, Clone)]
320pub struct BDatesGenerator {
321    dates_generator: DatesGenerator,
322    start_date: NaiveDate,
323    freq: DateFreq,
324    periods_remaining: usize,
325}
326
327impl BDatesGenerator {
328    /// Creates a new `BDatesGenerator`.
329    ///
330    /// It calculates the first valid business date based on the `start_date` and `freq`,
331    /// which will be the first item yielded by the iterator.
332    ///
333    /// # Arguments
334    ///
335    /// * `start_date` - The date from which to start searching for the first valid business date.
336    /// * `freq` - The frequency for generating dates.
337    /// * `n_periods` - The total number of business dates to generate.
338    ///
339    /// # Errors
340    ///
341    /// Can potentially return an error if date calculations lead to overflows,
342    /// though this is highly unlikely with realistic date ranges. (Currently returns Ok).
343    /// Note: The internal `find_first_bdate_on_or_after` might panic on extreme date overflows,
344    /// but practical usage should be safe.
345    pub fn new(
346        start_date: NaiveDate,
347        freq: DateFreq,
348        n_periods: usize,
349    ) -> Result<Self, Box<dyn Error>> {
350        // over-estimate the number of periods
351        let adj_n_periods = match freq {
352            DateFreq::Daily => n_periods + 5,
353            DateFreq::WeeklyMonday
354            | DateFreq::WeeklyFriday
355            | DateFreq::MonthStart
356            | DateFreq::MonthEnd
357            | DateFreq::QuarterStart
358            | DateFreq::QuarterEnd
359            | DateFreq::YearStart
360            | DateFreq::YearEnd => n_periods + 2,
361        };
362
363        let dates_generator = DatesGenerator::new(start_date, freq, adj_n_periods)?;
364
365        Ok(BDatesGenerator {
366            dates_generator,
367            start_date,
368            freq,
369            periods_remaining: n_periods,
370        })
371    }
372}
373
374impl Iterator for BDatesGenerator {
375    type Item = NaiveDate;
376
377    /// Returns the next business date in the sequence, or `None` if the specified
378    /// number of periods has been generated.
379    fn next(&mut self) -> Option<Self::Item> {
380        // Terminate if no periods remain or no initial date is set.
381        if self.periods_remaining == 0 {
382            return None;
383        }
384
385        // get the next date from the generator
386        let next_date = self.dates_generator.next()?;
387
388        let next_date = match self.freq {
389            DateFreq::Daily => {
390                let mut new_candidate = next_date.clone();
391                while !is_business_date(new_candidate) {
392                    new_candidate = self.dates_generator.next()?;
393                }
394                new_candidate
395            }
396
397            DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => next_date,
398            DateFreq::MonthEnd | DateFreq::QuarterEnd | DateFreq::YearEnd => {
399                let adjusted_date = iter_reverse_till_bdate(next_date);
400                if self.start_date > adjusted_date {
401                    // Skip this iteration if the adjusted date is before the start date.
402                    return self.next();
403                }
404                adjusted_date
405            }
406            DateFreq::MonthStart | DateFreq::QuarterStart | DateFreq::YearStart => {
407                // Adjust to the first business date of the month, quarter, or year.
408                iter_till_bdate(next_date)
409            }
410        };
411        // Decrement the remaining periods.
412        self.periods_remaining -= 1;
413        Some(next_date)
414    }
415}
416
417/// Check if the date is a weekend (Saturday or Sunday).
418pub fn is_business_date(date: NaiveDate) -> bool {
419    match date.weekday() {
420        Weekday::Sat | Weekday::Sun => false,
421        _ => true,
422    }
423}
424
425pub fn find_next_bdate(date: NaiveDate, freq: DateFreq) -> NaiveDate {
426    let next_date: NaiveDate = find_next_date(date, freq).unwrap();
427    let next_date = iter_till_bdate(next_date);
428    next_date
429}
430
431pub fn find_first_bdate_on_or_after(date: NaiveDate, freq: DateFreq) -> NaiveDate {
432    // Find the first business date on or after the given date.
433    let first_date = dates::find_first_date_on_or_after(date, freq).unwrap();
434    let first_date = iter_till_bdate_by_freq(first_date, freq);
435    // let first_date = iter_till_bdate(first_date);
436
437    first_date
438}
439
440/// Iterate forwards or backwards (depending on the frequency)
441/// until a business date is found.
442fn iter_till_bdate_by_freq(date: NaiveDate, freq: DateFreq) -> NaiveDate {
443    let agg_type = freq.agg_type();
444    let dur = match agg_type {
445        AggregationType::Start => Duration::days(1),
446        AggregationType::End => Duration::days(-1),
447    };
448    let mut current_date = date;
449    while !is_business_date(current_date) {
450        current_date = current_date + dur;
451    }
452    current_date
453}
454
455/// Increment day-by-day until a business date is found.
456fn iter_till_bdate(date: NaiveDate) -> NaiveDate {
457    let mut current_date = date;
458    while !is_business_date(current_date) {
459        current_date = current_date + Duration::days(1);
460    }
461    current_date
462}
463
464/// Increment day-by-day until a business date is found.
465fn iter_reverse_till_bdate(date: NaiveDate) -> NaiveDate {
466    let mut current_date = date;
467    while !is_business_date(current_date) {
468        current_date = current_date - Duration::days(1);
469    }
470    current_date
471}
472
473/// Helper function to get a list of business dates based on the frequency.
474pub fn get_bdates_list_with_freq(
475    start_date_str: &str,
476    end_date_str: &str,
477    freq: DateFreq,
478) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
479    // Generate the list of business dates using the shared logic.
480
481    let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")?;
482    let end_date = NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")?;
483
484    let mut dates = dates::get_dates_list_with_freq_from_naive_date(start_date, end_date, freq)?;
485
486    match freq {
487        DateFreq::Daily => {
488            dates.retain(|date| is_business_date(*date));
489        }
490        DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => {
491            // No logic needed (or possible?)
492        }
493        _ => {
494            dates.iter_mut().for_each(|date| {
495                *date = iter_till_bdate_by_freq(*date, freq);
496            });
497        }
498    }
499
500    Ok(dates)
501}
502
503// --- Example Usage and Tests ---
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use chrono::NaiveDate;
509    use std::str::FromStr;
510
511    // Helper to create a NaiveDate for tests, handling the expect for fixed dates.
512    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
513        NaiveDate::from_ymd_opt(year, month, day).expect("Invalid date in test setup")
514    }
515
516    // --- DateFreq Tests ---
517
518    #[test]
519    fn test_date_freq_from_str() -> Result<(), Box<dyn Error>> {
520        assert_eq!(DateFreq::from_str("D")?, DateFreq::Daily);
521        assert_eq!("D".parse::<DateFreq>()?, DateFreq::Daily); // Test FromStr impl
522        assert_eq!(DateFreq::from_str("W")?, DateFreq::WeeklyMonday);
523        assert_eq!(DateFreq::from_str("M")?, DateFreq::MonthStart);
524        assert_eq!(DateFreq::from_str("Q")?, DateFreq::QuarterStart);
525
526        // Test YearStart codes and aliases (Y, A, AS, YS)
527        assert_eq!(DateFreq::from_str("Y")?, DateFreq::YearStart);
528        assert_eq!(DateFreq::from_str("A")?, DateFreq::YearStart);
529        assert_eq!(DateFreq::from_str("AS")?, DateFreq::YearStart);
530        assert_eq!(DateFreq::from_str("YS")?, DateFreq::YearStart);
531        assert_eq!("Y".parse::<DateFreq>()?, DateFreq::YearStart); // Test FromStr impl
532
533        assert_eq!(DateFreq::from_str("ME")?, DateFreq::MonthEnd);
534        assert_eq!(DateFreq::from_str("QE")?, DateFreq::QuarterEnd);
535        assert_eq!(DateFreq::from_str("WF")?, DateFreq::WeeklyFriday);
536        assert_eq!("WF".parse::<DateFreq>()?, DateFreq::WeeklyFriday); // Test FromStr impl
537
538        // Test YearEnd codes and aliases (YE, AE)
539        assert_eq!(DateFreq::from_str("YE")?, DateFreq::YearEnd);
540        assert_eq!(DateFreq::from_str("AE")?, DateFreq::YearEnd);
541
542        // Test aliases for other frequencies
543        assert_eq!(DateFreq::from_str("WS")?, DateFreq::WeeklyMonday);
544        assert_eq!(DateFreq::from_str("MS")?, DateFreq::MonthStart);
545        assert_eq!(DateFreq::from_str("QS")?, DateFreq::QuarterStart);
546
547        // Test invalid string
548        assert!(DateFreq::from_str("INVALID").is_err());
549        assert!("INVALID".parse::<DateFreq>().is_err()); // Test FromStr impl
550        let err = DateFreq::from_str("INVALID").unwrap_err();
551        assert_eq!(err.to_string(), "Invalid frequency specified: INVALID");
552
553        Ok(())
554    }
555
556    #[test]
557    fn test_date_freq_to_string() {
558        assert_eq!(DateFreq::Daily.to_string(), "D");
559        assert_eq!(DateFreq::WeeklyMonday.to_string(), "W");
560        assert_eq!(DateFreq::MonthStart.to_string(), "M");
561        assert_eq!(DateFreq::QuarterStart.to_string(), "Q");
562        assert_eq!(DateFreq::YearStart.to_string(), "Y"); // Assert "Y"
563        assert_eq!(DateFreq::MonthEnd.to_string(), "ME");
564        assert_eq!(DateFreq::QuarterEnd.to_string(), "QE");
565        assert_eq!(DateFreq::WeeklyFriday.to_string(), "WF");
566        assert_eq!(DateFreq::YearEnd.to_string(), "YE");
567    }
568
569    #[test]
570    fn test_date_freq_from_string() -> Result<(), Box<dyn Error>> {
571        assert_eq!(DateFreq::from_string("D".to_string())?, DateFreq::Daily);
572        assert!(DateFreq::from_string("INVALID".to_string()).is_err());
573        Ok(())
574    }
575
576    #[test]
577    fn test_date_freq_agg_type() {
578        assert_eq!(DateFreq::Daily.agg_type(), AggregationType::Start);
579        assert_eq!(DateFreq::WeeklyMonday.agg_type(), AggregationType::Start);
580        assert_eq!(DateFreq::MonthStart.agg_type(), AggregationType::Start);
581        assert_eq!(DateFreq::QuarterStart.agg_type(), AggregationType::Start);
582        assert_eq!(DateFreq::YearStart.agg_type(), AggregationType::Start);
583
584        assert_eq!(DateFreq::WeeklyFriday.agg_type(), AggregationType::End);
585        assert_eq!(DateFreq::MonthEnd.agg_type(), AggregationType::End);
586        assert_eq!(DateFreq::QuarterEnd.agg_type(), AggregationType::End);
587        assert_eq!(DateFreq::YearEnd.agg_type(), AggregationType::End);
588    }
589
590    // --- BDatesList Property Tests ---
591
592    #[test]
593    fn test_bdates_list_properties_new() -> Result<(), Box<dyn Error>> {
594        let start_str = "2023-01-01".to_string();
595        let end_str = "2023-12-31".to_string();
596        let freq = DateFreq::QuarterEnd;
597        let dates_list = BDatesList::new(start_str.clone(), end_str.clone(), freq);
598
599        // check start_date_str
600        assert_eq!(dates_list.start_date_str(), start_str);
601        // check end_date_str
602        assert_eq!(dates_list.end_date_str(), end_str);
603        // check frequency enum
604        assert_eq!(dates_list.freq(), freq);
605        // check frequency string
606        assert_eq!(dates_list.freq_str(), "QE");
607
608        // Check parsed dates
609        assert_eq!(dates_list.start_date()?, date(2023, 1, 1));
610        assert_eq!(dates_list.end_date()?, date(2023, 12, 31));
611
612        Ok(())
613    }
614
615    #[test]
616    fn test_bdates_list_properties_from_n_periods() -> Result<(), Box<dyn Error>> {
617        let start_str = "2023-01-01".to_string(); // Sunday
618        let freq = DateFreq::Daily;
619        let n_periods = 5; // Expect: Jan 2, 3, 4, 5, 6
620        let dates_list = BDatesList::from_n_periods(start_str.clone(), freq, n_periods)?;
621
622        // check start_date_str (should be original)
623        assert_eq!(dates_list.start_date_str(), start_str);
624        // check end_date_str (should be the last generated date)
625        assert_eq!(dates_list.end_date_str(), "2023-01-06");
626        // check frequency enum
627        assert_eq!(dates_list.freq(), freq);
628        // check frequency string
629        assert_eq!(dates_list.freq_str(), "D");
630
631        // Check parsed dates
632        assert_eq!(dates_list.start_date()?, date(2023, 1, 1));
633        assert_eq!(dates_list.end_date()?, date(2023, 1, 6));
634
635        // Check the actual list matches
636        assert_eq!(
637            dates_list.list()?,
638            vec![
639                date(2023, 1, 2),
640                date(2023, 1, 3),
641                date(2023, 1, 4),
642                date(2023, 1, 5),
643                date(2023, 1, 6)
644            ]
645        );
646        assert_eq!(dates_list.count()?, 5);
647
648        Ok(())
649    }
650
651    #[test]
652    fn test_bdates_list_from_n_periods_zero_periods() {
653        let start_str = "2023-01-01".to_string();
654        let freq = DateFreq::Daily;
655        let n_periods = 0;
656        let result = BDatesList::from_n_periods(start_str.clone(), freq, n_periods);
657        assert!(result.is_err());
658        assert_eq!(
659            result.unwrap_err().to_string(),
660            "n_periods must be greater than 0"
661        );
662    }
663
664    #[test]
665    fn test_bdates_list_from_n_periods_invalid_start_date() {
666        let start_str = "invalid-date".to_string();
667        let freq = DateFreq::Daily;
668        let n_periods = 5;
669        let result = BDatesList::from_n_periods(start_str.clone(), freq, n_periods);
670        assert!(result.is_err());
671        // Error comes from NaiveDate::parse_from_str
672        assert!(result
673            .unwrap_err()
674            .to_string()
675            .contains("input contains invalid characters"));
676    }
677
678    #[test]
679    fn test_bdates_list_invalid_date_string_new() {
680        let dates_list_start_invalid = BDatesList::new(
681            "invalid-date".to_string(),
682            "2023-12-31".to_string(),
683            DateFreq::Daily,
684        );
685        assert!(dates_list_start_invalid.list().is_err());
686        assert!(dates_list_start_invalid.count().is_err());
687        assert!(dates_list_start_invalid.groups().is_err());
688        assert!(dates_list_start_invalid.start_date().is_err());
689        assert!(dates_list_start_invalid.end_date().is_ok()); // End date is valid
690
691        let dates_list_end_invalid = BDatesList::new(
692            "2023-01-01".to_string(),
693            "invalid-date".to_string(),
694            DateFreq::Daily,
695        );
696        assert!(dates_list_end_invalid.list().is_err());
697        assert!(dates_list_end_invalid.count().is_err());
698        assert!(dates_list_end_invalid.groups().is_err());
699        assert!(dates_list_end_invalid.start_date().is_ok()); // Start date is valid
700        assert!(dates_list_end_invalid.end_date().is_err());
701    }
702
703    // --- BDatesList Core Logic Tests (via list and count) ---
704
705    #[test]
706    /// Tests the `list()` method for QuarterEnd frequency over a full year.
707    fn test_bdates_list_quarterly_end_list() -> Result<(), Box<dyn Error>> {
708        let dates_list = BDatesList::new(
709            "2023-01-01".to_string(),
710            "2023-12-31".to_string(),
711            DateFreq::QuarterEnd,
712        );
713
714        let list = dates_list.list()?;
715        assert_eq!(list.len(), 4);
716        assert_eq!(
717            list,
718            vec![
719                date(2023, 3, 31),
720                date(2023, 6, 30),
721                date(2023, 9, 29),
722                date(2023, 12, 29)
723            ]
724        ); // Fri, Fri, Fri, Fri
725
726        Ok(())
727    }
728
729    #[test]
730    /// Tests the `list()` method for WeeklyMonday frequency.
731    fn test_bdates_list_weekly_monday_list() -> Result<(), Box<dyn Error>> {
732        // Range includes start date that is Monday, end date that is Sunday
733        let dates_list = BDatesList::new(
734            "2023-10-30".to_string(), // Monday (Week 44)
735            "2023-11-12".to_string(), // Sunday (Week 45 ends, Week 46 starts)
736            DateFreq::WeeklyMonday,
737        );
738
739        let list = dates_list.list()?;
740        // Mondays >= 2023-10-30 and <= 2023-11-12:
741        // 2023-10-30 (Included)
742        // 2023-11-06 (Included)
743        // 2023-11-13 (Excluded)
744        assert_eq!(list.len(), 2);
745        assert_eq!(list, vec![date(2023, 10, 30), date(2023, 11, 6)]);
746
747        Ok(())
748    }
749
750    #[test]
751    /// Tests the `list()` method for Daily frequency over a short range including weekends.
752    fn test_bdates_list_daily_list() -> Result<(), Box<dyn Error>> {
753        let dates_list = BDatesList::new(
754            "2023-11-01".to_string(), // Wednesday
755            "2023-11-05".to_string(), // Sunday
756            DateFreq::Daily,
757        );
758
759        let list = dates_list.list()?;
760        // Business days in range: Wed, Thu, Fri
761        assert_eq!(list.len(), 3);
762        assert_eq!(
763            list,
764            vec![date(2023, 11, 1), date(2023, 11, 2), date(2023, 11, 3)]
765        );
766
767        Ok(())
768    }
769
770    #[test]
771    /// Tests the `list()` method with an empty date range (end before start).
772    fn test_bdates_list_empty_range_list() -> Result<(), Box<dyn Error>> {
773        let dates_list = BDatesList::new(
774            "2023-12-31".to_string(),
775            "2023-01-01".to_string(), // End date before start date
776            DateFreq::Daily,
777        );
778        let list = dates_list.list()?;
779        assert!(list.is_empty());
780        assert_eq!(dates_list.count()?, 0); // Also test count here
781
782        Ok(())
783    }
784
785    #[test]
786    /// Tests the `count()` method for various frequencies.
787    fn test_bdates_list_count() -> Result<(), Box<dyn Error>> {
788        let dates_list = BDatesList::new(
789            "2023-01-01".to_string(),
790            "2023-12-31".to_string(),
791            DateFreq::MonthEnd,
792        );
793        assert_eq!(dates_list.count()?, 12, "{:?}", dates_list.list()); // 12 month ends in 2023
794
795        let dates_list_weekly = BDatesList::new(
796            "2023-11-01".to_string(), // Wed
797            "2023-11-30".to_string(), // Thu
798            DateFreq::WeeklyFriday,
799        );
800        // Fridays in range: 2023-11-03, 2023-11-10, 2023-11-17, 2023-11-24
801        assert_eq!(dates_list_weekly.count()?, 4);
802
803        Ok(())
804    }
805
806    #[test]
807    /// Tests `list()` and `count()` for YearlyStart frequency.
808    fn test_bdates_list_yearly_start() -> Result<(), Box<dyn Error>> {
809        let dates_list = BDatesList::new(
810            "2023-06-01".to_string(),
811            "2025-06-01".to_string(),
812            DateFreq::YearStart,
813        );
814        // Year starts >= 2023-06-01 and <= 2025-06-01:
815        // 2023-01-02 (Mon, Jan 1st is Sun) -> Excluded (< 2023-06-01)
816        // 2024-01-01 (Mon) -> Included
817        // 2025-01-01 (Wed) -> Included
818        assert_eq!(dates_list.list()?, vec![date(2024, 1, 1), date(2025, 1, 1)]);
819        assert_eq!(dates_list.count()?, 2);
820
821        Ok(())
822    }
823
824    #[test]
825    /// Tests `list()` and `count()` for MonthlyStart frequency.
826    fn test_bdates_list_monthly_start() -> Result<(), Box<dyn Error>> {
827        let dates_list = BDatesList::new(
828            "2023-11-15".to_string(), // Mid-Nov
829            "2024-02-15".to_string(), // Mid-Feb
830            DateFreq::MonthStart,
831        );
832        // Month starts >= 2023-11-15 and <= 2024-02-15:
833        // 2023-11-01 (Wed) -> Excluded (< 2023-11-15)
834        // 2023-12-01 (Fri) -> Included
835        // 2024-01-01 (Mon) -> Included
836        // 2024-02-01 (Thu) -> Included
837        // 2024-03-01 (Fri) -> Excluded (> 2024-02-15)
838        assert_eq!(
839            dates_list.list()?,
840            vec![date(2023, 12, 1), date(2024, 1, 1), date(2024, 2, 1)]
841        );
842        assert_eq!(dates_list.count()?, 3);
843
844        Ok(())
845    }
846
847    #[test]
848    /// Tests `list()` and `count()` for WeeklyFriday with a range ending mid-week.
849    fn test_bdates_list_weekly_friday_midweek_end() -> Result<(), Box<dyn Error>> {
850        let dates_list = BDatesList::new(
851            "2023-11-01".to_string(), // Wed (Week 44)
852            "2023-11-14".to_string(), // Tue (Week 46 starts on Mon 13th)
853            DateFreq::WeeklyFriday,
854        );
855        // Fridays >= 2023-11-01 and <= 2023-11-14:
856        // 2023-11-03 (Week 44) -> Included
857        // 2023-11-10 (Week 45) -> Included
858        // 2023-11-17 (Week 46) -> Excluded (> 2023-11-14)
859        assert_eq!(
860            dates_list.list()?,
861            vec![date(2023, 11, 3), date(2023, 11, 10)]
862        );
863        assert_eq!(dates_list.count()?, 2);
864
865        Ok(())
866    }
867
868    // --- Tests for groups() method ---
869
870    #[test]
871    /// Tests the `groups()` method for MonthlyEnd frequency across year boundary.
872    fn test_bdates_list_groups_monthly_end() -> Result<(), Box<dyn Error>> {
873        let dates_list = BDatesList::new(
874            "2023-10-15".to_string(), // Mid-October
875            "2024-01-15".to_string(), // Mid-January next year
876            DateFreq::MonthEnd,
877        );
878
879        let groups = dates_list.groups()?;
880        // Expected Month Ends within range ["2023-10-15", "2024-01-15"]:
881        // 2023-10-31 (>= 2023-10-15) -> Included
882        // 2023-11-30 (>= 2023-10-15) -> Included
883        // 2023-12-29 (>= 2023-10-15) -> Included
884        // 2024-01-31 (> 2024-01-15) -> Excluded
885        assert_eq!(groups.len(), 3);
886
887        // Check groups and dates within them (should be sorted by key, then by date).
888        // Keys: Monthly(2023, 10), Monthly(2023, 11), Monthly(2023, 12)
889        assert_eq!(groups[0], vec![date(2023, 10, 31)]); // Oct 2023 end
890        assert_eq!(groups[1], vec![date(2023, 11, 30)]); // Nov 2023 end
891        assert_eq!(groups[2], vec![date(2023, 12, 29)]); // Dec 2023 end (31st is Sunday)
892
893        Ok(())
894    }
895
896    #[test]
897    /// Tests the `groups()` method for Daily frequency over a short range.
898    fn test_bdates_list_groups_daily() -> Result<(), Box<dyn Error>> {
899        let dates_list = BDatesList::new(
900            "2023-11-01".to_string(), // Wed
901            "2023-11-05".to_string(), // Sun
902            DateFreq::Daily,
903        );
904
905        let groups = dates_list.groups()?;
906        // Business days in range: Wed, Thu, Fri. Each is its own group.
907        assert_eq!(groups.len(), 3);
908
909        // Keys: Daily(2023-11-01), Daily(2023-11-02), Daily(2023-11-03)
910        assert_eq!(groups[0], vec![date(2023, 11, 1)]);
911        assert_eq!(groups[1], vec![date(2023, 11, 2)]);
912        assert_eq!(groups[2], vec![date(2023, 11, 3)]);
913
914        Ok(())
915    }
916
917    #[test]
918    /// Tests the `groups()` method for WeeklyFriday frequency.
919    fn test_bdates_list_groups_weekly_friday() -> Result<(), Box<dyn Error>> {
920        let dates_list = BDatesList::new(
921            "2023-11-01".to_string(), // Wed (ISO Week 44)
922            "2023-11-15".to_string(), // Wed (ISO Week 46)
923            DateFreq::WeeklyFriday,
924        );
925
926        let groups = dates_list.groups()?;
927        // Fridays in range ["2023-11-01", "2023-11-15"]:
928        // 2023-11-03 (ISO Week 44) -> Included
929        // 2023-11-10 (ISO Week 45) -> Included
930        // 2023-11-17 (ISO Week 46) -> Excluded (> 2023-11-15)
931        assert_eq!(groups.len(), 2); // Groups for Week 44, Week 45
932
933        // Check grouping by ISO week
934        // Keys: Weekly(2023, 44), Weekly(2023, 45)
935        assert_eq!(groups[0], vec![date(2023, 11, 3)]); // ISO Week 44 group
936        assert_eq!(groups[1], vec![date(2023, 11, 10)]); // ISO Week 45 group
937
938        Ok(())
939    }
940
941    #[test]
942    /// Tests the `groups()` method for QuarterlyStart frequency spanning years.
943    fn test_bdates_list_groups_quarterly_start_spanning_years() -> Result<(), Box<dyn Error>> {
944        let dates_list = BDatesList::new(
945            "2023-08-01".to_string(), // Start date after Q3 2023 start business day
946            "2024-05-01".to_string(), // End date after Q2 2024 start business day
947            DateFreq::QuarterStart,
948        );
949
950        let groups = dates_list.groups()?;
951        // Quarterly starting business days *within the date range* ["2023-08-01", "2024-05-01"]:
952        // 2023-07-03 (Q3 2023 start) -> Excluded by start_date 2023-08-01
953        // 2023-10-02 (Q4 2023 start - Oct 1st is Sunday) -> Included
954        // 2024-01-01 (Q1 2024 start - Jan 1st is Monday) -> Included
955        // 2024-04-01 (Q2 2024 start) -> Included
956        // 2024-07-01 (Q3 2024 start) -> Excluded by end_date 2024-05-01
957
958        // Expected groups: Q4 2023, Q1 2024, Q2 2024
959        assert_eq!(groups.len(), 3);
960
961        // Check groups and dates within them (should be sorted by key, then by date)
962        // Key order: Quarterly(2023, 4), Quarterly(2024, 1), Quarterly(2024, 2)
963        assert_eq!(groups[0], vec![date(2023, 10, 2)]); // Q4 2023 group
964        assert_eq!(groups[1], vec![date(2024, 1, 1)]); // Q1 2024 group (Jan 1st 2024 was a Mon)
965        assert_eq!(groups[2], vec![date(2024, 4, 1)]); // Q2 2024 group
966
967        Ok(())
968    }
969
970    #[test]
971    /// Tests the `groups()` method for YearlyEnd frequency across year boundary.
972    fn test_bdates_list_groups_yearly_end() -> Result<(), Box<dyn Error>> {
973        let dates_list = BDatesList::new(
974            "2022-01-01".to_string(),
975            "2024-03-31".to_string(), // End date is Q1 2024
976            DateFreq::YearEnd,
977        );
978
979        let groups = dates_list.groups()?;
980        // Yearly ending business days *within the date range* ["2022-01-01", "2024-03-31"]:
981        // 2022-12-30 (Year 2022 end - 31st Sat) -> Included (>= 2022-01-01)
982        // 2023-12-29 (Year 2023 end - 31st Sun) -> Included (>= 2022-01-01)
983        // 2024-12-31 (Year 2024 end) -> Excluded because it's after 2024-03-31
984
985        // Expected groups: 2022, 2023
986        assert_eq!(groups.len(), 2);
987
988        // Check groups and dates within them (should be sorted by key, then by date)
989        // Key order: Yearly(2022), Yearly(2023)
990        assert_eq!(groups[0], vec![date(2022, 12, 30)]); // 2022 YE group
991        assert_eq!(groups[1], vec![date(2023, 12, 29)]); // 2023 YE group
992
993        Ok(())
994    }
995
996    #[test]
997    /// Tests the `groups()` method with an empty date range (end before start).
998    fn test_bdates_list_groups_empty_range() -> Result<(), Box<dyn Error>> {
999        let dates_list = BDatesList::new(
1000            "2023-12-31".to_string(),
1001            "2023-01-01".to_string(), // End date before start date
1002            DateFreq::Daily,
1003        );
1004        let groups = dates_list.groups()?;
1005        assert!(groups.is_empty());
1006
1007        Ok(())
1008    }
1009
1010    // --- Tests for BDatesGenerator ---
1011
1012    #[test]
1013    fn test_generator_new_zero_periods() -> Result<(), Box<dyn Error>> {
1014        let start_date = date(2023, 1, 1);
1015        let freq = DateFreq::Daily;
1016        let n_periods = 0;
1017        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1018        assert_eq!(generator.next(), None); // Should be immediately exhausted
1019        Ok(())
1020    }
1021
1022    #[test]
1023    fn test_generator_daily() -> Result<(), Box<dyn Error>> {
1024        let start_date = date(2023, 11, 10); // Friday
1025        let freq = DateFreq::Daily;
1026        let n_periods = 4;
1027        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1028
1029        assert_eq!(generator.next(), Some(date(2023, 11, 10))); // Fri
1030        assert_eq!(generator.next(), Some(date(2023, 11, 13))); // Mon
1031        assert_eq!(generator.next(), Some(date(2023, 11, 14))); // Tue
1032        assert_eq!(generator.next(), Some(date(2023, 11, 15))); // Wed
1033        assert_eq!(generator.next(), None); // Exhausted
1034
1035        // Test starting on weekend
1036        let start_date_sat = date(2023, 11, 11); // Saturday
1037        let mut generator_sat = BDatesGenerator::new(start_date_sat, freq, 2)?;
1038        assert_eq!(generator_sat.next(), Some(date(2023, 11, 13))); // Mon
1039        assert_eq!(generator_sat.next(), Some(date(2023, 11, 14))); // Tue
1040        assert_eq!(generator_sat.next(), None);
1041
1042        Ok(())
1043    }
1044
1045    #[test]
1046    fn test_generator_weekly_monday() -> Result<(), Box<dyn Error>> {
1047        let start_date = date(2023, 11, 8); // Wednesday
1048        let freq = DateFreq::WeeklyMonday;
1049        let n_periods = 3;
1050        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1051
1052        assert_eq!(generator.next(), Some(date(2023, 11, 13)));
1053        assert_eq!(generator.next(), Some(date(2023, 11, 20)));
1054        assert_eq!(generator.next(), Some(date(2023, 11, 27)));
1055        assert_eq!(generator.next(), None);
1056
1057        Ok(())
1058    }
1059
1060    #[test]
1061    fn test_generator_weekly_friday() -> Result<(), Box<dyn Error>> {
1062        let start_date = date(2023, 11, 11); // Saturday
1063        let freq = DateFreq::WeeklyFriday;
1064        let n_periods = 3;
1065        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1066
1067        assert_eq!(generator.next(), Some(date(2023, 11, 17)));
1068        assert_eq!(generator.next(), Some(date(2023, 11, 24)));
1069        assert_eq!(generator.next(), Some(date(2023, 12, 1)));
1070        assert_eq!(generator.next(), None);
1071
1072        Ok(())
1073    }
1074
1075    #[test]
1076    fn test_generator_month_start() -> Result<(), Box<dyn Error>> {
1077        let start_date = date(2023, 10, 15); // Mid-Oct
1078        let freq = DateFreq::MonthStart;
1079        let n_periods = 4; // Nov, Dec, Jan, Feb
1080        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1081
1082        assert_eq!(generator.next(), Some(date(2023, 11, 1)));
1083        assert_eq!(generator.next(), Some(date(2023, 12, 1)));
1084        assert_eq!(generator.next(), Some(date(2024, 1, 1)));
1085        assert_eq!(generator.next(), Some(date(2024, 2, 1)));
1086        assert_eq!(generator.next(), None);
1087
1088        Ok(())
1089    }
1090
1091    #[test]
1092    fn test_generator_month_end() -> Result<(), Box<dyn Error>> {
1093        let start_date = date(2023, 9, 30); // Sep 30 (Sat)
1094        let freq = DateFreq::MonthEnd;
1095        let n_periods = 4; // Oct, Nov, Dec, Jan
1096        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1097
1098        assert_eq!(generator.next(), Some(date(2023, 10, 31))); // Sep end was 29th < 30th, so start with Oct end
1099        assert_eq!(generator.next(), Some(date(2023, 11, 30)));
1100        assert_eq!(generator.next(), Some(date(2023, 12, 29)));
1101        assert_eq!(generator.next(), Some(date(2024, 1, 31)));
1102        assert_eq!(generator.next(), None);
1103
1104        Ok(())
1105    }
1106
1107    #[test]
1108    fn test_generator_quarter_start() -> Result<(), Box<dyn Error>> {
1109        let start_date = date(2023, 8, 1); // Mid-Q3
1110        let freq = DateFreq::QuarterStart;
1111        let n_periods = 3; // Q4'23, Q1'24, Q2'24
1112        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1113
1114        assert_eq!(generator.next(), Some(date(2023, 10, 2))); // Q3 start was Jul 3, < Aug 1. Next is Q4 start.
1115        assert_eq!(generator.next(), Some(date(2024, 1, 1)));
1116        assert_eq!(generator.next(), Some(date(2024, 4, 1)));
1117        assert_eq!(generator.next(), None);
1118
1119        Ok(())
1120    }
1121
1122    #[test]
1123    fn test_generator_quarter_end() -> Result<(), Box<dyn Error>> {
1124        let start_date = date(2023, 11, 1); // Mid-Q4
1125        let freq = DateFreq::QuarterEnd;
1126        let n_periods = 3; // Q4'23, Q1'24, Q2'24
1127        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1128
1129        assert_eq!(generator.next(), Some(date(2023, 12, 29))); // Q4 end is Dec 29 >= Nov 1
1130        assert_eq!(generator.next(), Some(date(2024, 3, 29))); // Q1 end (Mar 31 is Sun)
1131        assert_eq!(generator.next(), Some(date(2024, 6, 28))); // Q2 end (Jun 30 is Sun)
1132        assert_eq!(generator.next(), None);
1133
1134        Ok(())
1135    }
1136
1137    #[test]
1138    fn test_generator_year_start() -> Result<(), Box<dyn Error>> {
1139        let start_date = date(2023, 1, 1); // Jan 1 (Sun)
1140        let freq = DateFreq::YearStart;
1141        let n_periods = 3; // 2023, 2024, 2025
1142        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1143
1144        assert_eq!(generator.next(), Some(date(2023, 1, 2))); // 2023 start bday >= Jan 1
1145        assert_eq!(generator.next(), Some(date(2024, 1, 1)));
1146        assert_eq!(generator.next(), Some(date(2025, 1, 1)));
1147        assert_eq!(generator.next(), None);
1148
1149        Ok(())
1150    }
1151
1152    #[test]
1153    fn test_generator_year_end() -> Result<(), Box<dyn Error>> {
1154        let start_date = date(2022, 12, 31); // Dec 31 (Sat)
1155        let freq = DateFreq::YearEnd;
1156        let n_periods = 3; // 2023, 2024, 2025
1157        let mut generator = BDatesGenerator::new(start_date, freq, n_periods)?;
1158
1159        assert_eq!(generator.next(), Some(date(2023, 12, 29))); // 2022 end was Dec 30 < Dec 31. Next is 2023 end.
1160        assert_eq!(generator.next(), Some(date(2024, 12, 31)));
1161        assert_eq!(generator.next(), Some(date(2025, 12, 31)));
1162        assert_eq!(generator.next(), None);
1163
1164        Ok(())
1165    }
1166
1167    #[test]
1168    fn test_generator_collect() -> Result<(), Box<dyn Error>> {
1169        let start_date = date(2023, 11, 10); // Friday
1170        let freq = DateFreq::Daily;
1171        let n_periods = 4;
1172        let generator = BDatesGenerator::new(start_date, freq, n_periods)?; // Use non-mut binding for collect
1173        let dates: Vec<NaiveDate> = generator.collect();
1174
1175        assert_eq!(
1176            dates,
1177            vec![
1178                date(2023, 11, 10), // Fri
1179                date(2023, 11, 13), // Mon
1180                date(2023, 11, 14), // Tue
1181                date(2023, 11, 15)  // Wed
1182            ]
1183        );
1184        Ok(())
1185    }
1186}