rateslib/scheduling/frequency/frequency.rs
1use crate::scheduling::{ndt, Adjuster, Cal, Calendar, DateRoll, RollDay};
2use chrono::prelude::*;
3use chrono::Months;
4use pyo3::exceptions::PyValueError;
5use pyo3::{pyclass, PyErr};
6use serde::{Deserialize, Serialize};
7
8/// A frequency for generating unadjusted scheduling periods.
9#[pyclass(module = "rateslib.rs", eq)]
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum Frequency {
12 /// A set number of business days, defined by a [`Calendar`], which can only align with a
13 /// business day as defined by that [`Calendar`].
14 BusDays { number: i32, calendar: Calendar },
15 /// A set number of calendar days, which can align with any unadjusted date. To achieve a
16 /// `Weeks` variant use an appropriate `number` of days.
17 CalDays { number: i32 },
18 /// A set number of calendar months, with a potential [`RollDay`].
19 /// To achieve a `Years` variant use an appropriate `number` of months.
20 Months { number: i32, roll: Option<RollDay> },
21 /// Only ever defining one single period, and which can align with any unadjusted date.
22 Zero {},
23}
24
25/// Used to define periods of financial instrument schedules.
26pub trait Scheduling {
27 /// Validate if an unadjusted date aligns with the scheduling object.
28 fn try_udate(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr>;
29
30 /// Calculate the next unadjusted scheduling period date from a given `date`.
31 ///
32 /// <div class="warning">
33 ///
34 /// The input `date` is not checked to align with the scheduling object. This can lead to
35 /// to optically unexpected results (see examples). If a check on the date is required use the
36 /// [`try_unext`](Scheduling::try_unext) method instead.
37 ///
38 /// </div>
39 ///
40 /// # Examples
41 /// ```rust
42 /// # use rateslib::scheduling::{Frequency, Scheduling, ndt, RollDay};
43 /// let f = Frequency::Months{number:1, roll: Some(RollDay::Day(1))};
44 /// let result = f.next(&ndt(2000, 1, 31));
45 /// assert_eq!(ndt(2000, 2, 1), result);
46 /// assert!(f.try_unext(&ndt(2000, 1, 31)).is_err());
47 /// ```
48 fn next(&self, date: &NaiveDateTime) -> NaiveDateTime;
49
50 /// Calculate the previous unadjusted scheduling period date from a given `date`.
51 ///
52 /// <div class="warning">
53 ///
54 /// The input `date` is not checked to align with the scheduling object. This can lead to
55 /// to optically unexpected results (see examples). If a check on the date is required use the
56 /// [`try_uprevious`](Scheduling::try_uprevious) method instead.
57 ///
58 /// </div>
59 ///
60 /// # Examples
61 /// ```rust
62 /// # use rateslib::scheduling::{Frequency, Scheduling, ndt, RollDay};
63 /// let f = Frequency::Months{number:1, roll: Some(RollDay::Day(31))};
64 /// let result = f.previous(&ndt(2000, 2, 1));
65 /// assert_eq!(ndt(2000, 1, 31), result);
66 /// assert!(f.try_uprevious(&ndt(2000, 2, 1)).is_err());
67 /// ```
68 fn previous(&self, date: &NaiveDateTime) -> NaiveDateTime;
69
70 /// Return a vector of unadjusted regular scheduling dates if it exists.
71 ///
72 /// # Notes
73 /// In many standard cases this will simply use the provided method
74 /// [`try_uregular_from_unext`](Scheduling::try_uregular_from_unext), but allows for custom
75 /// implementations when required.
76 fn try_uregular(
77 &self,
78 ueffective: &NaiveDateTime,
79 utermination: &NaiveDateTime,
80 ) -> Result<Vec<NaiveDateTime>, PyErr>;
81
82 /// Calculate the next unadjusted scheduling period date from an unadjusted base date.
83 ///
84 /// # Notes
85 /// This method first checks that the `udate` is valid and returns an error if not.
86 fn try_unext(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr> {
87 let _ = self.try_udate(udate)?;
88 Ok(self.next(udate))
89 }
90
91 /// Calculate the previous unadjusted scheduling period date from an unadjusted base date.
92 ///
93 /// # Notes
94 /// This method first checks that the `udate` is valid and returns an error if not.
95 fn try_uprevious(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr> {
96 let _ = self.try_udate(udate)?;
97 Ok(self.previous(udate))
98 }
99
100 /// Return a vector of unadjusted regular scheduling dates if it exists.
101 ///
102 /// # Notes
103 /// This method begins with ``ueffective`` and repeatedly applies [`try_unext`](Scheduling::try_unext)
104 /// to derive all appropriate dates until ``utermination``.
105 fn try_uregular_from_unext(
106 &self,
107 ueffective: &NaiveDateTime,
108 utermination: &NaiveDateTime,
109 ) -> Result<Vec<NaiveDateTime>, PyErr> {
110 let mut v: Vec<NaiveDateTime> = vec![];
111 let mut date = *ueffective;
112 while date < *utermination {
113 v.push(date);
114 date = self.try_unext(&date)?;
115 }
116 if date == *utermination {
117 v.push(*utermination);
118 Ok(v)
119 } else {
120 Err(PyValueError::new_err(
121 "Input dates to Frequency do not define a regular unadjusted schedule",
122 ))
123 }
124 }
125
126 /// Check if two given unadjusted dates define a **regular period** under a scheduling object.
127 ///
128 /// # Notes
129 /// This method tests if [`try_uregular`](Scheduling::try_uregular) has exactly two dates.
130 fn is_regular_period(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
131 let s = self.try_uregular(ueffective, utermination);
132 match s {
133 Ok(v) => v.len() == 2,
134 Err(_) => false,
135 }
136 }
137
138 /// Check if two given unadjusted dates define a **short front stub period** under a scheduling object.
139 ///
140 /// # Notes
141 /// This method tests if [`try_uprevious`](Scheduling::try_uprevious) is before `ueffective`.
142 /// If dates are undeterminable this returns `false`.
143 fn is_short_front_stub(
144 &self,
145 ueffective: &NaiveDateTime,
146 utermination: &NaiveDateTime,
147 ) -> bool {
148 let quasi = self.try_uprevious(utermination);
149 match quasi {
150 Ok(date) => date < *ueffective,
151 Err(_) => false,
152 }
153 }
154
155 /// Check if two given unadjusted dates define a **long front stub period** under a scheduling object.
156 fn is_long_front_stub(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
157 let quasi = self.try_uprevious(utermination);
158 match quasi {
159 Ok(date) if *ueffective < date => {
160 let quasi_2 = self.try_uprevious(&date);
161 match quasi_2 {
162 Ok(date) => date <= *ueffective, // for long stub equal to allowed
163 Err(_) => false,
164 }
165 }
166 _ => false,
167 }
168 }
169
170 /// Check if two given unadjusted dates define a **short back stub period** under a scheduling object.
171 ///
172 /// # Notes
173 /// This method tests if [Scheduling::try_unext] is after `utermination`.
174 /// If dates are undeterminable this returns `false`.
175 fn is_short_back_stub(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
176 let quasi = self.try_unext(ueffective);
177 match quasi {
178 Ok(date) => *utermination < date,
179 Err(_) => false,
180 }
181 }
182
183 /// Check if two given unadjusted dates define a **long back stub period** under a scheduling object.
184 fn is_long_back_stub(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
185 let quasi = self.try_unext(ueffective);
186 match quasi {
187 Ok(date) if date < *utermination => {
188 let quasi_2 = self.try_unext(&date);
189 match quasi_2 {
190 Ok(date) => *utermination <= date, // for long stub equal to allowed.
191 Err(_) => false,
192 }
193 }
194 _ => false,
195 }
196 }
197
198 /// Check if two given unadjusted dates define any **front stub** under a scheduling object.
199 ///
200 /// # Notes
201 /// If dates are undeterminable this returns `false`.
202 fn is_front_stub(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
203 self.is_short_front_stub(ueffective, utermination)
204 || self.is_long_front_stub(ueffective, utermination)
205 }
206
207 /// Check if two given unadjusted dates define any **back stub** under a scheduling object.
208 ///
209 /// # Notes
210 /// If dates are undeterminable this returns `false`.
211 fn is_back_stub(&self, ueffective: &NaiveDateTime, utermination: &NaiveDateTime) -> bool {
212 self.is_short_back_stub(ueffective, utermination)
213 || self.is_long_back_stub(ueffective, utermination)
214 }
215
216 /// Infer an unadjusted front stub date from unadjusted irregular schedule dates.
217 ///
218 /// # Notes
219 /// If a regular schedule is defined then the result will hold `None` as no stub is required.
220 /// If a stub can be inferred then it will be returned as `Some(date)`.
221 /// An errors will be returned if the dates are too close together to infer stubs and do not
222 /// define a regular period.
223 fn try_infer_ufront_stub(
224 &self,
225 ueffective: &NaiveDateTime,
226 utermination: &NaiveDateTime,
227 short: bool,
228 ) -> Result<Option<NaiveDateTime>, PyErr> {
229 let mut date = *utermination;
230 while date > *ueffective {
231 date = self.try_uprevious(&date)?;
232 }
233 if date == *ueffective {
234 // defines a regular schedule and no stub is required.
235 Ok(None)
236 } else {
237 if short {
238 date = self.try_unext(&date)?;
239 } else {
240 date = self.try_unext(&date)?;
241 date = self.try_unext(&date)?;
242 }
243 if date >= *utermination {
244 // then the dates are too close together to define a stub
245 Ok(None)
246 } else {
247 // return the valid stub date
248 Ok(Some(date))
249 }
250 }
251 }
252
253 /// Infer an unadjusted back stub date from unadjusted irregular schedule dates.
254 ///
255 /// # Notes
256 /// If a regular schedule is defined then the result will hold `None` as no stub is required.
257 /// If a stub can be inferred then it will be returned as `Some(date)`.
258 /// An errors will be returned if the dates are too close together to infer stubs and do not
259 /// define a regular period.
260 fn try_infer_uback_stub(
261 &self,
262 ueffective: &NaiveDateTime,
263 utermination: &NaiveDateTime,
264 short: bool,
265 ) -> Result<Option<NaiveDateTime>, PyErr> {
266 let mut date = *ueffective;
267 while date < *utermination {
268 date = self.try_unext(&date)?;
269 }
270 if date == *utermination {
271 // regular schedule so no stub required
272 Ok(None)
273 } else {
274 if short {
275 date = self.try_uprevious(&date)?;
276 } else {
277 date = self.try_uprevious(&date)?;
278 date = self.try_uprevious(&date)?;
279 }
280 if date <= *ueffective {
281 // dates are too close together to define a stub.
282 Ok(None)
283 } else {
284 // return the valid stub
285 Ok(Some(date))
286 }
287 }
288 }
289
290 /// Get the approximate number of coupons per annum.
291 ///
292 /// This will average the number coupons paid in 50 year period.
293 fn periods_per_annum(&self) -> f64 {
294 let mut date = self.next(&ndt(1999, 12, 31));
295 if date > ndt(2049, 12, 31) {
296 // then the next method has generated an unusually long period. return nominal value
297 return 0.01_f64;
298 }
299 let estimated_end = date + Months::new(600);
300 let mut counter = 0_f64;
301 let count: f64;
302 loop {
303 counter += 1.0;
304 let prev = date;
305 date = self.next(&prev);
306 if date < prev {
307 // Scheduling object is reversed so make a correction.
308 date = self.previous(&prev)
309 }
310 if date >= estimated_end {
311 if (estimated_end - prev) < (date - estimated_end) {
312 count = f64::max(1.0, counter - 1.0);
313 } else {
314 count = counter;
315 }
316 break;
317 }
318 }
319 count / 50.0
320 }
321}
322
323impl Frequency {
324 /// Get a vector of possible, fully specified [`Frequency`] variants for a series of unadjusted dates.
325 ///
326 /// # Notes
327 /// This method exists primarily to resolve cases when the [`RollDay`] on a
328 /// [`Frequency::Months`](Frequency) variant is `None`, and there are multiple possibilities. In this case
329 /// the method [`RollDay::vec_from`] is called internally.
330 ///
331 /// If the [`Frequency`] variant does not align with any of the provided unadjusted dates this
332 /// will return an error.
333 ///
334 /// # Examples
335 /// ```rust
336 /// # use rateslib::scheduling::{Frequency, ndt, RollDay};
337 /// // The RollDay is unspecified here
338 /// let f = Frequency::Months{number: 3, roll: None};
339 /// let result = f.try_vec_from(&vec![ndt(2024, 2, 29)]);
340 /// assert_eq!(result.unwrap(), vec![
341 /// Frequency::Months{number: 3, roll: Some(RollDay::Day(29))},
342 /// Frequency::Months{number: 3, roll: Some(RollDay::Day(30))},
343 /// Frequency::Months{number: 3, roll: Some(RollDay::Day(31))},
344 /// ]);
345 /// ```
346 pub fn try_vec_from(&self, udates: &Vec<NaiveDateTime>) -> Result<Vec<Frequency>, PyErr> {
347 match self {
348 Frequency::Months {
349 number: n,
350 roll: None,
351 } => {
352 // the RollDay is unspecified so get all possible RollDay variants
353 Ok(RollDay::vec_from(udates)
354 .into_iter()
355 .map(|r| Frequency::Months {
356 number: *n,
357 roll: Some(r),
358 })
359 .collect())
360 }
361 _ => {
362 // the Frequency is fully specified so return single element vector if
363 // at least 1 udate is valid
364 for udate in udates {
365 if self.try_udate(udate).is_ok() {
366 return Ok(vec![self.clone()]);
367 }
368 }
369 Err(PyValueError::new_err(
370 "The Frequency does not align with any of the `udates`.",
371 ))
372 }
373 }
374 }
375}
376
377impl Scheduling for Frequency {
378 /// Validate if an unadjusted date aligns with the specified [Frequency] variant.
379 ///
380 /// # Notes
381 /// This method will return error in one of two cases:
382 /// - The `udate` does not align with the fully defined variant.
383 /// - The variant is not fully defined (e.g. a [`Months`](Frequency) variant is missing
384 /// a [`RollDay`](RollDay)) and cannot make the determination.
385 ///
386 /// Therefore,
387 /// - For a [CalDays](Frequency) variant or [Zero](Frequency) variant, any ``udate`` is valid.
388 /// - For a [BusDays](Frequency) variant, ``udate`` must be a business day.
389 /// - For a [Months](Frequency) variant, ``udate`` must align with the [RollDay]. If no [RollDay] is
390 /// specified an error will always be returned.
391 ///
392 /// # Examples
393 /// ```rust
394 /// # use rateslib::scheduling::{Frequency, RollDay, ndt, Scheduling};
395 /// let result = Frequency::Months{number: 1, roll: Some(RollDay::IMM{})}.try_udate(&ndt(2025, 7, 16));
396 /// assert!(result.is_ok());
397 ///
398 /// let result = Frequency::Months{number: 1, roll: None}.try_udate(&ndt(2025, 7, 16));
399 /// assert!(result.is_err());
400 /// ```
401 fn try_udate(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr> {
402 match self {
403 Frequency::BusDays {
404 number: _n,
405 calendar: c,
406 } => {
407 if c.is_bus_day(udate) {
408 Ok(*udate)
409 } else {
410 Err(PyValueError::new_err(
411 "`udate` is not a business day of the given calendar.",
412 ))
413 }
414 }
415 Frequency::CalDays { number: _n } => Ok(*udate),
416 Frequency::Months {
417 number: _n,
418 roll: r,
419 } => match r {
420 Some(r) => r.try_udate(udate),
421 None => Err(PyValueError::new_err(
422 "`udate` cannot be validated since RollDay is None.",
423 )),
424 },
425 Frequency::Zero {} => Ok(*udate),
426 }
427 }
428
429 fn next(&self, date: &NaiveDateTime) -> NaiveDateTime {
430 match self {
431 Frequency::BusDays {
432 number: n,
433 calendar: c,
434 } => c.lag_bus_days(date, *n, false),
435 Frequency::CalDays { number: n } => {
436 let cal = Cal::new(vec![], vec![]);
437 cal.add_cal_days(date, *n, &Adjuster::Actual {})
438 }
439 Frequency::Months { number: n, roll: r } => match r {
440 Some(r) => r.uadd(date, *n),
441 None => RollDay::Day(date.day()).uadd(date, *n),
442 },
443 Frequency::Zero {} => ndt(9999, 1, 1),
444 }
445 }
446
447 fn previous(&self, date: &NaiveDateTime) -> NaiveDateTime {
448 match self {
449 Frequency::BusDays {
450 number: n,
451 calendar: c,
452 } => c.lag_bus_days(date, -(*n), false),
453 Frequency::CalDays { number: n } => {
454 let cal = Cal::new(vec![], vec![]);
455 cal.add_cal_days(date, -(*n), &Adjuster::Actual {})
456 }
457 Frequency::Months { number: n, roll: r } => match r {
458 Some(r) => r.uadd(date, -(*n)),
459 None => RollDay::Day(date.day()).uadd(date, -(*n)),
460 },
461 Frequency::Zero {} => ndt(1500, 1, 1),
462 }
463 }
464
465 fn try_uregular(
466 &self,
467 ueffective: &NaiveDateTime,
468 utermination: &NaiveDateTime,
469 ) -> Result<Vec<NaiveDateTime>, PyErr> {
470 match self {
471 Frequency::Zero {} => Ok(vec![*ueffective, *utermination]),
472 _ => self.try_uregular_from_unext(ueffective, utermination),
473 }
474 }
475}
476
477// UNIT TESTS
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::scheduling::ndt;
482
483 #[test]
484 fn test_try_udate() {
485 let options: Vec<(Frequency, NaiveDateTime)> = vec![
486 (
487 Frequency::BusDays {
488 number: 4,
489 calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
490 },
491 ndt(2025, 7, 11),
492 ),
493 (Frequency::CalDays { number: 4 }, ndt(2025, 7, 11)),
494 (Frequency::Zero {}, ndt(2025, 7, 11)),
495 (
496 Frequency::Months {
497 number: 4,
498 roll: Some(RollDay::Day(11)),
499 },
500 ndt(2025, 7, 11),
501 ),
502 ];
503 for option in options {
504 let result = option.0.try_udate(&option.1).unwrap();
505 assert_eq!(result, option.1);
506 }
507 }
508
509 #[test]
510 fn test_try_udate_err() {
511 let options: Vec<(Frequency, NaiveDateTime)> = vec![
512 (
513 Frequency::BusDays {
514 number: 4,
515 calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
516 },
517 ndt(2025, 7, 12),
518 ),
519 (
520 Frequency::Months {
521 number: 4,
522 roll: None,
523 },
524 ndt(2025, 7, 12),
525 ),
526 (
527 Frequency::Months {
528 number: 4,
529 roll: Some(RollDay::IMM {}),
530 },
531 ndt(2025, 7, 1),
532 ),
533 ];
534 for option in options {
535 assert!(option.0.try_udate(&option.1).is_err());
536 }
537 }
538
539 #[test]
540 fn test_is_regular_period_ok() {
541 let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
542 (
543 Frequency::CalDays { number: 5 },
544 ndt(2000, 1, 1),
545 ndt(2000, 1, 6),
546 true,
547 ),
548 (
549 Frequency::CalDays { number: 5 },
550 ndt(2000, 1, 1),
551 ndt(2000, 1, 5),
552 false,
553 ),
554 (
555 Frequency::Months {
556 number: 5,
557 roll: Some(RollDay::Day(1)),
558 },
559 ndt(2000, 1, 1),
560 ndt(2000, 6, 1),
561 true,
562 ),
563 (
564 Frequency::Months {
565 number: 5,
566 roll: Some(RollDay::Day(1)),
567 },
568 ndt(2000, 1, 1),
569 ndt(2000, 6, 5),
570 false,
571 ),
572 ];
573
574 for option in options {
575 let result = option.0.is_regular_period(&option.1, &option.2);
576 assert_eq!(result, option.3);
577 }
578 }
579
580 #[test]
581 fn test_is_short_front_stub() {
582 assert_eq!(
583 true,
584 Frequency::Months {
585 number: 1,
586 roll: Some(RollDay::Day(20))
587 }
588 .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 1, 20))
589 );
590 assert_eq!(
591 false,
592 Frequency::Months {
593 number: 1,
594 roll: Some(RollDay::Day(1))
595 }
596 .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 2, 1))
597 );
598 assert_eq!(
599 false,
600 Frequency::Months {
601 number: 1,
602 roll: None
603 }
604 .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 1, 15))
605 );
606 }
607
608 #[test]
609 fn test_is_long_front_stub() {
610 assert_eq!(
611 // is a valid long stub
612 true,
613 Frequency::Months {
614 number: 1,
615 roll: Some(RollDay::Day(20))
616 }
617 .is_long_front_stub(&ndt(2000, 1, 1), &ndt(2000, 2, 20))
618 );
619 assert_eq!(
620 // is a valid 2-regular period long stub
621 true,
622 Frequency::Months {
623 number: 1,
624 roll: Some(RollDay::Day(20))
625 }
626 .is_long_front_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 20))
627 );
628 assert_eq!(
629 // is too short
630 false,
631 Frequency::Months {
632 number: 1,
633 roll: Some(RollDay::Day(20))
634 }
635 .is_long_front_stub(&ndt(2000, 1, 25), &ndt(2000, 2, 20))
636 );
637 assert_eq!(
638 // is too long
639 false,
640 Frequency::Months {
641 number: 1,
642 roll: Some(RollDay::Day(20))
643 }
644 .is_long_front_stub(&ndt(2000, 1, 15), &ndt(2000, 3, 20))
645 );
646 }
647
648 #[test]
649 fn test_is_long_back_stub() {
650 assert_eq!(
651 // is a valid long stub
652 true,
653 Frequency::Months {
654 number: 1,
655 roll: Some(RollDay::Day(20))
656 }
657 .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 2, 28))
658 );
659 assert_eq!(
660 // is a valid 2-regular period long stub
661 true,
662 Frequency::Months {
663 number: 1,
664 roll: Some(RollDay::Day(20))
665 }
666 .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 20))
667 );
668 assert_eq!(
669 // is too short
670 false,
671 Frequency::Months {
672 number: 1,
673 roll: Some(RollDay::Day(20))
674 }
675 .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 2, 10))
676 );
677 assert_eq!(
678 // is too long
679 false,
680 Frequency::Months {
681 number: 1,
682 roll: Some(RollDay::Day(20))
683 }
684 .is_long_front_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 30))
685 );
686 }
687
688 // #[test]
689 // fn test_try_scheduling() {
690 // let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime)> = vec![
691 // (
692 // Frequency::Months {
693 // number: 1,
694 // roll: None,
695 // },
696 // ndt(2022, 7, 30),
697 // ndt(2022, 8, 30),
698 // ),
699 // (
700 // Frequency::Months {
701 // number: 2,
702 // roll: Some(RollDay::Day { day: 30 }),
703 // },
704 // ndt(2022, 7, 30),
705 // ndt(2022, 9, 30),
706 // ),
707 // (
708 // Frequency::Months {
709 // number: 3,
710 // roll: Some(RollDay::Day { day: 30 }),
711 // },
712 // ndt(2022, 7, 30),
713 // ndt(2022, 10, 30),
714 // ),
715 // (
716 // Frequency::Months {
717 // number: 4,
718 // roll: None,
719 // },
720 // ndt(2022, 7, 30),
721 // ndt(2022, 11, 30),
722 // ),
723 // (
724 // Frequency::Months {
725 // number: 6,
726 // roll: Some(RollDay::Day { day: 30 }),
727 // },
728 // ndt(2022, 7, 30),
729 // ndt(2023, 1, 30),
730 // ),
731 // (
732 // Frequency::Months {
733 // number: 12,
734 // roll: Some(RollDay::Day { day: 30 }),
735 // },
736 // ndt(2022, 7, 30),
737 // ndt(2023, 7, 30),
738 // ),
739 // (
740 // Frequency::Months {
741 // number: 1,
742 // roll: Some(RollDay::Day { day: 31 }),
743 // },
744 // ndt(2022, 6, 30),
745 // ndt(2022, 7, 31),
746 // ),
747 // (
748 // Frequency::Months {
749 // number: 1,
750 // roll: Some(RollDay::IMM {}),
751 // },
752 // ndt(2022, 6, 15),
753 // ndt(2022, 7, 20),
754 // ),
755 // (
756 // Frequency::CalDays { number: 5 },
757 // ndt(2022, 6, 15),
758 // ndt(2022, 6, 20),
759 // ),
760 // (
761 // Frequency::CalDays { number: 14 },
762 // ndt(2022, 6, 15),
763 // ndt(2022, 6, 29),
764 // ),
765 // (
766 // Frequency::BusDays {
767 // number: 5,
768 // calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
769 // },
770 // ndt(2025, 6, 23),
771 // ndt(2025, 6, 30),
772 // ),
773 // (Frequency::Zero {}, ndt(1500, 1, 1), ndt(9999, 1, 1)),
774 // ];
775 // for option in options.iter() {
776 // assert_eq!(option.2, option.0.try_unext(&option.1).unwrap());
777 // assert_eq!(option.1, option.0.try_uprevious(&option.2).unwrap());
778 // }
779 // }
780 //
781 #[test]
782 fn test_get_uschedule_imm() {
783 // test the example given in Coding Interest Rates
784 let result = Frequency::Months {
785 number: 1,
786 roll: Some(RollDay::IMM {}),
787 }
788 .try_uregular(&ndt(2023, 3, 15), &ndt(2023, 9, 20))
789 .unwrap();
790 assert_eq!(
791 result,
792 vec![
793 ndt(2023, 3, 15),
794 ndt(2023, 4, 19),
795 ndt(2023, 5, 17),
796 ndt(2023, 6, 21),
797 ndt(2023, 7, 19),
798 ndt(2023, 8, 16),
799 ndt(2023, 9, 20)
800 ]
801 );
802 }
803 //
804 // #[test]
805 // fn test_get_uschedule() {
806 // let result = Frequency::Months {
807 // number: 3,
808 // roll: Some(RollDay::Day { day: 1 }),
809 // }
810 // .try_uregular(&ndt(2000, 1, 1), &ndt(2001, 1, 1))
811 // .unwrap();
812 // assert_eq!(
813 // result,
814 // vec![
815 // ndt(2000, 1, 1),
816 // ndt(2000, 4, 1),
817 // ndt(2000, 7, 1),
818 // ndt(2000, 10, 1),
819 // ndt(2001, 1, 1)
820 // ]
821 // );
822 // }
823
824 // #[test]
825 // fn test_infer_ufront() {
826 // let options: Vec<(
827 // Frequency,
828 // NaiveDateTime,
829 // NaiveDateTime,
830 // bool,
831 // Option<NaiveDateTime>,
832 // )> = vec![
833 // (
834 // Frequency::Months {
835 // number: 1,
836 // roll: Some(RollDay::Day { day: 15 }),
837 // },
838 // ndt(2022, 7, 30),
839 // ndt(2022, 10, 15),
840 // true,
841 // Some(ndt(2022, 8, 15)),
842 // ),
843 // (
844 // Frequency::Months {
845 // number: 1,
846 // roll: None,
847 // },
848 // ndt(2022, 7, 30),
849 // ndt(2022, 10, 15),
850 // false,
851 // Some(ndt(2022, 9, 15)),
852 // ),
853 // ];
854 //
855 // for option in options.iter() {
856 // assert_eq!(
857 // option.4,
858 // option
859 // .0
860 // .try_infer_ufront_stub(&option.1, &option.2, option.3)
861 // .unwrap()
862 // );
863 // }
864 // }
865
866 // #[test]
867 // fn test_infer_ufront_err() {
868 // let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
869 // (
870 // Frequency::Months {
871 // number: 1,
872 // roll: Some(RollDay::Day { day: 15 }),
873 // },
874 // ndt(2022, 7, 30),
875 // ndt(2022, 8, 15),
876 // true,
877 // ),
878 // (
879 // Frequency::Months {
880 // number: 1,
881 // roll: None,
882 // },
883 // ndt(2022, 7, 30),
884 // ndt(2022, 9, 15),
885 // false,
886 // ),
887 // (
888 // Frequency::Zero {},
889 // ndt(2022, 7, 30),
890 // ndt(2022, 9, 15),
891 // false,
892 // ),
893 // ];
894 //
895 // for option in options.iter() {
896 // let result = option
897 // .0
898 // .try_infer_ufront_stub(&option.1, &option.2, option.3)
899 // .is_err();
900 // assert_eq!(true, result);
901 // }
902 // }
903
904 // #[test]
905 // fn test_infer_uback() {
906 // let options: Vec<(
907 // Frequency,
908 // NaiveDateTime,
909 // NaiveDateTime,
910 // bool,
911 // Option<NaiveDateTime>,
912 // )> = vec![
913 // (
914 // Frequency::Months {
915 // number: 1,
916 // roll: Some(RollDay::Day { day: 30 }),
917 // },
918 // ndt(2022, 7, 30),
919 // ndt(2022, 10, 15),
920 // true,
921 // Some(ndt(2022, 9, 30)),
922 // ),
923 // (
924 // Frequency::Months {
925 // number: 1,
926 // roll: Some(RollDay::Day { day: 30 }),
927 // },
928 // ndt(2022, 7, 30),
929 // ndt(2022, 10, 15),
930 // false,
931 // Some(ndt(2022, 8, 30)),
932 // ),
933 // ];
934 //
935 // for option in options.iter() {
936 // assert_eq!(
937 // option.4,
938 // option
939 // .0
940 // .try_infer_uback_stub(&option.1, &option.2, option.3)
941 // .unwrap()
942 // );
943 // }
944 // }
945 //
946 // #[test]
947 // fn test_infer_uback_err() {
948 // let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
949 // (
950 // Frequency::Months {
951 // number: 1,
952 // roll: Some(RollDay::Day { day: 30 }),
953 // },
954 // ndt(2022, 7, 30),
955 // ndt(2022, 8, 15),
956 // true,
957 // ),
958 // (
959 // Frequency::Months {
960 // number: 1,
961 // roll: Some(RollDay::Day { day: 30 }),
962 // },
963 // ndt(2022, 7, 30),
964 // ndt(2022, 9, 15),
965 // false,
966 // ),
967 // ];
968 //
969 // for option in options.iter() {
970 // let result = option
971 // .0
972 // .try_infer_uback_stub(&option.1, &option.2, option.3)
973 // .is_err();
974 // assert_eq!(true, result);
975 // }
976 // }
977 //
978 #[test]
979 fn test_try_vec_from() {
980 let options: Vec<(Frequency, Vec<NaiveDateTime>, Vec<Frequency>)> = vec![
981 (
982 Frequency::Months {
983 number: 1,
984 roll: None,
985 },
986 vec![ndt(2022, 7, 30)],
987 vec![Frequency::Months {
988 number: 1,
989 roll: Some(RollDay::Day(30)),
990 }],
991 ),
992 (
993 Frequency::Months {
994 number: 1,
995 roll: None,
996 },
997 vec![ndt(2022, 2, 28)],
998 vec![
999 Frequency::Months {
1000 number: 1,
1001 roll: Some(RollDay::Day(28)),
1002 },
1003 Frequency::Months {
1004 number: 1,
1005 roll: Some(RollDay::Day(29)),
1006 },
1007 Frequency::Months {
1008 number: 1,
1009 roll: Some(RollDay::Day(30)),
1010 },
1011 Frequency::Months {
1012 number: 1,
1013 roll: Some(RollDay::Day(31)),
1014 },
1015 ],
1016 ),
1017 (
1018 Frequency::CalDays { number: 1 },
1019 vec![ndt(2022, 2, 28)],
1020 vec![Frequency::CalDays { number: 1 }],
1021 ),
1022 ];
1023
1024 for option in options.iter() {
1025 let result = option.0.try_vec_from(&option.1).unwrap();
1026 assert_eq!(option.2, result);
1027 }
1028 }
1029
1030 #[test]
1031 fn test_try_vec_from_err() {
1032 let options: Vec<(Frequency, Vec<NaiveDateTime>)> = vec![(
1033 Frequency::Months {
1034 number: 1,
1035 roll: Some(RollDay::IMM {}),
1036 },
1037 vec![ndt(2022, 7, 30)],
1038 )];
1039
1040 for option in options.iter() {
1041 assert_eq!(true, option.0.try_vec_from(&option.1).is_err());
1042 }
1043 }
1044
1045 #[test]
1046 fn test_coupons_per_annum() {
1047 let options: Vec<(Frequency, f64)> = vec![
1048 (Frequency::CalDays { number: 365 }, 1.0),
1049 (Frequency::CalDays { number: 182 }, 2.0),
1050 (Frequency::CalDays { number: 183 }, 2.0),
1051 (Frequency::CalDays { number: 91 }, 4.02),
1052 (Frequency::CalDays { number: 28 }, 13.04),
1053 (Frequency::CalDays { number: 7 }, 52.18),
1054 (
1055 Frequency::BusDays {
1056 number: 5,
1057 calendar: Cal::new(vec![], vec![5, 6]).into(),
1058 },
1059 52.18,
1060 ),
1061 (
1062 Frequency::BusDays {
1063 number: 63,
1064 calendar: Cal::new(vec![], vec![5, 6]).into(),
1065 },
1066 4.14,
1067 ),
1068 (
1069 Frequency::BusDays {
1070 number: 62,
1071 calendar: Cal::new(vec![], vec![5, 6]).into(),
1072 },
1073 4.2,
1074 ),
1075 (
1076 Frequency::Months {
1077 number: 1,
1078 roll: None,
1079 },
1080 12.0,
1081 ),
1082 (
1083 Frequency::Months {
1084 number: 2,
1085 roll: None,
1086 },
1087 6.0,
1088 ),
1089 (
1090 Frequency::Months {
1091 number: 3,
1092 roll: None,
1093 },
1094 4.0,
1095 ),
1096 (
1097 Frequency::Months {
1098 number: 4,
1099 roll: None,
1100 },
1101 3.0,
1102 ),
1103 (
1104 Frequency::Months {
1105 number: 6,
1106 roll: None,
1107 },
1108 2.0,
1109 ),
1110 (
1111 Frequency::Months {
1112 number: 9,
1113 roll: None,
1114 },
1115 1.34,
1116 ),
1117 (
1118 Frequency::Months {
1119 number: 12,
1120 roll: None,
1121 },
1122 1.0,
1123 ),
1124 (
1125 Frequency::Months {
1126 number: 24,
1127 roll: None,
1128 },
1129 0.5,
1130 ),
1131 (
1132 Frequency::Months {
1133 number: 3,
1134 roll: Some(RollDay::IMM()),
1135 },
1136 4.0,
1137 ),
1138 (Frequency::Zero {}, 0.01),
1139 ];
1140 for option in options {
1141 let result = option.0.periods_per_annum();
1142 assert_eq!(result, option.1);
1143 }
1144 }
1145}