Merge pull request #14 from Magnus167/update_comments

Updating debug messages, error messages, and comments
This commit is contained in:
Palash Tyagi 2025-04-24 18:06:17 +01:00 committed by GitHub
commit af74682db5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 474 additions and 549 deletions

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,6 @@ impl BoolOps for BoolMatrix {
self.data().iter().filter(|&&v| v).count() self.data().iter().filter(|&&v| v).count()
} }
} }
// use macros to generate the implementations for BitAnd, BitOr, BitXor, and Not
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -85,20 +85,22 @@ impl<T> Matrix<T> {
"column index out of bounds" "column index out of bounds"
); );
if c1 == c2 { if c1 == c2 {
return; // No-op if indices are the same // Indices are equal; no operation required
return;
} }
// Loop through each row // Iterate over each row to swap corresponding elements
for r in 0..self.rows { for r in 0..self.rows {
// Calculate the flat index for the element in row r, column c1 // Compute the one-dimensional index for (row r, column c1)
let idx1 = c1 * self.rows + r; let idx1 = c1 * self.rows + r;
// Calculate the flat index for the element in row r, column c2 // Compute the one-dimensional index for (row r, column c2)
let idx2 = c2 * self.rows + r; let idx2 = c2 * self.rows + r;
// Swap the elements directly in the data vector // Exchange the two elements in the internal data buffer
self.data.swap(idx1, idx2); self.data.swap(idx1, idx2);
} }
} }
/// Deletes a column from the matrix. /// Deletes a column from the matrix.
pub fn delete_column(&mut self, col: usize) { pub fn delete_column(&mut self, col: usize) {
assert!(col < self.cols, "column index out of bounds"); assert!(col < self.cols, "column index out of bounds");
@ -144,35 +146,45 @@ impl<T: Clone> Matrix<T> {
impl<T> Index<(usize, usize)> for Matrix<T> { impl<T> Index<(usize, usize)> for Matrix<T> {
type Output = T; type Output = T;
#[inline] #[inline]
fn index(&self, (r, c): (usize, usize)) -> &T { fn index(&self, (r, c): (usize, usize)) -> &T {
// Validate that the requested indices are within bounds
assert!(r < self.rows && c < self.cols, "index out of bounds"); assert!(r < self.rows && c < self.cols, "index out of bounds");
// Compute column-major offset and return reference
&self.data[c * self.rows + r] &self.data[c * self.rows + r]
} }
} }
impl<T> IndexMut<(usize, usize)> for Matrix<T> { impl<T> IndexMut<(usize, usize)> for Matrix<T> {
#[inline] #[inline]
fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut T { fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut T {
// Validate that the requested indices are within bounds
assert!(r < self.rows && c < self.cols, "index out of bounds"); assert!(r < self.rows && c < self.cols, "index out of bounds");
// Compute column-major offset and return mutable reference
&mut self.data[c * self.rows + r] &mut self.data[c * self.rows + r]
} }
} }
/// A view of one row /// Represents an immutable view of a single row in the matrix.
pub struct MatrixRow<'a, T> { pub struct MatrixRow<'a, T> {
matrix: &'a Matrix<T>, matrix: &'a Matrix<T>,
row: usize, row: usize,
} }
impl<'a, T> MatrixRow<'a, T> { impl<'a, T> MatrixRow<'a, T> {
/// Returns a reference to the element at the given column in this row.
pub fn get(&self, c: usize) -> &T { pub fn get(&self, c: usize) -> &T {
&self.matrix[(self.row, c)] &self.matrix[(self.row, c)]
} }
/// Returns an iterator over all elements in this row.
pub fn iter(&self) -> impl Iterator<Item = &T> { pub fn iter(&self) -> impl Iterator<Item = &T> {
(0..self.matrix.cols).map(move |c| &self.matrix[(self.row, c)]) (0..self.matrix.cols).map(move |c| &self.matrix[(self.row, c)])
} }
} }
/// Macro to generate elementwise impls for +, -, *, / /// Generates element-wise arithmetic implementations for matrices.
macro_rules! impl_elementwise_op { macro_rules! impl_elementwise_op {
($OpTrait:ident, $method:ident, $op:tt) => { ($OpTrait:ident, $method:ident, $op:tt) => {
impl<'a, 'b, T> std::ops::$OpTrait<&'b Matrix<T>> for &'a Matrix<T> impl<'a, 'b, T> std::ops::$OpTrait<&'b Matrix<T>> for &'a Matrix<T>
@ -182,8 +194,10 @@ macro_rules! impl_elementwise_op {
type Output = Matrix<T>; type Output = Matrix<T>;
fn $method(self, rhs: &'b Matrix<T>) -> Matrix<T> { fn $method(self, rhs: &'b Matrix<T>) -> Matrix<T> {
// Ensure both matrices have identical dimensions
assert_eq!(self.rows, rhs.rows, "row count mismatch"); assert_eq!(self.rows, rhs.rows, "row count mismatch");
assert_eq!(self.cols, rhs.cols, "col count mismatch"); assert_eq!(self.cols, rhs.cols, "col count mismatch");
// Apply the operation element-wise and collect into a new matrix
let data = self let data = self
.data .data
.iter() .iter()
@ -197,7 +211,7 @@ macro_rules! impl_elementwise_op {
}; };
} }
// invoke it 4 times: // Instantiate element-wise addition, subtraction, multiplication, and division
impl_elementwise_op!(Add, add, +); impl_elementwise_op!(Add, add, +);
impl_elementwise_op!(Sub, sub, -); impl_elementwise_op!(Sub, sub, -);
impl_elementwise_op!(Mul, mul, *); impl_elementwise_op!(Mul, mul, *);
@ -208,16 +222,17 @@ pub type BoolMatrix = Matrix<bool>;
pub type IntMatrix = Matrix<i32>; pub type IntMatrix = Matrix<i32>;
pub type StringMatrix = Matrix<String>; pub type StringMatrix = Matrix<String>;
// implement bit ops - and, or, xor, not -- using Macros /// Generates element-wise bitwise operations for boolean matrices.
macro_rules! impl_bitwise_op { macro_rules! impl_bitwise_op {
($OpTrait:ident, $method:ident, $op:tt) => { ($OpTrait:ident, $method:ident, $op:tt) => {
impl<'a, 'b> std::ops::$OpTrait<&'b Matrix<bool>> for &'a Matrix<bool> { impl<'a, 'b> std::ops::$OpTrait<&'b Matrix<bool>> for &'a Matrix<bool> {
type Output = Matrix<bool>; type Output = Matrix<bool>;
fn $method(self, rhs: &'b Matrix<bool>) -> Matrix<bool> { fn $method(self, rhs: &'b Matrix<bool>) -> Matrix<bool> {
// Ensure both matrices have identical dimensions
assert_eq!(self.rows, rhs.rows, "row count mismatch"); assert_eq!(self.rows, rhs.rows, "row count mismatch");
assert_eq!(self.cols, rhs.cols, "col count mismatch"); assert_eq!(self.cols, rhs.cols, "col count mismatch");
// Apply the bitwise operation element-wise
let data = self let data = self
.data .data
.iter() .iter()
@ -230,6 +245,8 @@ macro_rules! impl_bitwise_op {
} }
}; };
} }
// Instantiate bitwise AND, OR, and XOR for boolean matrices
impl_bitwise_op!(BitAnd, bitand, &); impl_bitwise_op!(BitAnd, bitand, &);
impl_bitwise_op!(BitOr, bitor, |); impl_bitwise_op!(BitOr, bitor, |);
impl_bitwise_op!(BitXor, bitxor, ^); impl_bitwise_op!(BitXor, bitxor, ^);
@ -238,6 +255,7 @@ impl Not for Matrix<bool> {
type Output = Matrix<bool>; type Output = Matrix<bool>;
fn not(self) -> Matrix<bool> { fn not(self) -> Matrix<bool> {
// Invert each boolean element in the matrix
let data = self.data.iter().map(|&v| !v).collect(); let data = self.data.iter().map(|&v| !v).collect();
Matrix { Matrix {
rows: self.rows, rows: self.rows,
@ -247,12 +265,12 @@ impl Not for Matrix<bool> {
} }
} }
/// Axis along which to apply a reduction. /// Specifies the axis along which to perform a reduction operation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Axis { pub enum Axis {
/// Operate columnwise (vertical). /// Apply reduction along columns (vertical axis).
Col, Col,
/// Operate rowwise (horizontal). /// Apply reduction along rows (horizontal axis).
Row, Row,
} }

View File

@ -1,6 +1,6 @@
use crate::matrix::{Axis, BoolMatrix, FloatMatrix}; use crate::matrix::{Axis, BoolMatrix, FloatMatrix};
/// “Serieslike” helpers that work along a single axis. /// "Series-like" helpers that work along a single axis.
/// ///
/// *All* the old methods (`sum_*`, `prod_*`, `is_nan`, …) are exposed /// *All* the old methods (`sum_*`, `prod_*`, `is_nan`, …) are exposed
/// through this trait, so nothing needs to stay on an `impl Matrix<f64>`; /// through this trait, so nothing needs to stay on an `impl Matrix<f64>`;
@ -100,7 +100,7 @@ impl SeriesOps for FloatMatrix {
} }
fn cumsum_horizontal(&self) -> FloatMatrix { fn cumsum_horizontal(&self) -> FloatMatrix {
// 1. Store row-wise cumulative sums temporarily // Compute cumulative sums for each row and store in a temporary buffer
let mut row_results: Vec<Vec<f64>> = Vec::with_capacity(self.rows()); let mut row_results: Vec<Vec<f64>> = Vec::with_capacity(self.rows());
for r in 0..self.rows() { for r in 0..self.rows() {
let mut row_data = Vec::with_capacity(self.cols()); let mut row_data = Vec::with_capacity(self.cols());
@ -115,16 +115,15 @@ impl SeriesOps for FloatMatrix {
row_results.push(row_data); row_results.push(row_data);
} }
// 2. Build the final data vector in column-major order // Assemble the final data vector in column-major format
let mut final_data = Vec::with_capacity(self.rows() * self.cols()); let mut final_data = Vec::with_capacity(self.rows() * self.cols());
for c in 0..self.cols() { for c in 0..self.cols() {
for r in 0..self.rows() { for r in 0..self.rows() {
// Get the element from row 'r', column 'c' of the row_results // Extract the element at (r, c) from the temporary row-wise results
final_data.push(row_results[r][c]); final_data.push(row_results[r][c]);
} }
} }
// 3. Construct the matrix using the correctly ordered data
FloatMatrix::from_vec(final_data, self.rows(), self.cols()) FloatMatrix::from_vec(final_data, self.rows(), self.cols())
} }
@ -146,7 +145,7 @@ impl SeriesOps for FloatMatrix {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// Helper function to create a FloatMatrix for SeriesOps testing // Helper function to create a FloatMatrix for SeriesOps testing
fn create_float_test_matrix() -> FloatMatrix { fn create_float_test_matrix() -> FloatMatrix {
// 3x3 matrix (column-major) with some NaNs // 3x3 matrix (column-major) with some NaNs
@ -361,4 +360,4 @@ mod tests {
assert_eq!(matrix.count_nan_horizontal(), vec![2, 2]); assert_eq!(matrix.count_nan_horizontal(), vec![2, 2]);
assert_eq!(matrix.is_nan().data(), &[true, true, true, true]); assert_eq!(matrix.is_nan().data(), &[true, true, true, true]);
} }
} }

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::hash::Hash; use std::hash::Hash;
use std::result::Result; use std::result::Result;
use std::str::FromStr; // Import FromStr trait use std::str::FromStr;
/// Represents the frequency at which business dates should be generated. /// Represents the frequency at which business dates should be generated.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -23,8 +23,8 @@ pub enum BDateFreq {
/// is selected for the frequency. /// is selected for the frequency.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregationType { pub enum AggregationType {
Start, // Indicates picking the first valid business date in a group's period. Start, // Select the first valid business day in the period
End, // Indicates picking the last valid business day in a group's period. End, // Select the last valid business day in the period
} }
impl BDateFreq { impl BDateFreq {
@ -40,7 +40,7 @@ impl BDateFreq {
/// ///
/// Returns an error if the string does not match any known frequency. /// Returns an error if the string does not match any known frequency.
pub fn from_string(freq: String) -> Result<Self, Box<dyn Error>> { pub fn from_string(freq: String) -> Result<Self, Box<dyn Error>> {
// Use the FromStr implementation directly // Delegate parsing to the FromStr implementation
freq.parse() freq.parse()
} }
@ -53,7 +53,7 @@ impl BDateFreq {
BDateFreq::WeeklyMonday => "W", BDateFreq::WeeklyMonday => "W",
BDateFreq::MonthStart => "M", BDateFreq::MonthStart => "M",
BDateFreq::QuarterStart => "Q", BDateFreq::QuarterStart => "Q",
BDateFreq::YearStart => "Y", // Changed to "Y" BDateFreq::YearStart => "Y",
BDateFreq::MonthEnd => "ME", BDateFreq::MonthEnd => "ME",
BDateFreq::QuarterEnd => "QE", BDateFreq::QuarterEnd => "QE",
BDateFreq::WeeklyFriday => "WF", BDateFreq::WeeklyFriday => "WF",
@ -112,11 +112,11 @@ impl FromStr for BDateFreq {
"W" | "WS" => BDateFreq::WeeklyMonday, "W" | "WS" => BDateFreq::WeeklyMonday,
"M" | "MS" => BDateFreq::MonthStart, "M" | "MS" => BDateFreq::MonthStart,
"Q" | "QS" => BDateFreq::QuarterStart, "Q" | "QS" => BDateFreq::QuarterStart,
"Y" | "A" | "AS" | "YS" => BDateFreq::YearStart, // Added Y, YS, A, AS aliases "Y" | "A" | "AS" | "YS" => BDateFreq::YearStart, // Support standard aliases for year start
"ME" => BDateFreq::MonthEnd, "ME" => BDateFreq::MonthEnd,
"QE" => BDateFreq::QuarterEnd, "QE" => BDateFreq::QuarterEnd,
"WF" => BDateFreq::WeeklyFriday, "WF" => BDateFreq::WeeklyFriday,
"YE" | "AE" => BDateFreq::YearEnd, // Added AE alias "YE" | "AE" => BDateFreq::YearEnd, // Include 'AE' alias for year end
_ => return Err(format!("Invalid frequency specified: {}", freq).into()), _ => return Err(format!("Invalid frequency specified: {}", freq).into()),
}; };
Ok(r) Ok(r)
@ -131,21 +131,19 @@ pub struct BDatesList {
start_date_str: String, start_date_str: String,
end_date_str: String, end_date_str: String,
freq: BDateFreq, freq: BDateFreq,
// Optional: Cache the generated list to avoid re-computation? // TODO: cache the generated date list to reduce repeated computation.
// For now, we recompute each time list(), count(), or groups() is called. // Currently, list(), count(), and groups() regenerate the list on every invocation.
// cached_list: Option<Vec<NaiveDate>>, // cached_list: Option<Vec<NaiveDate>>,
} }
// Helper enum to represent the key for grouping dates into periods. // Enumeration of period keys used for grouping dates.
// 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum GroupKey { enum GroupKey {
Daily(NaiveDate), // Group by the specific date (for Daily frequency) Daily(NaiveDate), // Daily grouping: use the exact date
Weekly(i32, u32), // Group by year and ISO week number Weekly(i32, u32), // Weekly grouping: use year and ISO week number
Monthly(i32, u32), // Group by year and month (1-12) Monthly(i32, u32), // Monthly grouping: use year and month (1-12)
Quarterly(i32, u32), // Group by year and quarter (1-4) Quarterly(i32, u32), // Quarterly grouping: use year and quarter (1-4)
Yearly(i32), // Group by year Yearly(i32), // Yearly grouping: use year
} }
/// Represents a collection of business dates generated according to specific rules. /// Represents a collection of business dates generated according to specific rules.
@ -168,23 +166,23 @@ enum GroupKey {
/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name /// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name
/// ///
/// fn main() -> Result<(), Box<dyn Error>> { /// fn main() -> Result<(), Box<dyn Error>> {
/// let start_date = "2023-11-01".to_string(); // Wednesday /// let start_date = "2023-11-01".to_string(); // Wednesday
/// let end_date = "2023-11-07".to_string(); // Tuesday /// let end_date = "2023-11-07".to_string(); // Tuesday
/// let freq = BDateFreq::Daily; /// let freq = BDateFreq::Daily;
/// ///
/// let bdates = BDatesList::new(start_date, end_date, freq); /// let bdates = BDatesList::new(start_date, end_date, freq);
/// ///
/// let expected_dates = vec![ /// let expected_dates = vec![
/// NaiveDate::from_ymd_opt(2023, 11, 1).unwrap(), // Wed /// NaiveDate::from_ymd_opt(2023, 11, 1).unwrap(), // Wed
/// NaiveDate::from_ymd_opt(2023, 11, 2).unwrap(), // Thu /// NaiveDate::from_ymd_opt(2023, 11, 2).unwrap(), // Thu
/// NaiveDate::from_ymd_opt(2023, 11, 3).unwrap(), // Fri /// NaiveDate::from_ymd_opt(2023, 11, 3).unwrap(), // Fri
/// NaiveDate::from_ymd_opt(2023, 11, 6).unwrap(), // Mon /// NaiveDate::from_ymd_opt(2023, 11, 6).unwrap(), // Mon
/// NaiveDate::from_ymd_opt(2023, 11, 7).unwrap(), // Tue /// NaiveDate::from_ymd_opt(2023, 11, 7).unwrap(), // Tue
/// ]; /// ];
/// ///
/// assert_eq!(bdates.list()?, expected_dates); /// assert_eq!(bdates.list()?, expected_dates);
/// assert_eq!(bdates.count()?, 5); /// assert_eq!(bdates.count()?, 5);
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
/// ///
@ -196,25 +194,25 @@ enum GroupKey {
/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name /// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name
/// ///
/// fn main() -> Result<(), Box<dyn Error>> { /// fn main() -> Result<(), Box<dyn Error>> {
/// let start_date = "2024-02-28".to_string(); // Wednesday /// let start_date = "2024-02-28".to_string(); // Wednesday
/// let freq = BDateFreq::WeeklyFriday; /// let freq = BDateFreq::WeeklyFriday;
/// let n_periods = 3; /// let n_periods = 3;
/// ///
/// let bdates = BDatesList::from_n_periods(start_date, freq, n_periods)?; /// let bdates = BDatesList::from_n_periods(start_date, freq, n_periods)?;
/// ///
/// // The first Friday on or after 2024-02-28 is Mar 1. /// // The first Friday on or after 2024-02-28 is Mar 1.
/// // The next two Fridays are Mar 8 and Mar 15. /// // The next two Fridays are Mar 8 and Mar 15.
/// let expected_dates = vec![ /// let expected_dates = vec![
/// NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(), /// NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
/// NaiveDate::from_ymd_opt(2024, 3, 8).unwrap(), /// NaiveDate::from_ymd_opt(2024, 3, 8).unwrap(),
/// NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(), /// NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
/// ]; /// ];
/// ///
/// assert_eq!(bdates.list()?, expected_dates); /// assert_eq!(bdates.list()?, expected_dates);
/// assert_eq!(bdates.count()?, 3); /// assert_eq!(bdates.count()?, 3);
/// assert_eq!(bdates.start_date_str(), "2024-02-28"); // Keeps original start string /// assert_eq!(bdates.start_date_str(), "2024-02-28"); // Keeps original start string
/// assert_eq!(bdates.end_date_str(), "2024-03-15"); // End date is the last generated date /// assert_eq!(bdates.end_date_str(), "2024-03-15"); // End date is the last generated date
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
/// ///
@ -226,20 +224,20 @@ enum GroupKey {
/// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name /// use rustframe::utils::{BDatesList, BDateFreq}; // Replace bdates with your actual crate/module name
/// ///
/// fn main() -> Result<(), Box<dyn Error>> { /// fn main() -> Result<(), Box<dyn Error>> {
/// let start_date = "2023-11-20".to_string(); // Mon, Week 47 /// let start_date = "2023-11-20".to_string(); // Mon, Week 47
/// let end_date = "2023-12-08".to_string(); // Fri, Week 49 /// let end_date = "2023-12-08".to_string(); // Fri, Week 49
/// let freq = BDateFreq::WeeklyMonday; /// let freq = BDateFreq::WeeklyMonday;
/// ///
/// let bdates = BDatesList::new(start_date, end_date, freq); /// let bdates = BDatesList::new(start_date, end_date, freq);
/// ///
/// // Mondays in range: Nov 20, Nov 27, Dec 4 /// // Mondays in range: Nov 20, Nov 27, Dec 4
/// let groups = bdates.groups()?; /// let groups = bdates.groups()?;
/// ///
/// assert_eq!(groups.len(), 3); // One group per week containing a Monday /// assert_eq!(groups.len(), 3); // One group per week containing a Monday
/// assert_eq!(groups[0], vec![NaiveDate::from_ymd_opt(2023, 11, 20).unwrap()]); // Week 47 /// assert_eq!(groups[0], vec![NaiveDate::from_ymd_opt(2023, 11, 20).unwrap()]); // Week 47
/// assert_eq!(groups[1], vec![NaiveDate::from_ymd_opt(2023, 11, 27).unwrap()]); // Week 48 /// assert_eq!(groups[1], vec![NaiveDate::from_ymd_opt(2023, 11, 27).unwrap()]); // Week 48
/// assert_eq!(groups[2], vec![NaiveDate::from_ymd_opt(2023, 12, 4).unwrap()]); // Week 49 /// assert_eq!(groups[2], vec![NaiveDate::from_ymd_opt(2023, 12, 4).unwrap()]); // Week 49
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
impl BDatesList { impl BDatesList {
@ -286,19 +284,19 @@ impl BDatesList {
let start_date = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d")?; let start_date = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d")?;
// Use the generator to find all the dates // Instantiate the date generator to compute the sequence of business dates.
let generator = BDatesGenerator::new(start_date, freq, n_periods)?; let generator = BDatesGenerator::new(start_date, freq, n_periods)?;
let dates: Vec<NaiveDate> = generator.collect(); let dates: Vec<NaiveDate> = generator.collect();
// Should always have at least one date if n_periods > 0 and generator construction succeeded // Confirm that the generator returned at least one date when n_periods > 0.
let last_date = dates let last_date = dates
.last() .last()
.ok_or("Generator failed to produce dates even though n_periods > 0")?; .ok_or("Generator failed to produce dates for the specified periods")?;
let end_date_str = last_date.format("%Y-%m-%d").to_string(); let end_date_str = last_date.format("%Y-%m-%d").to_string();
Ok(BDatesList { Ok(BDatesList {
start_date_str, // Keep the original start date string start_date_str,
end_date_str, end_date_str,
freq, freq,
}) })
@ -312,7 +310,7 @@ impl BDatesList {
/// ///
/// Returns an error if the start or end date strings cannot be parsed. /// Returns an error if the start or end date strings cannot be parsed.
pub fn list(&self) -> Result<Vec<NaiveDate>, Box<dyn Error>> { pub fn list(&self) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
// Delegate the core logic to the internal helper function // Retrieve the list of business dates via the shared helper function.
get_bdates_list_with_freq(&self.start_date_str, &self.end_date_str, self.freq) get_bdates_list_with_freq(&self.start_date_str, &self.end_date_str, self.freq)
} }
@ -320,41 +318,33 @@ impl BDatesList {
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error if the start or end date strings cannot be parsed (as it /// Returns an error if the start or end date strings cannot be parsed.
/// calls `list` internally).
pub fn count(&self) -> Result<usize, Box<dyn Error>> { pub fn count(&self) -> Result<usize, Box<dyn Error>> {
// Get the list and return its length. Uses map to handle the Result elegantly. // Compute the total number of business dates by invoking `list()` and returning its length.
self.list().map(|list| list.len()) self.list().map(|list| list.len())
} }
/// Returns a list of date lists, where each inner list contains dates /// Returns a list of date lists, where each inner list contains dates
/// belonging to the same period (determined by frequency). /// belonging to the same period (determined by frequency).
/// ///
/// The outer list (groups) is sorted by period chronologically, and the /// The outer list (groups) is sorted chronologically by period, and the
/// inner lists (dates within groups) are also sorted chronologically. /// inner lists (dates within each period) are also sorted.
///
/// 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 /// # Errors
/// ///
/// Returns an error if the start or end date strings cannot be parsed. /// Returns an error if the start or end date strings cannot be parsed.
pub fn groups(&self) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error>> { pub fn groups(&self) -> Result<Vec<Vec<NaiveDate>>, Box<dyn Error>> {
// Get the sorted list of all dates first. This sorted order is crucial // Retrieve all business dates in chronological order.
// for ensuring the inner vectors (dates within groups) are also sorted
// as we insert into the HashMap.
let dates = self.list()?; let dates = self.list()?;
// Use a HashMap to collect dates into their respective groups. // Aggregate dates into buckets keyed by period.
let mut groups: HashMap<GroupKey, Vec<NaiveDate>> = HashMap::new(); let mut groups: HashMap<GroupKey, Vec<NaiveDate>> = HashMap::new();
for date in dates { for date in dates {
// Determine the grouping key based on frequency. // Derive the appropriate GroupKey for the current date based on the configured frequency.
let key = match self.freq { let key = match self.freq {
BDateFreq::Daily => GroupKey::Daily(date), BDateFreq::Daily => GroupKey::Daily(date),
BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => { BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => {
// Use ISO week number for consistent weekly grouping across year boundaries
let iso_week = date.iso_week(); let iso_week = date.iso_week();
GroupKey::Weekly(iso_week.year(), iso_week.week()) GroupKey::Weekly(iso_week.year(), iso_week.week())
} }
@ -367,30 +357,19 @@ impl BDatesList {
BDateFreq::YearStart | BDateFreq::YearEnd => GroupKey::Yearly(date.year()), BDateFreq::YearStart | BDateFreq::YearEnd => GroupKey::Yearly(date.year()),
}; };
// Add the current date to the vector corresponding to the determined key. // Append the date to its period group.
// entry().or_insert_with() gets a mutable reference to the vector for the key, groups.entry(key).or_insert_with(Vec::new).push(date);
// inserting a new empty vector if the key doesn't exist yet.
groups.entry(key).or_insert_with(Vec::new).push(date); // Using or_insert_with is slightly more idiomatic
} }
// Convert the HashMap into a vector of (key, vector_of_dates) tuples. // Transform the group map into a vector of (GroupKey, Vec<NaiveDate>) tuples.
let mut sorted_groups: Vec<(GroupKey, Vec<NaiveDate>)> = groups.into_iter().collect(); let mut sorted_groups: Vec<(GroupKey, Vec<NaiveDate>)> = groups.into_iter().collect();
// Sort the vector of groups by the `GroupKey`. Since `GroupKey` derives `Ord`, // Sort groups chronologically using the derived `Ord` implementation on `GroupKey`.
// this sorts the groups chronologically (Yearly < Quarterly < Monthly < Weekly < Daily,
// then by year, quarter, month, week, or date within each category).
sorted_groups.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); sorted_groups.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
// The dates *within* each group (`Vec<NaiveDate>`) are already sorted // Note: Dates within each group remain sorted due to initial ordered input.
// because they were pushed in the order they appeared in the initially
// sorted `dates` vector obtained from `self.list()`.
// If the source `dates` wasn't guaranteed sorted, or for clarity,
// an inner sort could be added here:
// for (_, dates_in_group) in sorted_groups.iter_mut() {
// dates_in_group.sort();
// }
// Extract just the vectors of dates from the sorted tuples, discarding the keys. // Discard group keys to return only the list of date vectors.
let result_groups = sorted_groups.into_iter().map(|(_, dates)| dates).collect(); let result_groups = sorted_groups.into_iter().map(|(_, dates)| dates).collect();
Ok(result_groups) Ok(result_groups)
@ -435,7 +414,7 @@ impl BDatesList {
} }
} }
// --- Business Date Generator (Iterator) --- // Business date iterator: generates a sequence of business dates for a given frequency and period count.
/// An iterator that generates a sequence of business dates based on a start date, /// An iterator that generates a sequence of business dates based on a start date,
/// frequency, and a specified number of periods. /// frequency, and a specified number of periods.
@ -451,22 +430,22 @@ impl BDatesList {
/// ```rust /// ```rust
/// use chrono::NaiveDate; /// use chrono::NaiveDate;
/// use std::error::Error; /// use std::error::Error;
/// use rustframe::utils::{BDatesGenerator, BDateFreq}; /// use rustframe::utils::{BDatesGenerator, BDateFreq};
/// ///
/// fn main() -> Result<(), Box<dyn Error>> { /// fn main() -> Result<(), Box<dyn Error>> {
/// let start = NaiveDate::from_ymd_opt(2023, 12, 28).unwrap(); // Thursday /// let start = NaiveDate::from_ymd_opt(2023, 12, 28).unwrap(); // Thursday
/// let freq = BDateFreq::MonthEnd; /// let freq = BDateFreq::MonthEnd;
/// let n_periods = 4; // Dec '23, Jan '24, Feb '24, Mar '24 /// let n_periods = 4; // Dec '23, Jan '24, Feb '24, Mar '24
/// ///
/// let mut generator = BDatesGenerator::new(start, freq, n_periods)?; /// let mut generator = BDatesGenerator::new(start, freq, n_periods)?;
/// ///
/// // First month-end on or after 2023-12-28 is 2023-12-29 /// // First month-end on or after 2023-12-28 is 2023-12-29
/// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2023, 12, 29).unwrap())); /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2023, 12, 29).unwrap()));
/// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 1, 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, 2, 29).unwrap())); // Leap year
/// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 3, 29).unwrap())); // Mar 31 is Sun /// assert_eq!(generator.next(), Some(NaiveDate::from_ymd_opt(2024, 3, 29).unwrap())); // Mar 31 is Sun
/// assert_eq!(generator.next(), None); // Exhausted /// assert_eq!(generator.next(), None); // Exhausted
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
/// ///
@ -478,31 +457,30 @@ impl BDatesList {
/// use rustframe::utils::{BDatesGenerator, BDateFreq}; // Replace bdates with your actual crate/module name /// use rustframe::utils::{BDatesGenerator, BDateFreq}; // Replace bdates with your actual crate/module name
/// ///
/// fn main() -> Result<(), Box<dyn Error>> { /// fn main() -> Result<(), Box<dyn Error>> {
/// let start = NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(); // Monday /// let start = NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(); // Monday
/// let freq = BDateFreq::Daily; /// let freq = BDateFreq::Daily;
/// let n_periods = 5; /// let n_periods = 5;
/// ///
/// let generator = BDatesGenerator::new(start, freq, n_periods)?; /// let generator = BDatesGenerator::new(start, freq, n_periods)?;
/// let dates: Vec<NaiveDate> = generator.collect(); /// let dates: Vec<NaiveDate> = generator.collect();
/// ///
/// let expected_dates = vec![ /// let expected_dates = vec![
/// NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(), // Mon /// NaiveDate::from_ymd_opt(2024, 4, 29).unwrap(), // Mon
/// NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(), // Tue /// NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(), // Tue
/// NaiveDate::from_ymd_opt(2024, 5, 1).unwrap(), // Wed /// NaiveDate::from_ymd_opt(2024, 5, 1).unwrap(), // Wed
/// NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(), // Thu /// NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(), // Thu
/// NaiveDate::from_ymd_opt(2024, 5, 3).unwrap(), // Fri /// NaiveDate::from_ymd_opt(2024, 5, 3).unwrap(), // Fri
/// ]; /// ];
/// ///
/// assert_eq!(dates, expected_dates); /// assert_eq!(dates, expected_dates);
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BDatesGenerator { pub struct BDatesGenerator {
freq: BDateFreq, freq: BDateFreq,
periods_remaining: usize, periods_remaining: usize,
// Stores the *next* date to be yielded by the iterator. // Next business date candidate to yield; None when iteration is complete.
// This is None initially or when the iterator is exhausted.
next_date_candidate: Option<NaiveDate>, next_date_candidate: Option<NaiveDate>,
} }
@ -532,7 +510,8 @@ impl BDatesGenerator {
let first_date = if n_periods > 0 { let first_date = if n_periods > 0 {
Some(find_first_bdate_on_or_after(start_date, freq)) Some(find_first_bdate_on_or_after(start_date, freq))
} else { } else {
None // No dates to generate if n_periods is 0 // No dates when period count is zero.
None
}; };
Ok(BDatesGenerator { Ok(BDatesGenerator {
@ -546,29 +525,29 @@ impl BDatesGenerator {
impl Iterator for BDatesGenerator { impl Iterator for BDatesGenerator {
type Item = NaiveDate; type Item = NaiveDate;
/// Returns the next business date in the sequence, or `None` if `n_periods` /// Returns the next business date in the sequence, or `None` if the specified
/// dates have already been generated. /// number of periods has been generated.
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
// Check if exhausted or if there was no initial date // Terminate if no periods remain or no initial date is set.
if self.periods_remaining == 0 || self.next_date_candidate.is_none() { if self.periods_remaining == 0 || self.next_date_candidate.is_none() {
return None; return None;
} }
// Get the date to return (unwrap is safe due to the check above) // Retrieve and store the current date for output.
let current_date = self.next_date_candidate.unwrap(); let current_date = self.next_date_candidate.unwrap();
// Prepare the *next* candidate for the subsequent call // Compute and queue the subsequent date for the next call.
self.next_date_candidate = Some(find_next_bdate(current_date, self.freq)); self.next_date_candidate = Some(find_next_bdate(current_date, self.freq));
// Decrement the count // Decrement the remaining period count.
self.periods_remaining -= 1; self.periods_remaining -= 1;
// Return the stored current date // Yield the current business date.
Some(current_date) Some(current_date)
} }
} }
// --- Internal helper functions (not part of the public API) --- // Internal helper functions (private implementation)
/// Generates the flat list of business dates for the given range and frequency. /// Generates the flat list of business dates for the given range and frequency.
/// ///
@ -589,70 +568,58 @@ fn get_bdates_list_with_freq(
end_date_str: &str, end_date_str: &str,
freq: BDateFreq, freq: BDateFreq,
) -> Result<Vec<NaiveDate>, Box<dyn Error>> { ) -> Result<Vec<NaiveDate>, Box<dyn Error>> {
// Parse the start and end dates, returning error if parsing fails. // Parse input date strings; propagate parsing errors.
let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")?; let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")?;
let end_date = NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")?; let end_date = NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")?;
// Handle edge case where end date is before start date. // Return empty list immediately if the date range is invalid.
if start_date > end_date { if start_date > end_date {
return Ok(Vec::new()); return Ok(Vec::new());
} }
// Collect dates based on the specified frequency. // Generate dates according to the requested frequency.
let mut dates = match freq { let mut dates = match freq {
BDateFreq::Daily => collect_daily(start_date, end_date), BDateFreq::Daily => collect_daily(start_date, end_date),
BDateFreq::WeeklyMonday => collect_weekly(start_date, end_date, Weekday::Mon), BDateFreq::WeeklyMonday => collect_weekly(start_date, end_date, Weekday::Mon),
BDateFreq::WeeklyFriday => collect_weekly(start_date, end_date, Weekday::Fri), BDateFreq::WeeklyFriday => collect_weekly(start_date, end_date, Weekday::Fri),
BDateFreq::MonthStart => { BDateFreq::MonthStart => collect_monthly(start_date, end_date, true),
collect_monthly(start_date, end_date, /*want_first_day=*/ true) BDateFreq::MonthEnd => collect_monthly(start_date, end_date, false),
} BDateFreq::QuarterStart => collect_quarterly(start_date, end_date, true),
BDateFreq::MonthEnd => { BDateFreq::QuarterEnd => collect_quarterly(start_date, end_date, false),
collect_monthly(start_date, end_date, /*want_first_day=*/ false) BDateFreq::YearStart => collect_yearly(start_date, end_date, true),
} BDateFreq::YearEnd => collect_yearly(start_date, end_date, false),
BDateFreq::QuarterStart => {
collect_quarterly(start_date, end_date, /*want_first_day=*/ true)
}
BDateFreq::QuarterEnd => {
collect_quarterly(start_date, end_date, /*want_first_day=*/ false)
}
BDateFreq::YearStart => collect_yearly(start_date, end_date, /*want_first_day=*/ true),
BDateFreq::YearEnd => collect_yearly(start_date, end_date, /*want_first_day=*/ false),
}; };
// Filter out any weekend days. While the core logic aims for business days, // Exclude weekends to ensure only business days remain.
// this ensures robustness against edge cases where computed dates might fall
// on a weekend (e.g., first day of month being Saturday).
// Note: This retain is redundant if collect_* functions are correct, but adds safety.
// It's essential for Daily, less so for others if they always return bdays.
dates.retain(|d| is_weekday(*d)); dates.retain(|d| is_weekday(*d));
// Ensure the final list is sorted. The `collect_*` functions generally // Guarantee chronological order of the result.
// produce sorted output, but an explicit sort guarantees it.
dates.sort(); dates.sort();
Ok(dates) Ok(dates)
} }
/* ---------------------- Low-Level Date Collection Functions (Internal) ---------------------- */ /* Low-level date collection routines (private) */
// These functions generate dates within a *range* [start_date, end_date]
/// Returns all business days (Mon-Fri) day-by-day within the range. /// Returns all weekdays from `start_date` through `end_date`, inclusive.
fn collect_daily(start_date: NaiveDate, end_date: NaiveDate) -> Vec<NaiveDate> { fn collect_daily(start_date: NaiveDate, end_date: NaiveDate) -> Vec<NaiveDate> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut current = start_date; let mut current = start_date;
// Iterate one day at a time.
while current <= end_date { while current <= end_date {
if is_weekday(current) { if is_weekday(current) {
result.push(current); result.push(current);
} }
// Use succ_opt() and expect(), assuming valid date range and no overflow in practical scenarios
current = current current = current
.succ_opt() .succ_opt()
.expect("date overflow near end of supported range"); .expect("Date overflow near end of supported range");
} }
result result
} }
/// Returns the specified `target_weekday` in each week within the range. /// Returns each occurrence of `target_weekday` within the date range.
fn collect_weekly( fn collect_weekly(
start_date: NaiveDate, start_date: NaiveDate,
end_date: NaiveDate, end_date: NaiveDate,
@ -660,79 +627,70 @@ fn collect_weekly(
) -> Vec<NaiveDate> { ) -> Vec<NaiveDate> {
let mut result = Vec::new(); let mut result = Vec::new();
// Find the first target_weekday on or after the start date. // Find the first matching weekday on or after the start date.
let mut current = move_to_weekday_on_or_after(start_date, target_weekday); let mut current = move_to_weekday_on_or_after(start_date, target_weekday);
// Step through the range in 7-day increments. // Step through each week until exceeding the end date.
while current <= end_date { while current <= end_date {
// Ensure the found date is actually a weekday (should be Mon/Fri but belt-and-suspenders) // Only include if still a weekday.
if is_weekday(current) { if is_weekday(current) {
result.push(current); result.push(current);
} }
// Use checked_add_signed for safety, though basic addition is likely fine for 7 days.
current = current current = current
.checked_add_signed(Duration::days(7)) .checked_add_signed(Duration::days(7))
.expect("date overflow adding 7 days"); .expect("Date overflow when advancing by one week");
} }
result result
} }
/// Returns either the first or last business day in each month of the range. /// Returns either the first or last business day of each month in the range.
fn collect_monthly( fn collect_monthly(
start_date: NaiveDate, start_date: NaiveDate,
end_date: NaiveDate, end_date: NaiveDate,
want_first_day: bool, want_first_day: bool,
) -> Vec<NaiveDate> { ) -> Vec<NaiveDate> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut year = start_date.year(); let mut year = start_date.year();
let mut month = start_date.month(); let mut month = start_date.month();
// Helper closure to advance year and month by one month. // Advance (year, month) by one month.
let next_month = let next_month = |(yr, mo): (i32, u32)| {
|(yr, mo): (i32, u32)| -> (i32, u32) { if mo == 12 { (yr + 1, 1) } else { (yr, mo + 1) } }; if mo == 12 { (yr + 1, 1) } else { (yr, mo + 1) }
};
// Iterate month by month from the start date's month up to or past the end date's month. // Iterate months from the start date until past the end date.
loop { loop {
// Compute the candidate date (first or last business day) for the current month. // Determine the candidate business date for this month.
// Use _opt and expect(), expecting valid month/year combinations within realistic ranges.
let candidate = if want_first_day { let candidate = if want_first_day {
first_business_day_of_month(year, month) first_business_day_of_month(year, month)
} else { } else {
last_business_day_of_month(year, month) last_business_day_of_month(year, month)
}; };
// If the candidate is after the end date, we've gone past the range, so stop. // Stop if the candidate is beyond the allowed range.
if candidate > end_date { if candidate > end_date {
break; break;
} }
// If the candidate is within the specified range [start_date, end_date], add it. // Include candidate if it falls within [start_date, end_date].
if candidate >= start_date { if candidate >= start_date && is_weekday(candidate) {
// Ensure it's actually a weekday (should be, but adds safety) result.push(candidate);
if is_weekday(candidate) {
result.push(candidate);
}
} }
// Note: We don't break if candidate < start_date because a later month's candidate
// might be within the range.
// Check if the current month is the last month we should process // If we've processed the end date's month, terminate.
if year > end_date.year() || (year == end_date.year() && month >= end_date.month()) { if year > end_date.year() || (year == end_date.year() && month >= end_date.month()) {
// If we just processed the end_date's month, stop.
// Need >= because we need to include the end date's month itself if its candidate is valid.
break; break;
} }
// Advance to the next month. // Move to the next month.
let (ny, nm) = next_month((year, month)); let (ny, nm) = next_month((year, month));
year = ny; year = ny;
month = nm; month = nm;
// Safety break: Stop if we have moved clearly past the end date's year. // Safety guard against unexpected infinite loops.
// This check is technically redundant given the loop condition above, but harmless.
if year > end_date.year() + 1 { if year > end_date.year() + 1 {
break; // Avoid potential infinite loops in unexpected scenarios break;
} }
} }
@ -864,7 +822,7 @@ fn move_to_weekday_on_or_after(date: NaiveDate, target: Weekday) -> NaiveDate {
fn first_business_day_of_month(year: i32, month: u32) -> NaiveDate { fn first_business_day_of_month(year: i32, month: u32) -> NaiveDate {
// Start with the 1st of the month. Use _opt and expect(), assuming valid Y/M. // Start with the 1st of the month. Use _opt and expect(), assuming valid Y/M.
let mut d = NaiveDate::from_ymd_opt(year, month, 1).expect("invalid year-month combination"); let mut d = NaiveDate::from_ymd_opt(year, month, 1).expect("invalid year-month combination");
// If its Sat/Sun, move forward until we find a weekday. // If it's Sat/Sun, move forward until we find a weekday.
while !is_weekday(d) { while !is_weekday(d) {
// Use succ_opt() and expect(), assuming valid date and no overflow. // Use succ_opt() and expect(), assuming valid date and no overflow.
d = d.succ_opt().expect("date overflow finding first bday"); d = d.succ_opt().expect("date overflow finding first bday");
@ -872,112 +830,128 @@ fn first_business_day_of_month(year: i32, month: u32) -> NaiveDate {
d d
} }
/// Return the latest business day of the given (year, month). /// Returns the last business day (Monday-Friday) of the specified month.
///
/// Calculates the number of days in the month, then steps backward from the
/// month's last day until a weekday is found.
///
/// # Panics
/// Panics if date construction or predecessor operations underflow.
fn last_business_day_of_month(year: i32, month: u32) -> NaiveDate { fn last_business_day_of_month(year: i32, month: u32) -> NaiveDate {
let last_dom = days_in_month(year, month); let last_dom = days_in_month(year, month);
// Use _opt and expect(), assuming valid Y/M/D combination.
let mut d = let mut d =
NaiveDate::from_ymd_opt(year, month, last_dom).expect("invalid year-month-day combination"); NaiveDate::from_ymd_opt(year, month, last_dom).expect("Invalid year-month-day combination");
// If its Sat/Sun, move backward until we find a weekday.
while !is_weekday(d) { while !is_weekday(d) {
// Use pred_opt() and expect(), assuming valid date and no underflow. d = d
d = d.pred_opt().expect("date underflow finding last bday"); .pred_opt()
.expect("Date underflow finding last business day");
} }
d d
} }
/// Returns the number of days in a given month and year. /// Returns the number of days in the specified month, correctly handling leap years.
///
/// Determines the first day of the next month and subtracts one day.
///
/// # Panics
/// Panics if date construction or predecessor operations underflow.
fn days_in_month(year: i32, month: u32) -> u32 { fn days_in_month(year: i32, month: u32) -> u32 {
// A common trick: find the first day of the *next* month, then subtract one day.
// This correctly handles leap years.
let (ny, nm) = if month == 12 { let (ny, nm) = if month == 12 {
(year + 1, 1) (year + 1, 1)
} else { } else {
(year, month + 1) (year, month + 1)
}; };
// Use _opt and expect(), assuming valid Y/M combination (start of next month). let first_of_next =
let first_of_next = NaiveDate::from_ymd_opt(ny, nm, 1).expect("invalid next year-month"); NaiveDate::from_ymd_opt(ny, nm, 1).expect("Invalid next year-month combination");
// Use pred_opt() and expect(), assuming valid date and no underflow (first of month - 1).
let last_of_this = first_of_next let last_of_this = first_of_next
.pred_opt() .pred_opt()
.expect("invalid date before first of month"); .expect("Date underflow computing last day of month");
last_of_this.day() last_of_this.day()
} }
/// Converts a month number (1-12) to a quarter number (1-4). /// Maps a month (1-12) to its corresponding quarter (1-4).
///
/// # Panics
/// Panics if `m` is outside the range 1-12.
fn month_to_quarter(m: u32) -> u32 { fn month_to_quarter(m: u32) -> u32 {
match m { match m {
1..=3 => 1, 1..=3 => 1,
4..=6 => 2, 4..=6 => 2,
7..=9 => 3, 7..=9 => 3,
10..=12 => 4, 10..=12 => 4,
_ => panic!("Invalid month: {}", m), // Should not happen with valid dates _ => panic!("Invalid month: {}", m),
} }
} }
/// Returns the 1st day of the month that starts a given (year, quarter). /// Returns the starting month (1, 4, 7, or 10) for the given quarter (1-4).
///
/// # Panics
/// Panics if `quarter` is not in 1-4.
fn quarter_start_month(quarter: u32) -> u32 { fn quarter_start_month(quarter: u32) -> u32 {
match quarter { match quarter {
1 => 1, 1 => 1,
2 => 4, 2 => 4,
3 => 7, 3 => 7,
4 => 10, 4 => 10,
_ => panic!("invalid quarter: {}", quarter), // This function expects quarter 1-4 _ => panic!("Invalid quarter: {}", quarter),
} }
} }
/// Return the earliest business day in the given (year, quarter). /// Returns the first business day (Monday-Friday) of the specified quarter.
///
/// Delegates to `first_business_day_of_month` using the quarter's start month.
fn first_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate { fn first_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate {
let month = quarter_start_month(quarter); let month = quarter_start_month(quarter);
first_business_day_of_month(year, month) first_business_day_of_month(year, month)
} }
/// Return the last business day in the given (year, quarter). /// Returns the last business day (Monday-Friday) of the specified quarter.
///
/// Determines the quarter's final month and delegates to
/// `last_business_day_of_month`.
fn last_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate { fn last_business_day_of_quarter(year: i32, quarter: u32) -> NaiveDate {
// The last month of a quarter is the start month + 2. let last_month = quarter * 3;
let last_month_in_quarter = match quarter { last_business_day_of_month(year, last_month)
1 => 3,
2 => 6,
3 => 9,
4 => 12,
_ => panic!("invalid quarter: {}", quarter),
};
last_business_day_of_month(year, last_month_in_quarter)
} }
/// Returns the earliest business day (Mon-Fri) of the given year. /// Returns the first business day (Monday-Friday) of the specified year.
///
/// Starts at January 1st and advances to the next weekday if needed.
///
/// # Panics
/// Panics if date construction or successor operations overflow.
fn first_business_day_of_year(year: i32) -> NaiveDate { fn first_business_day_of_year(year: i32) -> NaiveDate {
// Start with Jan 1st. Use _opt and expect(), assuming valid Y/M/D combination. let mut d = NaiveDate::from_ymd_opt(year, 1, 1).expect("Invalid year for January 1st");
let mut d = NaiveDate::from_ymd_opt(year, 1, 1).expect("invalid year for Jan 1st");
// If Jan 1st is a weekend, move forward to the next weekday.
while !is_weekday(d) { while !is_weekday(d) {
// Use succ_opt() and expect(), assuming valid date and no overflow.
d = d d = d
.succ_opt() .succ_opt()
.expect("date overflow finding first bday of year"); .expect("Date overflow finding first business day of year");
} }
d d
} }
/// Returns the last business day (Mon-Fri) of the given year. /// Returns the last business day (Monday-Friday) of the specified year.
///
/// Starts at December 31st and moves backward to the previous weekday if needed.
///
/// # Panics
/// Panics if date construction or predecessor operations underflow.
fn last_business_day_of_year(year: i32) -> NaiveDate { fn last_business_day_of_year(year: i32) -> NaiveDate {
// Start with Dec 31st. Use _opt and expect(), assuming valid Y/M/D combination. let mut d = NaiveDate::from_ymd_opt(year, 12, 31).expect("Invalid year for December 31st");
let mut d = NaiveDate::from_ymd_opt(year, 12, 31).expect("invalid year for Dec 31st");
// If Dec 31st is a weekend, move backward to the previous weekday.
while !is_weekday(d) { while !is_weekday(d) {
// Use pred_opt() and expect(), assuming valid date and no underflow.
d = d d = d
.pred_opt() .pred_opt()
.expect("date underflow finding last bday of year"); .expect("Date underflow finding last business day of year");
} }
d d
} }
// --- Generator Helper Functions --- /// Finds the first valid business date on or after `start_date` according to `freq`.
///
/// Finds the *first* valid business date according to the frequency, /// This may advance across days, weeks, months, quarters, or years depending on `freq`.
/// starting the search *on or after* the given `start_date`. ///
/// Panics on date overflow/underflow in extreme cases, but generally safe. /// # Panics
/// Panics on extreme date overflows or underflows.
fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> NaiveDate { fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> NaiveDate {
match freq { match freq {
BDateFreq::Daily => { BDateFreq::Daily => {
@ -994,28 +968,24 @@ fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> Naive
BDateFreq::MonthStart => { BDateFreq::MonthStart => {
let mut candidate = first_business_day_of_month(start_date.year(), start_date.month()); let mut candidate = first_business_day_of_month(start_date.year(), start_date.month());
if candidate < start_date { if candidate < start_date {
// If the first bday of the current month is before start_date, let (ny, nm) = if start_date.month() == 12 {
// we need the first bday of the *next* month.
let (next_y, next_m) = if start_date.month() == 12 {
(start_date.year() + 1, 1) (start_date.year() + 1, 1)
} else { } else {
(start_date.year(), start_date.month() + 1) (start_date.year(), start_date.month() + 1)
}; };
candidate = first_business_day_of_month(next_y, next_m); candidate = first_business_day_of_month(ny, nm);
} }
candidate candidate
} }
BDateFreq::MonthEnd => { BDateFreq::MonthEnd => {
let mut candidate = last_business_day_of_month(start_date.year(), start_date.month()); let mut candidate = last_business_day_of_month(start_date.year(), start_date.month());
if candidate < start_date { if candidate < start_date {
// If the last bday of current month is before start_date, let (ny, nm) = if start_date.month() == 12 {
// we need the last bday of the *next* month.
let (next_y, next_m) = if start_date.month() == 12 {
(start_date.year() + 1, 1) (start_date.year() + 1, 1)
} else { } else {
(start_date.year(), start_date.month() + 1) (start_date.year(), start_date.month() + 1)
}; };
candidate = last_business_day_of_month(next_y, next_m); candidate = last_business_day_of_month(ny, nm);
} }
candidate candidate
} }
@ -1023,14 +993,12 @@ fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> Naive
let current_q = month_to_quarter(start_date.month()); let current_q = month_to_quarter(start_date.month());
let mut candidate = first_business_day_of_quarter(start_date.year(), current_q); let mut candidate = first_business_day_of_quarter(start_date.year(), current_q);
if candidate < start_date { if candidate < start_date {
// If the first bday of the current quarter is before start_date, let (ny, nq) = if current_q == 4 {
// we need the first bday of the *next* quarter.
let (next_y, next_q) = if current_q == 4 {
(start_date.year() + 1, 1) (start_date.year() + 1, 1)
} else { } else {
(start_date.year(), current_q + 1) (start_date.year(), current_q + 1)
}; };
candidate = first_business_day_of_quarter(next_y, next_q); candidate = first_business_day_of_quarter(ny, nq);
} }
candidate candidate
} }
@ -1038,22 +1006,18 @@ fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> Naive
let current_q = month_to_quarter(start_date.month()); let current_q = month_to_quarter(start_date.month());
let mut candidate = last_business_day_of_quarter(start_date.year(), current_q); let mut candidate = last_business_day_of_quarter(start_date.year(), current_q);
if candidate < start_date { if candidate < start_date {
// If the last bday of the current quarter is before start_date, let (ny, nq) = if current_q == 4 {
// we need the last bday of the *next* quarter.
let (next_y, next_q) = if current_q == 4 {
(start_date.year() + 1, 1) (start_date.year() + 1, 1)
} else { } else {
(start_date.year(), current_q + 1) (start_date.year(), current_q + 1)
}; };
candidate = last_business_day_of_quarter(next_y, next_q); candidate = last_business_day_of_quarter(ny, nq);
} }
candidate candidate
} }
BDateFreq::YearStart => { BDateFreq::YearStart => {
let mut candidate = first_business_day_of_year(start_date.year()); let mut candidate = first_business_day_of_year(start_date.year());
if candidate < start_date { if candidate < start_date {
// If the first bday of the current year is before start_date,
// we need the first bday of the *next* year.
candidate = first_business_day_of_year(start_date.year() + 1); candidate = first_business_day_of_year(start_date.year() + 1);
} }
candidate candidate
@ -1061,8 +1025,6 @@ fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> Naive
BDateFreq::YearEnd => { BDateFreq::YearEnd => {
let mut candidate = last_business_day_of_year(start_date.year()); let mut candidate = last_business_day_of_year(start_date.year());
if candidate < start_date { if candidate < start_date {
// If the last bday of the current year is before start_date,
// we need the last bday of the *next* year.
candidate = last_business_day_of_year(start_date.year() + 1); candidate = last_business_day_of_year(start_date.year() + 1);
} }
candidate candidate
@ -1070,15 +1032,19 @@ fn find_first_bdate_on_or_after(start_date: NaiveDate, freq: BDateFreq) -> Naive
} }
} }
/// Finds the *next* valid business date according to the frequency, /// Finds the next business date after `current_date` according to `freq`.
/// given the `current_date` (which is assumed to be a valid date previously generated). ///
/// Panics on date overflow/underflow in extreme cases, but generally safe. /// Assumes `current_date` was previously generated. Advances by days, weeks,
/// months, quarters, or years as specified.
///
/// # Panics
/// Panics on extreme date overflows or underflows.
fn find_next_bdate(current_date: NaiveDate, freq: BDateFreq) -> NaiveDate { fn find_next_bdate(current_date: NaiveDate, freq: BDateFreq) -> NaiveDate {
match freq { match freq {
BDateFreq::Daily => { BDateFreq::Daily => {
let mut next_day = current_date let mut next_day = current_date
.succ_opt() .succ_opt()
.expect("Date overflow finding next daily"); .expect("Date overflow finding next daily date");
while !is_weekday(next_day) { while !is_weekday(next_day) {
next_day = next_day next_day = next_day
.succ_opt() .succ_opt()
@ -1086,45 +1052,42 @@ fn find_next_bdate(current_date: NaiveDate, freq: BDateFreq) -> NaiveDate {
} }
next_day next_day
} }
BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => { BDateFreq::WeeklyMonday | BDateFreq::WeeklyFriday => current_date
// Assuming current_date is already a Mon/Fri, the next one is 7 days later. .checked_add_signed(Duration::days(7))
current_date .expect("Date overflow adding one week"),
.checked_add_signed(Duration::days(7))
.expect("Date overflow adding 7 days")
}
BDateFreq::MonthStart => { BDateFreq::MonthStart => {
let (next_y, next_m) = if current_date.month() == 12 { let (ny, nm) = if current_date.month() == 12 {
(current_date.year() + 1, 1) (current_date.year() + 1, 1)
} else { } else {
(current_date.year(), current_date.month() + 1) (current_date.year(), current_date.month() + 1)
}; };
first_business_day_of_month(next_y, next_m) first_business_day_of_month(ny, nm)
} }
BDateFreq::MonthEnd => { BDateFreq::MonthEnd => {
let (next_y, next_m) = if current_date.month() == 12 { let (ny, nm) = if current_date.month() == 12 {
(current_date.year() + 1, 1) (current_date.year() + 1, 1)
} else { } else {
(current_date.year(), current_date.month() + 1) (current_date.year(), current_date.month() + 1)
}; };
last_business_day_of_month(next_y, next_m) last_business_day_of_month(ny, nm)
} }
BDateFreq::QuarterStart => { BDateFreq::QuarterStart => {
let current_q = month_to_quarter(current_date.month()); let current_q = month_to_quarter(current_date.month());
let (next_y, next_q) = if current_q == 4 { let (ny, nq) = if current_q == 4 {
(current_date.year() + 1, 1) (current_date.year() + 1, 1)
} else { } else {
(current_date.year(), current_q + 1) (current_date.year(), current_q + 1)
}; };
first_business_day_of_quarter(next_y, next_q) first_business_day_of_quarter(ny, nq)
} }
BDateFreq::QuarterEnd => { BDateFreq::QuarterEnd => {
let current_q = month_to_quarter(current_date.month()); let current_q = month_to_quarter(current_date.month());
let (next_y, next_q) = if current_q == 4 { let (ny, nq) = if current_q == 4 {
(current_date.year() + 1, 1) (current_date.year() + 1, 1)
} else { } else {
(current_date.year(), current_q + 1) (current_date.year(), current_q + 1)
}; };
last_business_day_of_quarter(next_y, next_q) last_business_day_of_quarter(ny, nq)
} }
BDateFreq::YearStart => first_business_day_of_year(current_date.year() + 1), BDateFreq::YearStart => first_business_day_of_year(current_date.year() + 1),
BDateFreq::YearEnd => last_business_day_of_year(current_date.year() + 1), BDateFreq::YearEnd => last_business_day_of_year(current_date.year() + 1),