Skip to main content

finance_dates/
range.rs

1//! Plain and business-day range iteration helpers.
2
3use chrono::{Datelike, Duration, NaiveDate};
4use std::collections::BTreeSet;
5
6/// Inclusive calendar-day range with a fixed integer step (days).
7pub 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
22/// Inclusive business-day range. `weekmask[i]` is true if weekday `i`
23/// (Mon=0 … Sun=6) is a business day. `holidays` is a set of dates
24/// to exclude regardless of weekday.
25pub 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
46/// Standard Mon–Fri weekmask.
47pub const STANDARD_WEEKMASK: [bool; 7] = [true, true, true, true, true, false, false];
48
49/// Move to the next business day strictly after `d`.
50pub 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
65/// Move to the previous business day strictly before `d`.
66pub 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
81/// Number of business days in [start, end] inclusive.
82pub 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        // 2024 has 262 weekdays.
121        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(); // Friday
133        let next = next_business_day(fri, &STANDARD_WEEKMASK, &BTreeSet::new());
134        assert_eq!(next, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
135    }
136}