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 {
131 if self.is_bus_day(date) {
132 return self.add_bus_days(date, days, settlement).unwrap();
133 }
134 match days.cmp(&0_i32) {
135 Ordering::Equal => self
136 .add_bus_days(&self.roll_forward_bus_day(date), 0, settlement)
137 .unwrap(),
138 Ordering::Less => self
139 .add_bus_days(&self.roll_backward_bus_day(date), days + 1, settlement)
140 .unwrap(),
141 Ordering::Greater => self
142 .add_bus_days(&self.roll_forward_bus_day(date), days - 1, settlement)
143 .unwrap(),
144 }
145 }
146
147 fn add_cal_days(&self, date: &NaiveDateTime, days: i32, adjuster: &Adjuster) -> NaiveDateTime
150 where
151 Self: Sized,
152 {
153 let new_date = if days < 0 {
154 *date - Days::new(u64::try_from(-days).unwrap())
155 } else {
156 *date + Days::new(u64::try_from(days).unwrap())
157 };
158 adjuster.adjust(&new_date, self)
159 }
160
161 fn add_bus_days(
167 &self,
168 date: &NaiveDateTime,
169 days: i32,
170 settlement: bool,
171 ) -> Result<NaiveDateTime, PyErr> {
172 if self.is_non_bus_day(date) {
173 return Err(PyValueError::new_err(
174 "Cannot add business days to an input `date` that is not a business day.",
175 ));
176 }
177 let mut new_date = *date;
178 let mut counter: i32 = 0;
179 if days < 0 {
180 while counter > days {
182 new_date = self.roll_backward_bus_day(&(new_date - Days::new(1)));
183 counter -= 1;
184 }
185 } else {
186 while counter < days {
188 new_date = self.roll_forward_bus_day(&(new_date + Days::new(1)));
189 counter += 1;
190 }
191 }
192
193 if !settlement {
194 Ok(new_date)
195 } else if days < 0 {
196 Ok(self.roll_backward_settled_bus_day(&new_date))
197 } else {
198 Ok(self.roll_forward_settled_bus_day(&new_date))
199 }
200 }
201
202 fn bus_date_range(
204 &self,
205 start: &NaiveDateTime,
206 end: &NaiveDateTime,
207 ) -> Result<Vec<NaiveDateTime>, PyErr> {
208 if self.is_non_bus_day(start) || self.is_non_bus_day(end) {
209 return Err(PyValueError::new_err("`start` and `end` for a calendar `bus_date_range` must both be valid business days"));
210 }
211 let mut vec = Vec::new();
212 let mut sample_date = *start;
213 while sample_date <= *end {
214 vec.push(sample_date);
215 sample_date = self.add_bus_days(&sample_date, 1, false)?;
216 }
217 Ok(vec)
218 }
219
220 fn cal_date_range(
222 &self,
223 start: &NaiveDateTime,
224 end: &NaiveDateTime,
225 ) -> Result<Vec<NaiveDateTime>, PyErr> {
226 let mut vec = Vec::new();
227 let mut sample_date = *start;
228 while sample_date <= *end {
229 vec.push(sample_date);
230 sample_date = sample_date + Days::new(1);
231 }
232 Ok(vec)
233 }
234
235 fn print_month(&self, year: i32, month: u8) -> String {
237 let _map: Vec<String> = vec![
238 format!(" January {}\n", year),
239 format!(" February {}\n", year),
240 format!(" March {}\n", year),
241 format!(" April {}\n", year),
242 format!(" May {}\n", year),
243 format!(" June {}\n", year),
244 format!(" July {}\n", year),
245 format!(" August {}\n", year),
246 format!(" September {}\n", year),
247 format!(" October {}\n", year),
248 format!(" November {}\n", year),
249 format!(" December {}\n", year),
250 ];
251 let mut output = _map[(month - 1) as usize].clone();
252 output += "Su Mo Tu We Th Fr Sa\n";
253
254 let month_obj = Month::try_from(month).unwrap();
255 let days: u8 = month_obj.num_days(year).unwrap();
256 let weekday = NaiveDate::from_ymd_opt(year, month.into(), 1)
257 .unwrap()
258 .weekday()
259 .num_days_from_monday();
260 let idx_start: u32 = (weekday + 1) % 7;
261
262 let mut arr: [String; 42] = std::array::from_fn(|_| String::from(" "));
263 for i in 0..days {
264 let date = NaiveDate::from_ymd_opt(year, month.into(), (i + 1).into())
265 .expect("`year`, `month` `day` are invalid.")
266 .and_hms_opt(0, 0, 0)
267 .unwrap();
268 let s: String = {
269 if self.is_bus_day(&date) && self.is_settlement(&date) {
270 format!("{:>2}", i + 1)
271 } else if self.is_bus_day(&date) && !self.is_settlement(&date) {
272 " X".to_string()
273 } else if !self.is_bus_day(&date)
274 && matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
275 {
276 " .".to_string()
277 } else {
278 " *".to_string()
279 }
280 };
281 let index: u32 = i as u32 + idx_start;
282 arr[index as usize] = s;
283 }
284
285 for row in 0..6 {
286 output += &format!(
287 "{} {} {} {} {} {} {}\n",
288 &arr[row * 7],
289 &arr[row * 7 + 1],
290 &arr[row * 7 + 2],
291 &arr[row * 7 + 3],
292 &arr[row * 7 + 4],
293 &arr[row * 7 + 5],
294 &arr[row * 7 + 6]
295 );
296 }
297 output
298 }
299
300 fn print_year(&self, year: i32) -> String {
302 let mut data: Vec<Vec<String>> = vec![];
303 for i in 1..13 {
304 data.push(
305 self.print_month(year, i)
306 .lines()
307 .map(|s| s.to_string())
308 .collect(),
309 );
310 }
311 let mut output = "\n".to_string();
312 for i in 0..8 {
313 output += &format!(
314 "{} {} {} {}\n",
315 data[0][i], data[3][i], data[6][i], data[9][i]
316 );
317 }
318 for i in 0..8 {
319 output += &format!(
320 "{} {} {} {}\n",
321 data[1][i], data[4][i], data[7][i], data[10][i]
322 );
323 }
324 for i in 0..8 {
325 output += &format!(
326 "{} {} {} {}\n",
327 data[2][i], data[5][i], data[8][i], data[11][i]
328 );
329 }
330 output += "Legend:\n";
331 output += "'1-31': Settleable business day 'X': Non-settleable business day\n";
332 output += " '.': Non-business weekend '*': Non-business day\n";
333 output
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::scheduling::{ndt, Cal, CalendarAdjustment, UnionCal};
341
342 fn fixture_hol_cal() -> Cal {
343 let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; Cal::new(hols, vec![5, 6])
345 }
346
347 #[test]
348 fn test_roll_forward_bus_day() {
349 let cal = fixture_hol_cal();
350 let hol =
351 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
352 let next = cal.roll_forward_bus_day(&hol);
353 assert_eq!(
354 next,
355 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
356 );
357
358 let sat =
359 NaiveDateTime::parse_from_str("2015-09-05 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
360 let next = cal.roll_forward_bus_day(&sat);
361 assert_eq!(
362 next,
363 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
364 );
365
366 let fri =
367 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
368 let next = cal.roll_forward_bus_day(&fri);
369 assert_eq!(
370 next,
371 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
372 )
373 }
374
375 #[test]
376 fn test_roll_backward_bus_day() {
377 let cal = fixture_hol_cal();
378 let hol =
379 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
380 let prev = cal.roll_backward_bus_day(&hol);
381 assert_eq!(
382 prev,
383 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
384 );
385
386 let fri =
387 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
388 let next = cal.roll_backward_bus_day(&fri);
389 assert_eq!(
390 next,
391 NaiveDateTime::parse_from_str("2015-09-04 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
392 )
393 }
394
395 #[test]
396 fn test_is_business_day() {
397 let cal = fixture_hol_cal();
398 let hol =
399 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
400 let no_hol =
401 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
402 let saturday =
403 NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
404 assert!(!cal.is_bus_day(&hol)); assert!(cal.is_bus_day(&no_hol)); assert!(!cal.is_bus_day(&saturday)); }
408
409 #[test]
410 fn test_is_non_business_day() {
411 let cal = fixture_hol_cal();
412 let hol =
413 NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
414 let no_hol =
415 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
416 let saturday =
417 NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
418 assert!(cal.is_non_bus_day(&hol)); assert!(!cal.is_non_bus_day(&no_hol)); assert!(cal.is_non_bus_day(&saturday)); }
422
423 #[test]
424 fn test_lag_bus_days() {
425 let cal = fixture_hol_cal();
426 let result = cal.lag_bus_days(&ndt(2015, 9, 7), 1, true);
427 assert_eq!(result, ndt(2015, 9, 8));
428
429 let result = cal.lag_bus_days(&ndt(2025, 2, 15), -1, true);
430 assert_eq!(result, ndt(2025, 2, 14));
431
432 let result = cal.lag_bus_days(&ndt(2015, 9, 7), 0, true);
433 assert_eq!(result, ndt(2015, 9, 8))
434 }
435
436 #[test]
437 fn test_add_days() {
438 let hols = vec![
439 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
440 NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
441 ];
442 let settle =
443 vec![
444 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
445 ];
446 let hcal = Cal::new(hols, vec![5, 6]);
447 let scal = Cal::new(settle, vec![5, 6]);
448 let cal = UnionCal::new(vec![hcal], vec![scal].into());
449
450 let tue =
452 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
453 let next = cal.add_cal_days(&tue, 2, &Adjuster::Following {});
454 assert_eq!(
455 next,
456 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
457 );
458
459 let tue =
461 NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
462 let next = cal.add_cal_days(&tue, 2, &Adjuster::FollowingSettle {});
463 assert_eq!(
464 next,
465 NaiveDateTime::parse_from_str("2015-09-14 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
466 );
467
468 let tue =
470 NaiveDateTime::parse_from_str("2015-09-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
471 let prev = cal.add_cal_days(&tue, -2, &Adjuster::Previous {});
472 assert_eq!(
473 prev,
474 NaiveDateTime::parse_from_str("2015-09-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
475 );
476
477 let tue =
479 NaiveDateTime::parse_from_str("2015-09-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
480 let prev = cal.add_cal_days(&tue, -2, &Adjuster::PreviousSettle {});
481 assert_eq!(
482 prev,
483 NaiveDateTime::parse_from_str("2015-09-09 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
484 );
485 }
486
487 #[test]
488 fn test_add_bus_days() {
489 let hols = vec![ndt(2015, 9, 8), ndt(2015, 9, 10)];
490 let settle = vec![ndt(2015, 9, 11)];
491
492 let hcal = Cal::new(hols, vec![5, 6]);
493 let scal = Cal::new(settle, vec![5, 6]);
494 let cal = UnionCal::new(vec![hcal], vec![scal].into());
495
496 let mon = ndt(2015, 9, 7);
498 let next = cal.add_bus_days(&mon, 2, false).unwrap();
499 assert_eq!(next, ndt(2015, 9, 11));
500
501 let next = cal.add_bus_days(&mon, 2, true).unwrap();
503 assert_eq!(next, ndt(2015, 9, 14));
504
505 let tue = ndt(2015, 9, 15);
507 let prev = cal.add_bus_days(&tue, -2, false).unwrap();
508 assert_eq!(prev, ndt(2015, 9, 11));
509
510 let prev = cal.add_bus_days(&tue, -2, true).unwrap();
512 assert_eq!(prev, ndt(2015, 9, 9));
513 }
514
515 #[test]
516 fn test_add_bus_days_error() {
517 let cal = fixture_hol_cal();
518 match cal.add_bus_days(&ndt(2015, 9, 7), 3, true) {
519 Ok(_) => assert!(false),
520 Err(_) => assert!(true),
521 }
522 }
523
524 #[test]
525 fn test_add_bus_days_with_settlement() {
526 let cal = Cal::new(vec![ndt(2024, 6, 5)], vec![5, 6]);
527 let settle = Cal::new(vec![ndt(2024, 6, 4), ndt(2024, 6, 6)], vec![5, 6]);
528 let union = UnionCal::new(vec![cal], Some(vec![settle]));
529
530 let result = union.add_bus_days(&ndt(2024, 6, 4), 1, false).unwrap();
531 assert_eq!(result, ndt(2024, 6, 6)); let result = union.add_bus_days(&ndt(2024, 6, 4), 1, true).unwrap();
533 assert_eq!(result, ndt(2024, 6, 7)); let result = union.add_bus_days(&ndt(2024, 6, 6), -1, false).unwrap();
536 assert_eq!(result, ndt(2024, 6, 4)); let result = union.add_bus_days(&ndt(2024, 6, 6), -1, true).unwrap();
538 assert_eq!(result, ndt(2024, 6, 3)); }
540
541 #[test]
542 fn test_rolls() {
543 let cal = fixture_hol_cal();
544 let udates = vec![
545 ndt(2015, 9, 4),
546 ndt(2015, 9, 5),
547 ndt(2015, 9, 6),
548 ndt(2015, 9, 7),
549 ];
550 let result = cal.adjusts(&udates, &Adjuster::Following {});
551 assert_eq!(
552 result,
553 vec![
554 ndt(2015, 9, 4),
555 ndt(2015, 9, 8),
556 ndt(2015, 9, 8),
557 ndt(2015, 9, 8)
558 ]
559 );
560 }
561
562 #[test]
563 fn test_lag_bus_days_zero_with_settlement() {
564 let cal = Cal::new(vec![ndt(2000, 6, 27)], vec![]);
566 let settle = Cal::new(vec![ndt(2000, 6, 26), ndt(2000, 6, 28)], vec![]);
567 let uni = UnionCal::new(vec![cal], Some(vec![settle]));
568
569 assert_eq!(
571 ndt(2000, 6, 28),
572 uni.lag_bus_days(&ndt(2000, 6, 27), 0, false)
573 );
574
575 assert_eq!(
577 ndt(2000, 6, 29),
578 uni.lag_bus_days(&ndt(2000, 6, 27), 0, true)
579 );
580
581 assert_eq!(
583 ndt(2000, 6, 28),
584 uni.lag_bus_days(&ndt(2000, 6, 28), 0, false)
585 );
586
587 assert_eq!(
589 ndt(2000, 6, 29),
590 uni.lag_bus_days(&ndt(2000, 6, 28), 0, true)
591 );
592 }
593
594 #[test]
595 fn test_print_month() {
596 let cal = Cal::new(vec![ndt(2026, 1, 1), ndt(2026, 1, 19)], vec![5, 6]);
597 let result = cal.print_month(2026, 1);
598 let raw_output = r#" January 2026
599Su Mo Tu We Th Fr Sa
600 * 2 .
601 . 5 6 7 8 9 .
602 . 12 13 14 15 16 .
603 . * 20 21 22 23 .
604 . 26 27 28 29 30 .
605$$$$$$$$$$$$$$$$$$$$
606"#;
607 let expected = raw_output.replace("$", " ");
608 assert_eq!(result, expected);
609 }
610
611 #[test]
612 fn test_print_year() {
613 let cal = Cal::new(vec![ndt(2026, 1, 1), ndt(2026, 1, 19)], vec![5, 6]);
614 let result = cal.print_year(2026);
615 println!("{}", result);
616 let raw_output = r#"
617 January 2026 April 2026 July 2026 October 2026
618Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
619 * 2 . 1 2 3 . 1 2 3 . 1 2 .
620 . 5 6 7 8 9 . . 6 7 8 9 10 . . 6 7 8 9 10 . . 5 6 7 8 9 .
621 . 12 13 14 15 16 . . 13 14 15 16 17 . . 13 14 15 16 17 . . 12 13 14 15 16 .
622 . * 20 21 22 23 . . 20 21 22 23 24 . . 20 21 22 23 24 . . 19 20 21 22 23 .
623 . 26 27 28 29 30 . . 27 28 29 30 . 27 28 29 30 31 . 26 27 28 29 30 .
624$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
625 February 2026 May 2026 August 2026 November 2026
626Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
627 . 2 3 4 5 6 . 1 . . . 2 3 4 5 6 .
628 . 9 10 11 12 13 . . 4 5 6 7 8 . . 3 4 5 6 7 . . 9 10 11 12 13 .
629 . 16 17 18 19 20 . . 11 12 13 14 15 . . 10 11 12 13 14 . . 16 17 18 19 20 .
630 . 23 24 25 26 27 . . 18 19 20 21 22 . . 17 18 19 20 21 . . 23 24 25 26 27 .
631 . 25 26 27 28 29 . . 24 25 26 27 28 . . 30$$$$$$$$$$$$$$$
632 . . 31$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
633 March 2026 June 2026 September 2026 December 2026
634Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
635 . 2 3 4 5 6 . 1 2 3 4 5 . 1 2 3 4 . 1 2 3 4 .
636 . 9 10 11 12 13 . . 8 9 10 11 12 . . 7 8 9 10 11 . . 7 8 9 10 11 .
637 . 16 17 18 19 20 . . 15 16 17 18 19 . . 14 15 16 17 18 . . 14 15 16 17 18 .
638 . 23 24 25 26 27 . . 22 23 24 25 26 . . 21 22 23 24 25 . . 21 22 23 24 25 .
639 . 30 31 . 29 30 . 28 29 30 . 28 29 30 31$$$$$$
640$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
641Legend:
642'1-31': Settleable business day 'X': Non-settleable business day
643 '.': Non-business weekend '*': Non-business day
644"#;
645 let expected = raw_output.replace("$", " ");
646
647 let result_lines: Vec<&str> = result.lines().collect();
648 let expected_lines: Vec<&str> = expected.lines().collect();
649 for i in 0..result_lines.len() {
650 assert_eq!(expected_lines[i], result_lines[i]);
651 }
652 }
653}