mirror of
https://github.com/Magnus167/rustframe.git
synced 2025-08-20 04:00:01 +00:00
Merge pull request #14 from Magnus167/update_comments
Updating debug messages, error messages, and comments
This commit is contained in:
commit
af74682db5
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||||
|
@ -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 element‐wise 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 column‑wise (vertical).
|
/// Apply reduction along columns (vertical axis).
|
||||||
Col,
|
Col,
|
||||||
/// Operate row‑wise (horizontal).
|
/// Apply reduction along rows (horizontal axis).
|
||||||
Row,
|
Row,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::matrix::{Axis, BoolMatrix, FloatMatrix};
|
use crate::matrix::{Axis, BoolMatrix, FloatMatrix};
|
||||||
|
|
||||||
/// “Series–like” 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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 it’s 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 it’s 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),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user