1use chrono::prelude::*;
2use chrono::Days;
3use pyo3::exceptions::PyValueError;
4use pyo3::PyErr;
5use std::cmp::Ordering;
6
7use crate::scheduling::{Adjuster, Adjustment};
8
9pub trait DateRoll {
11 fn is_weekday(&self, date: &NaiveDateTime) -> bool;
13
14 fn is_holiday(&self, date: &NaiveDateTime) -> bool;
16
17 fn is_settlement(&self, date: &NaiveDateTime) -> bool;
22
23 fn is_bus_day(&self, date: &NaiveDateTime) -> bool {
25 self.is_weekday(date) && !self.is_holiday(date)
26 }
27
28 fn is_non_bus_day(&self, date: &NaiveDateTime) -> bool {
30 !self.is_bus_day(date)
31 }
32
33 fn roll_forward_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
35 let mut new_date = *date;
36 while !self.is_bus_day(&new_date) {
37 new_date = new_date + Days::new(1);
38 }
39 new_date
40 }
41
42 fn roll_backward_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
44 let mut new_date = *date;
45 while !self.is_bus_day(&new_date) {
46 new_date = new_date - Days::new(1);
47 }
48 new_date
49 }
50
51 fn roll_mod_forward_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
54 let new_date = self.roll_forward_bus_day(date);
55 if new_date.month() != date.month() {
56 self.roll_backward_bus_day(date)
57 } else {
58 new_date
59 }
60 }
61
62 fn roll_mod_backward_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
65 let new_date = self.roll_backward_bus_day(date);
66 if new_date.month() != date.month() {
67 self.roll_forward_bus_day(date)
68 } else {
69 new_date
70 }
71 }
72
73 fn roll_forward_settled_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
77 let mut new_date = self.roll_forward_bus_day(date);
78 while !self.is_settlement(&new_date) {
79 new_date = self.roll_forward_bus_day(&(new_date + Days::new(1)));
80 }
81 new_date
82 }
83
84 fn roll_backward_settled_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
88 let mut new_date = self.roll_backward_bus_day(date);
89 while !self.is_settlement(&new_date) {
90 new_date = self.roll_backward_bus_day(&(new_date - Days::new(1)));
91 }
92 new_date
93 }
94
95 fn roll_forward_mod_settled_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
98 let new_date = self.roll_forward_settled_bus_day(date);
99 if new_date.month() != date.month() {
100 self.roll_backward_settled_bus_day(date)
101 } else {
102 new_date
103 }
104 }
105
106 fn roll_backward_mod_settled_bus_day(&self, date: &NaiveDateTime) -> NaiveDateTime {
109 let new_date = self.roll_backward_settled_bus_day(date);
110 if new_date.month() != date.month() {
111 self.roll_forward_settled_bus_day(date)
112 } else {
113 new_date
114 }
115 }
116
117 fn lag_bus_days(&self, date: &NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
125 if self.is_bus_day(date) {
126 return self.add_bus_days(date, days, settlement).unwrap();
127 }
128 match days.cmp(&0_i32) {
129 Ordering::Equal => self.roll_forward_bus_day(date),
130 Ordering::Less => self
131 .add_bus_days(&self.roll_backward_bus_day(date), days + 1, settlement)
132 .unwrap(),
133 Ordering::Greater => self
134 .add_bus_days(&self.roll_forward_bus_day(date), days - 1, settlement)
135 .unwrap(),
136 }
137 }
138
139 fn add_cal_days(&self, date: &NaiveDateTime, days: i32, adjuster: &Adjuster) -> NaiveDateTime
142 where
143 Self: Sized,
144 {
145 let new_date = if days < 0 {
146 *date - Days::new(u64::try_from(-days).unwrap())
147 } else {
148 *date + Days::new(u64::try_from(days).unwrap())
149 };
150 adjuster.adjust(&new_date, self)
151 }
152
153 fn add_bus_days(
159 &self,
160 date: &NaiveDateTime,
161 days: i32,
162 settlement: bool,
163 ) -> Result<NaiveDateTime, PyErr> {
164 if self.is_non_bus_day(date) {
165 return Err(PyValueError::new_err(
166 "Cannot add business days to an input `date` that is not a business day.",
167 ));
168 }
169 let mut new_date = *date;
170 let mut counter: i32 = 0;
171 if days < 0 {
172 while counter > days {
174 new_date = self.roll_backward_bus_day(&(new_date - Days::new(1)));
175 counter -= 1;
176 }
177 } else {
178 while counter < days {
180 new_date = self.roll_forward_bus_day(&(new_date + Days::new(1)));
181 counter += 1;
182 }
183 }
184
185 if !settlement {
186 Ok(new_date)
187 } else if days < 0 {
188 Ok(self.roll_backward_settled_bus_day(&new_date))
189 } else {
190 Ok(self.roll_forward_settled_bus_day(&new_date))
191 }
192 }
193
194 fn bus_date_range(
196 &self,
197 start: &NaiveDateTime,
198 end: &NaiveDateTime,
199 ) -> Result<Vec<NaiveDateTime>, PyErr> {
200 if self.is_non_bus_day(start) || self.is_non_bus_day(end) {
201 return Err(PyValueError::new_err("`start` and `end` for a calendar `bus_date_range` must both be valid business days"));
202 }
203 let mut vec = Vec::new();
204 let mut sample_date = *start;
205 while sample_date <= *end {
206 vec.push(sample_date);
207 sample_date = self.add_bus_days(&sample_date, 1, false)?;
208 }
209 Ok(vec)
210 }
211
212 fn cal_date_range(
214 &self,
215 start: &NaiveDateTime,
216 end: &NaiveDateTime,
217 ) -> Result<Vec<NaiveDateTime>, PyErr> {
218 let mut vec = Vec::new();
219 let mut sample_date = *start;
220 while sample_date <= *end {
221 vec.push(sample_date);
222 sample_date = sample_date + Days::new(1);
223 }
224 Ok(vec)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::scheduling::{ndt, Cal, CalendarAdjustment, UnionCal};
232
233 fn fixture_hol_cal() -> Cal {
234 let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; Cal::new(hols, vec![5, 6])
236 }
237
238 #[test]
239 fn test_roll_forward_bus_day() {
240 let cal = fixture_hol_cal();
241 let hol =
242 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
243 let next = cal.roll_forward_bus_day(&hol);
244 assert_eq!(
245 next,
246 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
247 );
248
249 let sat =
250 NaiveDateTime::parse_from_str("2015-09-05 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
251 let next = cal.roll_forward_bus_day(&sat);
252 assert_eq!(
253 next,
254 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
255 );
256
257 let fri =
258 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
259 let next = cal.roll_forward_bus_day(&fri);
260 assert_eq!(
261 next,
262 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
263 )
264 }
265
266 #[test]
267 fn test_roll_backward_bus_day() {
268 let cal = fixture_hol_cal();
269 let hol =
270 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
271 let prev = cal.roll_backward_bus_day(&hol);
272 assert_eq!(
273 prev,
274 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
275 );
276
277 let fri =
278 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
279 let next = cal.roll_backward_bus_day(&fri);
280 assert_eq!(
281 next,
282 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
283 )
284 }
285
286 #[test]
287 fn test_is_business_day() {
288 let cal = fixture_hol_cal();
289 let hol =
290 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
291 let no_hol =
292 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
293 let saturday =
294 NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
295 assert!(!cal.is_bus_day(&hol)); assert!(cal.is_bus_day(&no_hol)); assert!(!cal.is_bus_day(&saturday)); }
299
300 #[test]
301 fn test_is_non_business_day() {
302 let cal = fixture_hol_cal();
303 let hol =
304 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
305 let no_hol =
306 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
307 let saturday =
308 NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
309 assert!(cal.is_non_bus_day(&hol)); assert!(!cal.is_non_bus_day(&no_hol)); assert!(cal.is_non_bus_day(&saturday)); }
313
314 #[test]
315 fn test_lag_bus_days() {
316 let cal = fixture_hol_cal();
317 let result = cal.lag_bus_days(&ndt(2015, 9, 7), 1, true);
318 assert_eq!(result, ndt(2015, 9, 8));
319
320 let result = cal.lag_bus_days(&ndt(2025, 2, 15), -1, true);
321 assert_eq!(result, ndt(2025, 2, 14));
322
323 let result = cal.lag_bus_days(&ndt(2015, 9, 7), 0, true);
324 assert_eq!(result, ndt(2015, 9, 8))
325 }
326
327 #[test]
328 fn test_add_days() {
329 let hols = vec![
330 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
331 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
332 ];
333 let settle =
334 vec![
335 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
336 ];
337 let hcal = Cal::new(hols, vec![5, 6]);
338 let scal = Cal::new(settle, vec![5, 6]);
339 let cal = UnionCal::new(vec![hcal], vec![scal].into());
340
341 let tue =
343 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
344 let next = cal.add_cal_days(&tue, 2, &Adjuster::Following {});
345 assert_eq!(
346 next,
347 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
348 );
349
350 let tue =
352 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
353 let next = cal.add_cal_days(&tue, 2, &Adjuster::FollowingSettle {});
354 assert_eq!(
355 next,
356 NaiveDateTime::parse_from_str("2015-09-14 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
357 );
358
359 let tue =
361 NaiveDateTime::parse_from_str("2015-09-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
362 let prev = cal.add_cal_days(&tue, -2, &Adjuster::Previous {});
363 assert_eq!(
364 prev,
365 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
366 );
367
368 let tue =
370 NaiveDateTime::parse_from_str("2015-09-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
371 let prev = cal.add_cal_days(&tue, -2, &Adjuster::PreviousSettle {});
372 assert_eq!(
373 prev,
374 NaiveDateTime::parse_from_str("2015-09-09 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
375 );
376 }
377
378 #[test]
379 fn test_add_bus_days() {
380 let hols = vec![ndt(2015, 9, 8), ndt(2015, 9, 10)];
381 let settle = vec![ndt(2015, 9, 11)];
382
383 let hcal = Cal::new(hols, vec![5, 6]);
384 let scal = Cal::new(settle, vec![5, 6]);
385 let cal = UnionCal::new(vec![hcal], vec![scal].into());
386
387 let mon = ndt(2015, 9, 7);
389 let next = cal.add_bus_days(&mon, 2, false).unwrap();
390 assert_eq!(next, ndt(2015, 9, 11));
391
392 let next = cal.add_bus_days(&mon, 2, true).unwrap();
394 assert_eq!(next, ndt(2015, 9, 14));
395
396 let tue = ndt(2015, 9, 15);
398 let prev = cal.add_bus_days(&tue, -2, false).unwrap();
399 assert_eq!(prev, ndt(2015, 9, 11));
400
401 let prev = cal.add_bus_days(&tue, -2, true).unwrap();
403 assert_eq!(prev, ndt(2015, 9, 9));
404 }
405
406 #[test]
407 fn test_add_bus_days_error() {
408 let cal = fixture_hol_cal();
409 match cal.add_bus_days(&ndt(2015, 9, 7), 3, true) {
410 Ok(_) => assert!(false),
411 Err(_) => assert!(true),
412 }
413 }
414
415 #[test]
416 fn test_add_bus_days_with_settlement() {
417 let cal = Cal::new(vec![ndt(2024, 6, 5)], vec![5, 6]);
418 let settle = Cal::new(vec![ndt(2024, 6, 4), ndt(2024, 6, 6)], vec![5, 6]);
419 let union = UnionCal::new(vec![cal], Some(vec![settle]));
420
421 let result = union.add_bus_days(&ndt(2024, 6, 4), 1, false).unwrap();
422 assert_eq!(result, ndt(2024, 6, 6)); let result = union.add_bus_days(&ndt(2024, 6, 4), 1, true).unwrap();
424 assert_eq!(result, ndt(2024, 6, 7)); let result = union.add_bus_days(&ndt(2024, 6, 6), -1, false).unwrap();
427 assert_eq!(result, ndt(2024, 6, 4)); let result = union.add_bus_days(&ndt(2024, 6, 6), -1, true).unwrap();
429 assert_eq!(result, ndt(2024, 6, 3)); }
431
432 #[test]
433 fn test_rolls() {
434 let cal = fixture_hol_cal();
435 let udates = vec![
436 ndt(2015, 9, 4),
437 ndt(2015, 9, 5),
438 ndt(2015, 9, 6),
439 ndt(2015, 9, 7),
440 ];
441 let result = cal.adjusts(&udates, &Adjuster::Following {});
442 assert_eq!(
443 result,
444 vec![
445 ndt(2015, 9, 4),
446 ndt(2015, 9, 8),
447 ndt(2015, 9, 8),
448 ndt(2015, 9, 8)
449 ]
450 );
451 }
452}