mirror of
https://github.com/Magnus167/rustframe.git
synced 2025-08-20 04:00:01 +00:00
2172 lines
79 KiB
Rust
2172 lines
79 KiB
Rust
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<Self, Box<dyn Error>> {
|
|
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<dyn Error>;
|
|
|
|
/// 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<Self, Self::Err> {
|
|
let r = match freq {
|
|
"D" => DateFreq::Daily,
|
|
"W" | "WS" => DateFreq::WeeklyMonday,
|
|
"M" | "MS" => DateFreq::MonthStart,
|
|
"Q" | "QS" => DateFreq::QuarterStart,
|
|
"Y" | "A" | "AS" | "YS" => DateFreq::YearStart,
|
|
"ME" => DateFreq::MonthEnd,
|
|
"QE" => DateFreq::QuarterEnd,
|
|
"WF" => DateFreq::WeeklyFriday,
|
|
"YE" | "AE" => DateFreq::YearEnd,
|
|
_ => return Err(format!("Invalid frequency specified: {}", freq).into()),
|
|
};
|
|
Ok(r)
|
|
}
|
|
}
|
|
|
|
/// 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<dyn Error>> {
|
|
/// 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<dyn Error>> {
|
|
/// 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<dyn Error>> {
|
|
/// 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<Self, Box<dyn Error>> {
|
|
if n_periods == 0 {
|
|
return Err("n_periods must be greater than 0".into());
|
|
}
|
|
|
|
let start_date = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d")?;
|
|
|
|
// Use the generator to find all the dates
|
|
let generator = DatesGenerator::new(start_date, freq, n_periods)?;
|
|
let dates: Vec<NaiveDate> = 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
// 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<usize, Box<dyn Error>> {
|
|
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<Vec<Vec<NaiveDate>>, Box<dyn Error>> {
|
|
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, Box<dyn Error>> {
|
|
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, Box<dyn Error>> {
|
|
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<dyn Error>> {
|
|
/// 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<dyn Error>> {
|
|
/// 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<NaiveDate> = 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<dyn Error>> {
|
|
/// 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<NaiveDate>,
|
|
}
|
|
|
|
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<Self, Box<dyn Error>> {
|
|
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<Self::Item> {
|
|
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<NaiveDate>,
|
|
freq: DateFreq,
|
|
) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error + 'static>> {
|
|
let mut groups: HashMap<GroupKey, Vec<NaiveDate>> = HashMap::new();
|
|
|
|
for date in dates {
|
|
let key = match freq {
|
|
DateFreq::Daily => GroupKey::Daily(date),
|
|
DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => {
|
|
let iso_week = date.iso_week();
|
|
GroupKey::Weekly(iso_week.year(), iso_week.week())
|
|
}
|
|
DateFreq::MonthStart | DateFreq::MonthEnd => {
|
|
GroupKey::Monthly(date.year(), date.month())
|
|
}
|
|
DateFreq::QuarterStart | DateFreq::QuarterEnd => {
|
|
GroupKey::Quarterly(date.year(), month_to_quarter(date.month()))
|
|
}
|
|
DateFreq::YearStart | DateFreq::YearEnd => GroupKey::Yearly(date.year()),
|
|
};
|
|
groups.entry(key).or_insert_with(Vec::new).push(date);
|
|
}
|
|
|
|
let mut sorted_groups: Vec<(GroupKey, Vec<NaiveDate>)> = groups.into_iter().collect();
|
|
sorted_groups.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
|
|
|
|
// 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")?;
|
|
let end_date = NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")?;
|
|
|
|
if start_date > end_date {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let dates = match freq {
|
|
DateFreq::Daily => collect_daily(start_date, end_date)?,
|
|
DateFreq::WeeklyMonday => collect_weekly(start_date, end_date, Weekday::Mon)?,
|
|
DateFreq::WeeklyFriday => collect_weekly(start_date, end_date, Weekday::Fri)?,
|
|
DateFreq::MonthStart => {
|
|
collect_monthly(start_date, end_date, /*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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
let mut current = start_date;
|
|
while current <= end_date {
|
|
result.push(current);
|
|
current = current
|
|
.succ_opt()
|
|
.ok_or("Date overflow near end of supported range")?;
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
/// Returns the specified `target_weekday` in each week within the range.
|
|
fn collect_weekly(
|
|
start_date: NaiveDate,
|
|
end_date: NaiveDate,
|
|
target_weekday: Weekday,
|
|
) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
let mut current = move_to_day_of_week_on_or_after(start_date, target_weekday)?;
|
|
|
|
while current <= end_date {
|
|
result.push(current);
|
|
current = current
|
|
.checked_add_signed(Duration::days(7))
|
|
.ok_or("Date overflow adding 7 days")?;
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
/// 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
let mut year = start_date.year();
|
|
let mut month = start_date.month();
|
|
|
|
let next_month = |(yr, mo): (i32, u32)| -> (i32, u32) {
|
|
if mo == 12 {
|
|
(yr + 1, 1)
|
|
} else {
|
|
(yr, mo + 1)
|
|
}
|
|
};
|
|
|
|
loop {
|
|
let candidate = if want_first_day {
|
|
first_day_of_month(year, month)?
|
|
} else {
|
|
last_day_of_month(year, month)?
|
|
};
|
|
|
|
if candidate > end_date {
|
|
break;
|
|
}
|
|
|
|
if candidate >= start_date {
|
|
result.push(candidate);
|
|
}
|
|
|
|
if year > end_date.year() || (year == end_date.year() && month >= end_date.month()) {
|
|
break;
|
|
}
|
|
|
|
let (ny, nm) = next_month((year, month));
|
|
year = ny;
|
|
month = nm;
|
|
|
|
// 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
let mut year = start_date.year();
|
|
let mut q = month_to_quarter(start_date.month());
|
|
|
|
loop {
|
|
let candidate = if want_first_day {
|
|
first_day_of_quarter(year, q)?
|
|
} else {
|
|
last_day_of_quarter(year, q)?
|
|
};
|
|
|
|
if candidate > end_date {
|
|
break;
|
|
}
|
|
|
|
if candidate >= start_date {
|
|
result.push(candidate);
|
|
}
|
|
|
|
let end_q = month_to_quarter(end_date.month());
|
|
if year > end_date.year() || (year == end_date.year() && q >= end_q) {
|
|
break;
|
|
}
|
|
|
|
if q == 4 {
|
|
year += 1;
|
|
q = 1;
|
|
} else {
|
|
q += 1;
|
|
}
|
|
// 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
get_dates_list_with_freq(
|
|
&start_date.format("%Y-%m-%d").to_string(),
|
|
&end_date.format("%Y-%m-%d").to_string(),
|
|
freq,
|
|
)
|
|
}
|
|
|
|
/// 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<Vec<NaiveDate>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
let mut year = start_date.year();
|
|
|
|
while year <= end_date.year() {
|
|
let candidate = if want_first_day {
|
|
first_day_of_year(year)?
|
|
} else {
|
|
last_day_of_year(year)?
|
|
};
|
|
|
|
if candidate >= start_date && candidate <= end_date {
|
|
result.push(candidate);
|
|
} else if candidate > end_date {
|
|
// 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<NaiveDate, Box<dyn Error>> {
|
|
let mut current = date;
|
|
while current.weekday() != target {
|
|
current = current
|
|
.succ_opt()
|
|
.ok_or("Date overflow moving to next weekday")?;
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
/// Return the first calendar day of the given (year, month).
|
|
fn first_day_of_month(year: i32, month: u32) -> Result<NaiveDate, Box<dyn Error>> {
|
|
if !(1..=12).contains(&month) {
|
|
return Err(format!("Invalid month: {}", month).into());
|
|
}
|
|
NaiveDate::from_ymd_opt(year, month, 1)
|
|
.ok_or_else(|| format!("Invalid year-month combination: {}-{}", year, month).into())
|
|
}
|
|
/// Returns the number of days in a given month and year.
|
|
fn days_in_month(year: i32, month: u32) -> Result<u32, Box<dyn Error>> {
|
|
if !(1..=12).contains(&month) {
|
|
return Err(format!("Invalid month: {}", month).into());
|
|
}
|
|
let (ny, nm) = if month == 12 {
|
|
(
|
|
year.checked_add(1)
|
|
.ok_or("Year overflow calculating next month")?,
|
|
1,
|
|
)
|
|
} else {
|
|
(year, month + 1)
|
|
};
|
|
// 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<NaiveDate, Box<dyn Error>> {
|
|
// 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<u32, Box<dyn Error>> {
|
|
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<NaiveDate, Box<dyn Error>> {
|
|
// 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<u32, Box<dyn Error>> {
|
|
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<NaiveDate, Box<dyn Error>> {
|
|
// 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, Box<dyn Error>> {
|
|
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, Box<dyn Error>> {
|
|
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<NaiveDate, Box<dyn Error>> {
|
|
if matches!(
|
|
freq,
|
|
DateFreq::Daily | DateFreq::WeeklyMonday | DateFreq::WeeklyFriday
|
|
) {
|
|
panic!("Daily, WeeklyMonday, and WeeklyFriday frequencies are not supported here");
|
|
}
|
|
|
|
match freq {
|
|
DateFreq::MonthStart => first_day_of_month,
|
|
DateFreq::MonthEnd => last_day_of_month,
|
|
DateFreq::QuarterStart => first_day_of_quarter,
|
|
DateFreq::QuarterEnd => last_day_of_quarter,
|
|
DateFreq::YearStart => |year, _| first_day_of_year(year),
|
|
DateFreq::YearEnd => |year, _| last_day_of_year(year),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// 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<NaiveDate, Box<dyn Error>> {
|
|
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<NaiveDate, Box<dyn Error>> {
|
|
match freq {
|
|
DateFreq::Daily => current_date
|
|
.succ_opt()
|
|
.ok_or_else(|| "Date overflow finding next daily".into()),
|
|
DateFreq::WeeklyMonday | DateFreq::WeeklyFriday => current_date
|
|
.checked_add_signed(Duration::days(7))
|
|
.ok_or_else(|| "Date overflow adding 7 days".into()),
|
|
DateFreq::MonthStart | DateFreq::MonthEnd => {
|
|
let get_cand_func = get_first_date_helper(freq);
|
|
let (next_y, next_m) = if current_date.month() == 12 {
|
|
(
|
|
current_date.year().checked_add(1).ok_or("Year overflow")?,
|
|
1,
|
|
)
|
|
} else {
|
|
(current_date.year(), current_date.month() + 1)
|
|
};
|
|
get_cand_func(next_y, next_m)
|
|
}
|
|
DateFreq::QuarterStart | DateFreq::QuarterEnd => {
|
|
let current_q = month_to_quarter(current_date.month());
|
|
let get_cand_func = get_first_date_helper(freq);
|
|
let (next_y, next_q) = if current_q == 4 {
|
|
(
|
|
current_date.year().checked_add(1).ok_or("Year overflow")?,
|
|
1,
|
|
)
|
|
} else {
|
|
(current_date.year(), current_q + 1)
|
|
};
|
|
get_cand_func(next_y, next_q)
|
|
}
|
|
DateFreq::YearStart | DateFreq::YearEnd => {
|
|
let get_cand_func = get_first_date_helper(freq);
|
|
get_cand_func(
|
|
current_date.year().checked_add(1).ok_or("Year overflow")?,
|
|
0,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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<dyn Error>> {
|
|
assert_eq!(DateFreq::from_str("D")?, DateFreq::Daily);
|
|
assert_eq!("D".parse::<DateFreq>()?, DateFreq::Daily);
|
|
assert_eq!(DateFreq::from_str("W")?, DateFreq::WeeklyMonday);
|
|
assert_eq!(DateFreq::from_str("WS")?, DateFreq::WeeklyMonday);
|
|
assert_eq!(DateFreq::from_str("M")?, DateFreq::MonthStart);
|
|
assert_eq!(DateFreq::from_str("MS")?, DateFreq::MonthStart);
|
|
assert_eq!(DateFreq::from_str("Q")?, DateFreq::QuarterStart);
|
|
assert_eq!(DateFreq::from_str("QS")?, DateFreq::QuarterStart);
|
|
assert_eq!(DateFreq::from_str("Y")?, DateFreq::YearStart);
|
|
assert_eq!(DateFreq::from_str("A")?, DateFreq::YearStart);
|
|
assert_eq!(DateFreq::from_str("AS")?, DateFreq::YearStart);
|
|
assert_eq!(DateFreq::from_str("YS")?, DateFreq::YearStart);
|
|
assert_eq!(DateFreq::from_str("ME")?, DateFreq::MonthEnd);
|
|
assert_eq!(DateFreq::from_str("QE")?, DateFreq::QuarterEnd);
|
|
assert_eq!(DateFreq::from_str("WF")?, DateFreq::WeeklyFriday);
|
|
assert_eq!("WF".parse::<DateFreq>()?, DateFreq::WeeklyFriday);
|
|
assert_eq!(DateFreq::from_str("YE")?, DateFreq::YearEnd);
|
|
assert_eq!(DateFreq::from_str("AE")?, DateFreq::YearEnd);
|
|
|
|
assert!(DateFreq::from_str("INVALID").is_err());
|
|
assert!("INVALID".parse::<DateFreq>().is_err());
|
|
let err = DateFreq::from_str("INVALID").unwrap_err();
|
|
assert_eq!(err.to_string(), "Invalid frequency specified: INVALID");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_datefreq_to_string() {
|
|
assert_eq!(DateFreq::Daily.to_string(), "D");
|
|
assert_eq!(DateFreq::WeeklyMonday.to_string(), "W");
|
|
assert_eq!(DateFreq::MonthStart.to_string(), "M");
|
|
assert_eq!(DateFreq::QuarterStart.to_string(), "Q");
|
|
assert_eq!(DateFreq::YearStart.to_string(), "Y");
|
|
assert_eq!(DateFreq::MonthEnd.to_string(), "ME");
|
|
assert_eq!(DateFreq::QuarterEnd.to_string(), "QE");
|
|
assert_eq!(DateFreq::WeeklyFriday.to_string(), "WF");
|
|
assert_eq!(DateFreq::YearEnd.to_string(), "YE");
|
|
}
|
|
|
|
#[test]
|
|
fn test_datefreq_from_string() -> Result<(), Box<dyn Error>> {
|
|
assert_eq!(DateFreq::from_string("D".to_string())?, DateFreq::Daily);
|
|
assert!(DateFreq::from_string("INVALID".to_string()).is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_datefreq_agg_type() {
|
|
assert_eq!(DateFreq::Daily.agg_type(), AggregationType::Start);
|
|
assert_eq!(DateFreq::WeeklyMonday.agg_type(), AggregationType::Start);
|
|
assert_eq!(DateFreq::MonthStart.agg_type(), AggregationType::Start);
|
|
assert_eq!(DateFreq::QuarterStart.agg_type(), AggregationType::Start);
|
|
assert_eq!(DateFreq::YearStart.agg_type(), AggregationType::Start);
|
|
|
|
assert_eq!(DateFreq::WeeklyFriday.agg_type(), AggregationType::End);
|
|
assert_eq!(DateFreq::MonthEnd.agg_type(), AggregationType::End);
|
|
assert_eq!(DateFreq::QuarterEnd.agg_type(), AggregationType::End);
|
|
assert_eq!(DateFreq::YearEnd.agg_type(), AggregationType::End);
|
|
}
|
|
|
|
// --- DatesList Property Tests ---
|
|
|
|
#[test]
|
|
fn test_dates_list_properties_new() -> Result<(), Box<dyn Error>> {
|
|
let start_str = "2023-01-01".to_string();
|
|
let end_str = "2023-12-31".to_string();
|
|
let freq = DateFreq::QuarterEnd;
|
|
let dates_list = DatesList::new(start_str.clone(), end_str.clone(), freq);
|
|
|
|
assert_eq!(dates_list.start_date_str(), start_str);
|
|
assert_eq!(dates_list.end_date_str(), end_str);
|
|
assert_eq!(dates_list.freq(), freq);
|
|
assert_eq!(dates_list.freq_str(), "QE");
|
|
assert_eq!(dates_list.start_date()?, date(2023, 1, 1));
|
|
assert_eq!(dates_list.end_date()?, date(2023, 12, 31));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_dates_list_properties_from_n_periods() -> Result<(), Box<dyn Error>> {
|
|
let start_str = "2023-01-01".to_string(); // 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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
assert_eq!(
|
|
move_to_day_of_week_on_or_after(date(2023, 11, 6), Weekday::Mon)?,
|
|
date(2023, 11, 6)
|
|
);
|
|
assert_eq!(
|
|
move_to_day_of_week_on_or_after(date(2023, 11, 8), Weekday::Fri)?,
|
|
date(2023, 11, 10)
|
|
);
|
|
assert_eq!(
|
|
move_to_day_of_week_on_or_after(date(2023, 11, 11), Weekday::Mon)?, // 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<dyn Error>> {
|
|
assert_eq!(first_day_of_month(2023, 11)?, date(2023, 11, 1));
|
|
assert_eq!(first_day_of_month(2024, 2)?, date(2024, 2, 1));
|
|
assert!(first_day_of_month(2023, 0).is_err()); // Invalid month 0
|
|
assert!(first_day_of_month(2023, 13).is_err()); // Invalid month 13
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_days_in_month() -> Result<(), Box<dyn Error>> {
|
|
assert_eq!(days_in_month(2023, 1)?, 31);
|
|
assert_eq!(days_in_month(2023, 2)?, 28);
|
|
// 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<dyn Error>> {
|
|
assert_eq!(last_day_of_month(2023, 11)?, date(2023, 11, 30));
|
|
assert_eq!(last_day_of_month(2024, 2)?, date(2024, 2, 29)); // 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<dyn Error>> {
|
|
assert_eq!(first_day_of_quarter(2023, 1)?, date(2023, 1, 1));
|
|
assert_eq!(first_day_of_quarter(2023, 2)?, date(2023, 4, 1));
|
|
assert_eq!(first_day_of_quarter(2023, 3)?, date(2023, 7, 1));
|
|
assert_eq!(first_day_of_quarter(2023, 4)?, date(2023, 10, 1));
|
|
// 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<dyn Error>> {
|
|
assert_eq!(last_day_of_quarter(2023, 1)?, date(2023, 3, 31));
|
|
assert_eq!(last_day_of_quarter(2023, 2)?, date(2023, 6, 30));
|
|
assert_eq!(last_day_of_quarter(2023, 3)?, date(2023, 9, 30));
|
|
assert_eq!(last_day_of_quarter(2023, 4)?, date(2023, 12, 31));
|
|
// 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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
// 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<dyn Error>> {
|
|
// 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<dyn Error>> {
|
|
let start_date = date(2023, 1, 1);
|
|
let freq = DateFreq::Daily;
|
|
let n_periods = 0;
|
|
let mut generator = DatesGenerator::new(start_date, freq, n_periods)?;
|
|
assert_eq!(generator.next(), None); // Immediately exhausted
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_generator_new_fail_find_first() -> Result<(), Box<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<_>>(),
|
|
vec![
|
|
date(2023, 11, 10),
|
|
date(2023, 11, 11),
|
|
date(2023, 11, 12),
|
|
date(2023, 11, 13)
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_generator_weekly_monday() -> Result<(), Box<dyn Error>> {
|
|
let start_date = date(2023, 11, 8); // 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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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
|