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}