1use chrono::{Datelike, Duration, NaiveDate};
4use std::collections::BTreeSet;
5
6pub fn date_range(start: NaiveDate, end: NaiveDate, step_days: u32) -> Vec<NaiveDate> {
8 if step_days == 0 || end < start {
9 return Vec::new();
10 }
11 let total = (end - start).num_days();
12 let cap = (total / step_days as i64) as usize + 1;
13 let mut out = Vec::with_capacity(cap);
14 let mut d = start;
15 while d <= end {
16 out.push(d);
17 d += Duration::days(step_days as i64);
18 }
19 out
20}
21
22pub fn business_day_range(
26 start: NaiveDate,
27 end: NaiveDate,
28 weekmask: &[bool; 7],
29 holidays: &BTreeSet<NaiveDate>,
30) -> Vec<NaiveDate> {
31 if end < start {
32 return Vec::new();
33 }
34 let mut out = Vec::with_capacity(((end - start).num_days() as usize).saturating_add(1));
35 let mut d = start;
36 while d <= end {
37 let i = d.weekday().num_days_from_monday() as usize;
38 if weekmask[i] && !holidays.contains(&d) {
39 out.push(d);
40 }
41 d += Duration::days(1);
42 }
43 out
44}
45
46pub const STANDARD_WEEKMASK: [bool; 7] = [true, true, true, true, true, false, false];
48
49pub fn next_business_day(
51 d: NaiveDate,
52 weekmask: &[bool; 7],
53 holidays: &BTreeSet<NaiveDate>,
54) -> NaiveDate {
55 let mut x = d + Duration::days(1);
56 loop {
57 let i = x.weekday().num_days_from_monday() as usize;
58 if weekmask[i] && !holidays.contains(&x) {
59 return x;
60 }
61 x += Duration::days(1);
62 }
63}
64
65pub fn previous_business_day(
67 d: NaiveDate,
68 weekmask: &[bool; 7],
69 holidays: &BTreeSet<NaiveDate>,
70) -> NaiveDate {
71 let mut x = d - Duration::days(1);
72 loop {
73 let i = x.weekday().num_days_from_monday() as usize;
74 if weekmask[i] && !holidays.contains(&x) {
75 return x;
76 }
77 x -= Duration::days(1);
78 }
79}
80
81pub fn business_days_between(
83 start: NaiveDate,
84 end: NaiveDate,
85 weekmask: &[bool; 7],
86 holidays: &BTreeSet<NaiveDate>,
87) -> i64 {
88 if end < start {
89 return 0;
90 }
91 let mut n = 0i64;
92 let mut d = start;
93 while d <= end {
94 let i = d.weekday().num_days_from_monday() as usize;
95 if weekmask[i] && !holidays.contains(&d) {
96 n += 1;
97 }
98 d += Duration::days(1);
99 }
100 n
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use chrono::NaiveDate;
107
108 #[test]
109 fn date_range_unit() {
110 let out = date_range(
111 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
112 NaiveDate::from_ymd_opt(2024, 1, 5).unwrap(),
113 1,
114 );
115 assert_eq!(out.len(), 5);
116 }
117
118 #[test]
119 fn business_days_2024_no_holidays() {
120 let bd = business_days_between(
122 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
123 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
124 &STANDARD_WEEKMASK,
125 &BTreeSet::new(),
126 );
127 assert_eq!(bd, 262);
128 }
129
130 #[test]
131 fn next_business_day_skips_weekend() {
132 let fri = NaiveDate::from_ymd_opt(2024, 5, 24).unwrap(); let next = next_business_day(fri, &STANDARD_WEEKMASK, &BTreeSet::new());
134 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
135 }
136}