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/// Specifier 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        periods_per_annum(self)
295    }
296}
297
298impl Frequency {
299    /// Get a vector of possible, fully specified [`Frequency`] variants for a series of unadjusted dates.
300    ///
301    /// # Notes
302    /// This method exists primarily to resolve cases when the [`RollDay`] on a
303    /// [`Frequency::Months`](Frequency) variant is `None`, and there are multiple possibilities. In this case
304    /// the method [`RollDay::vec_from`] is called internally.
305    ///
306    /// If the [`Frequency`] variant does not align with any of the provided unadjusted dates this
307    /// will return an error.
308    ///
309    /// # Examples
310    /// ```rust
311    /// # use rateslib::scheduling::{Frequency, ndt, RollDay};
312    /// // The RollDay is unspecified here
313    /// let f = Frequency::Months{number: 3, roll: None};
314    /// let result = f.try_vec_from(&vec![ndt(2024, 2, 29)]);
315    /// assert_eq!(result.unwrap(), vec![
316    ///     Frequency::Months{number: 3, roll: Some(RollDay::Day(29))},
317    ///     Frequency::Months{number: 3, roll: Some(RollDay::Day(30))},
318    ///     Frequency::Months{number: 3, roll: Some(RollDay::Day(31))},
319    /// ]);
320    /// ```
321    pub fn try_vec_from(&self, udates: &Vec<NaiveDateTime>) -> Result<Vec<Frequency>, PyErr> {
322        match self {
323            Frequency::Months {
324                number: n,
325                roll: None,
326            } => {
327                // the RollDay is unspecified so get all possible RollDay variants
328                Ok(RollDay::vec_from(udates)
329                    .into_iter()
330                    .map(|r| Frequency::Months {
331                        number: *n,
332                        roll: Some(r),
333                    })
334                    .collect())
335            }
336            _ => {
337                // the Frequency is fully specified so return single element vector if
338                // at least 1 udate is valid
339                for udate in udates {
340                    if self.try_udate(udate).is_ok() {
341                        return Ok(vec![self.clone()]);
342                    }
343                }
344                Err(PyValueError::new_err(
345                    "The Frequency does not align with any of the `udates`.",
346                ))
347            }
348        }
349    }
350}
351
352impl Scheduling for Frequency {
353    /// Validate if an unadjusted date aligns with the specified [Frequency] variant.
354    ///
355    /// # Notes
356    /// This method will return error in one of two cases:
357    /// - The `udate` does not align with the fully defined variant.
358    /// - The variant is not fully defined (e.g. a [`Months`](Frequency) variant is missing
359    ///   a [`RollDay`](RollDay)) and cannot make the determination.
360    ///
361    /// Therefore,
362    /// - For a [CalDays](Frequency) variant or [Zero](Frequency) variant, any ``udate`` is valid.
363    /// - For a [BusDays](Frequency) variant, ``udate`` must be a business day.
364    /// - For a [Months](Frequency) variant, ``udate`` must align with the [RollDay]. If no [RollDay] is
365    ///   specified an error will always be returned.
366    ///
367    /// # Examples
368    /// ```rust
369    /// # use rateslib::scheduling::{Frequency, RollDay, ndt, Scheduling};
370    /// let result = Frequency::Months{number: 1, roll: Some(RollDay::IMM{})}.try_udate(&ndt(2025, 7, 16));
371    /// assert!(result.is_ok());
372    ///
373    /// let result = Frequency::Months{number: 1, roll: None}.try_udate(&ndt(2025, 7, 16));
374    /// assert!(result.is_err());
375    /// ```
376    fn try_udate(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr> {
377        match self {
378            Frequency::BusDays {
379                number: _n,
380                calendar: c,
381            } => {
382                if c.is_bus_day(udate) {
383                    Ok(*udate)
384                } else {
385                    Err(PyValueError::new_err(
386                        "`udate` is not a business day of the given calendar.",
387                    ))
388                }
389            }
390            Frequency::CalDays { number: _n } => Ok(*udate),
391            Frequency::Months {
392                number: _n,
393                roll: r,
394            } => match r {
395                Some(r) => r.try_udate(udate),
396                None => Err(PyValueError::new_err(
397                    "`udate` cannot be validated since RollDay is None.",
398                )),
399            },
400            Frequency::Zero {} => Ok(*udate),
401        }
402    }
403
404    fn next(&self, date: &NaiveDateTime) -> NaiveDateTime {
405        match self {
406            Frequency::BusDays {
407                number: n,
408                calendar: c,
409            } => c.lag_bus_days(date, *n, false),
410            Frequency::CalDays { number: n } => {
411                let cal = Cal::new(vec![], vec![]);
412                cal.add_cal_days(date, *n, &Adjuster::Actual {})
413            }
414            Frequency::Months { number: n, roll: r } => match r {
415                Some(r) => r.uadd(date, *n),
416                None => RollDay::Day(date.day()).uadd(date, *n),
417            },
418            Frequency::Zero {} => ndt(9999, 1, 1),
419        }
420    }
421
422    fn previous(&self, date: &NaiveDateTime) -> NaiveDateTime {
423        match self {
424            Frequency::BusDays {
425                number: n,
426                calendar: c,
427            } => c.lag_bus_days(date, -(*n), false),
428            Frequency::CalDays { number: n } => {
429                let cal = Cal::new(vec![], vec![]);
430                cal.add_cal_days(date, -(*n), &Adjuster::Actual {})
431            }
432            Frequency::Months { number: n, roll: r } => match r {
433                Some(r) => r.uadd(date, -(*n)),
434                None => RollDay::Day(date.day()).uadd(date, -(*n)),
435            },
436            Frequency::Zero {} => ndt(1500, 1, 1),
437        }
438    }
439
440    fn try_uregular(
441        &self,
442        ueffective: &NaiveDateTime,
443        utermination: &NaiveDateTime,
444    ) -> Result<Vec<NaiveDateTime>, PyErr> {
445        match self {
446            Frequency::Zero {} => Ok(vec![*ueffective, *utermination]),
447            _ => self.try_uregular_from_unext(ueffective, utermination),
448        }
449    }
450
451    fn periods_per_annum(&self) -> f64 {
452        match self {
453            Frequency::Zero {} => 0.01_f64,
454            Frequency::Months { number: 1, roll: _ } => 12.0_f64,
455            Frequency::Months { number: 2, roll: _ } => 6.0_f64,
456            Frequency::Months { number: 3, roll: _ } => 4.0_f64,
457            Frequency::Months { number: 4, roll: _ } => 3.0_f64,
458            Frequency::Months { number: 6, roll: _ } => 2.0_f64,
459            Frequency::Months {
460                number: 12,
461                roll: _,
462            } => 1.0_f64,
463            _ => periods_per_annum(self),
464        }
465    }
466}
467
468fn periods_per_annum<T: Scheduling + ?Sized>(obj: &T) -> f64 {
469    let mut date = obj.next(&ndt(1999, 12, 31));
470    if date > ndt(2049, 12, 31) {
471        // then the next method has generated an unusually long period. return nominal value
472        return 0.01_f64;
473    }
474    let estimated_end = date + Months::new(600);
475    let mut counter = 0_f64;
476    let count: f64;
477    loop {
478        counter += 1.0;
479        let prev = date;
480        date = obj.next(&prev);
481        if date < prev {
482            // Scheduling object is reversed so make a correction.
483            date = obj.previous(&prev)
484        }
485        if date >= estimated_end {
486            if (estimated_end - prev) < (date - estimated_end) {
487                count = f64::max(1.0, counter - 1.0);
488            } else {
489                count = counter;
490            }
491            break;
492        }
493    }
494    count / 50.0
495}
496
497// UNIT TESTS
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use crate::scheduling::ndt;
502
503    #[test]
504    fn test_try_udate() {
505        let options: Vec<(Frequency, NaiveDateTime)> = vec![
506            (
507                Frequency::BusDays {
508                    number: 4,
509                    calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
510                },
511                ndt(2025, 7, 11),
512            ),
513            (Frequency::CalDays { number: 4 }, ndt(2025, 7, 11)),
514            (Frequency::Zero {}, ndt(2025, 7, 11)),
515            (
516                Frequency::Months {
517                    number: 4,
518                    roll: Some(RollDay::Day(11)),
519                },
520                ndt(2025, 7, 11),
521            ),
522        ];
523        for option in options {
524            let result = option.0.try_udate(&option.1).unwrap();
525            assert_eq!(result, option.1);
526        }
527    }
528
529    #[test]
530    fn test_try_udate_err() {
531        let options: Vec<(Frequency, NaiveDateTime)> = vec![
532            (
533                Frequency::BusDays {
534                    number: 4,
535                    calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
536                },
537                ndt(2025, 7, 12),
538            ),
539            (
540                Frequency::Months {
541                    number: 4,
542                    roll: None,
543                },
544                ndt(2025, 7, 12),
545            ),
546            (
547                Frequency::Months {
548                    number: 4,
549                    roll: Some(RollDay::IMM {}),
550                },
551                ndt(2025, 7, 1),
552            ),
553        ];
554        for option in options {
555            assert!(option.0.try_udate(&option.1).is_err());
556        }
557    }
558
559    #[test]
560    fn test_is_regular_period_ok() {
561        let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
562            (
563                Frequency::CalDays { number: 5 },
564                ndt(2000, 1, 1),
565                ndt(2000, 1, 6),
566                true,
567            ),
568            (
569                Frequency::CalDays { number: 5 },
570                ndt(2000, 1, 1),
571                ndt(2000, 1, 5),
572                false,
573            ),
574            (
575                Frequency::Months {
576                    number: 5,
577                    roll: Some(RollDay::Day(1)),
578                },
579                ndt(2000, 1, 1),
580                ndt(2000, 6, 1),
581                true,
582            ),
583            (
584                Frequency::Months {
585                    number: 5,
586                    roll: Some(RollDay::Day(1)),
587                },
588                ndt(2000, 1, 1),
589                ndt(2000, 6, 5),
590                false,
591            ),
592        ];
593
594        for option in options {
595            let result = option.0.is_regular_period(&option.1, &option.2);
596            assert_eq!(result, option.3);
597        }
598    }
599
600    #[test]
601    fn test_is_short_front_stub() {
602        assert_eq!(
603            true,
604            Frequency::Months {
605                number: 1,
606                roll: Some(RollDay::Day(20))
607            }
608            .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 1, 20))
609        );
610        assert_eq!(
611            false,
612            Frequency::Months {
613                number: 1,
614                roll: Some(RollDay::Day(1))
615            }
616            .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 2, 1))
617        );
618        assert_eq!(
619            false,
620            Frequency::Months {
621                number: 1,
622                roll: None
623            }
624            .is_short_front_stub(&ndt(2000, 1, 1), &ndt(2000, 1, 15))
625        );
626    }
627
628    #[test]
629    fn test_is_long_front_stub() {
630        assert_eq!(
631            // is a valid long stub
632            true,
633            Frequency::Months {
634                number: 1,
635                roll: Some(RollDay::Day(20))
636            }
637            .is_long_front_stub(&ndt(2000, 1, 1), &ndt(2000, 2, 20))
638        );
639        assert_eq!(
640            // is a valid 2-regular period long stub
641            true,
642            Frequency::Months {
643                number: 1,
644                roll: Some(RollDay::Day(20))
645            }
646            .is_long_front_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 20))
647        );
648        assert_eq!(
649            // is too short
650            false,
651            Frequency::Months {
652                number: 1,
653                roll: Some(RollDay::Day(20))
654            }
655            .is_long_front_stub(&ndt(2000, 1, 25), &ndt(2000, 2, 20))
656        );
657        assert_eq!(
658            // is too long
659            false,
660            Frequency::Months {
661                number: 1,
662                roll: Some(RollDay::Day(20))
663            }
664            .is_long_front_stub(&ndt(2000, 1, 15), &ndt(2000, 3, 20))
665        );
666    }
667
668    #[test]
669    fn test_is_long_back_stub() {
670        assert_eq!(
671            // is a valid long stub
672            true,
673            Frequency::Months {
674                number: 1,
675                roll: Some(RollDay::Day(20))
676            }
677            .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 2, 28))
678        );
679        assert_eq!(
680            // is a valid 2-regular period long stub
681            true,
682            Frequency::Months {
683                number: 1,
684                roll: Some(RollDay::Day(20))
685            }
686            .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 20))
687        );
688        assert_eq!(
689            // is too short
690            false,
691            Frequency::Months {
692                number: 1,
693                roll: Some(RollDay::Day(20))
694            }
695            .is_long_back_stub(&ndt(2000, 1, 20), &ndt(2000, 2, 10))
696        );
697        assert_eq!(
698            // is too long
699            false,
700            Frequency::Months {
701                number: 1,
702                roll: Some(RollDay::Day(20))
703            }
704            .is_long_front_stub(&ndt(2000, 1, 20), &ndt(2000, 3, 30))
705        );
706    }
707
708    // #[test]
709    // fn test_try_scheduling() {
710    //     let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime)> = vec![
711    //         (
712    //             Frequency::Months {
713    //                 number: 1,
714    //                 roll: None,
715    //             },
716    //             ndt(2022, 7, 30),
717    //             ndt(2022, 8, 30),
718    //         ),
719    //         (
720    //             Frequency::Months {
721    //                 number: 2,
722    //                 roll: Some(RollDay::Day { day: 30 }),
723    //             },
724    //             ndt(2022, 7, 30),
725    //             ndt(2022, 9, 30),
726    //         ),
727    //         (
728    //             Frequency::Months {
729    //                 number: 3,
730    //                 roll: Some(RollDay::Day { day: 30 }),
731    //             },
732    //             ndt(2022, 7, 30),
733    //             ndt(2022, 10, 30),
734    //         ),
735    //         (
736    //             Frequency::Months {
737    //                 number: 4,
738    //                 roll: None,
739    //             },
740    //             ndt(2022, 7, 30),
741    //             ndt(2022, 11, 30),
742    //         ),
743    //         (
744    //             Frequency::Months {
745    //                 number: 6,
746    //                 roll: Some(RollDay::Day { day: 30 }),
747    //             },
748    //             ndt(2022, 7, 30),
749    //             ndt(2023, 1, 30),
750    //         ),
751    //         (
752    //             Frequency::Months {
753    //                 number: 12,
754    //                 roll: Some(RollDay::Day { day: 30 }),
755    //             },
756    //             ndt(2022, 7, 30),
757    //             ndt(2023, 7, 30),
758    //         ),
759    //         (
760    //             Frequency::Months {
761    //                 number: 1,
762    //                 roll: Some(RollDay::Day { day: 31 }),
763    //             },
764    //             ndt(2022, 6, 30),
765    //             ndt(2022, 7, 31),
766    //         ),
767    //         (
768    //             Frequency::Months {
769    //                 number: 1,
770    //                 roll: Some(RollDay::IMM {}),
771    //             },
772    //             ndt(2022, 6, 15),
773    //             ndt(2022, 7, 20),
774    //         ),
775    //         (
776    //             Frequency::CalDays { number: 5 },
777    //             ndt(2022, 6, 15),
778    //             ndt(2022, 6, 20),
779    //         ),
780    //         (
781    //             Frequency::CalDays { number: 14 },
782    //             ndt(2022, 6, 15),
783    //             ndt(2022, 6, 29),
784    //         ),
785    //         (
786    //             Frequency::BusDays {
787    //                 number: 5,
788    //                 calendar: Calendar::Cal(Cal::new(vec![], vec![5, 6])),
789    //             },
790    //             ndt(2025, 6, 23),
791    //             ndt(2025, 6, 30),
792    //         ),
793    //         (Frequency::Zero {}, ndt(1500, 1, 1), ndt(9999, 1, 1)),
794    //     ];
795    //     for option in options.iter() {
796    //         assert_eq!(option.2, option.0.try_unext(&option.1).unwrap());
797    //         assert_eq!(option.1, option.0.try_uprevious(&option.2).unwrap());
798    //     }
799    // }
800    //
801    #[test]
802    fn test_get_uschedule_imm() {
803        // test the example given in Coding Interest Rates
804        let result = Frequency::Months {
805            number: 1,
806            roll: Some(RollDay::IMM {}),
807        }
808        .try_uregular(&ndt(2023, 3, 15), &ndt(2023, 9, 20))
809        .unwrap();
810        assert_eq!(
811            result,
812            vec![
813                ndt(2023, 3, 15),
814                ndt(2023, 4, 19),
815                ndt(2023, 5, 17),
816                ndt(2023, 6, 21),
817                ndt(2023, 7, 19),
818                ndt(2023, 8, 16),
819                ndt(2023, 9, 20)
820            ]
821        );
822    }
823    //
824    // #[test]
825    // fn test_get_uschedule() {
826    //     let result = Frequency::Months {
827    //         number: 3,
828    //         roll: Some(RollDay::Day { day: 1 }),
829    //     }
830    //     .try_uregular(&ndt(2000, 1, 1), &ndt(2001, 1, 1))
831    //     .unwrap();
832    //     assert_eq!(
833    //         result,
834    //         vec![
835    //             ndt(2000, 1, 1),
836    //             ndt(2000, 4, 1),
837    //             ndt(2000, 7, 1),
838    //             ndt(2000, 10, 1),
839    //             ndt(2001, 1, 1)
840    //         ]
841    //     );
842    // }
843
844    // #[test]
845    // fn test_infer_ufront() {
846    //     let options: Vec<(
847    //         Frequency,
848    //         NaiveDateTime,
849    //         NaiveDateTime,
850    //         bool,
851    //         Option<NaiveDateTime>,
852    //     )> = vec![
853    //         (
854    //             Frequency::Months {
855    //                 number: 1,
856    //                 roll: Some(RollDay::Day { day: 15 }),
857    //             },
858    //             ndt(2022, 7, 30),
859    //             ndt(2022, 10, 15),
860    //             true,
861    //             Some(ndt(2022, 8, 15)),
862    //         ),
863    //         (
864    //             Frequency::Months {
865    //                 number: 1,
866    //                 roll: None,
867    //             },
868    //             ndt(2022, 7, 30),
869    //             ndt(2022, 10, 15),
870    //             false,
871    //             Some(ndt(2022, 9, 15)),
872    //         ),
873    //     ];
874    //
875    //     for option in options.iter() {
876    //         assert_eq!(
877    //             option.4,
878    //             option
879    //                 .0
880    //                 .try_infer_ufront_stub(&option.1, &option.2, option.3)
881    //                 .unwrap()
882    //         );
883    //     }
884    // }
885
886    // #[test]
887    // fn test_infer_ufront_err() {
888    //     let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
889    //         (
890    //             Frequency::Months {
891    //                 number: 1,
892    //                 roll: Some(RollDay::Day { day: 15 }),
893    //             },
894    //             ndt(2022, 7, 30),
895    //             ndt(2022, 8, 15),
896    //             true,
897    //         ),
898    //         (
899    //             Frequency::Months {
900    //                 number: 1,
901    //                 roll: None,
902    //             },
903    //             ndt(2022, 7, 30),
904    //             ndt(2022, 9, 15),
905    //             false,
906    //         ),
907    //         (
908    //             Frequency::Zero {},
909    //             ndt(2022, 7, 30),
910    //             ndt(2022, 9, 15),
911    //             false,
912    //         ),
913    //     ];
914    //
915    //     for option in options.iter() {
916    //         let result = option
917    //             .0
918    //             .try_infer_ufront_stub(&option.1, &option.2, option.3)
919    //             .is_err();
920    //         assert_eq!(true, result);
921    //     }
922    // }
923
924    // #[test]
925    // fn test_infer_uback() {
926    //     let options: Vec<(
927    //         Frequency,
928    //         NaiveDateTime,
929    //         NaiveDateTime,
930    //         bool,
931    //         Option<NaiveDateTime>,
932    //     )> = vec![
933    //         (
934    //             Frequency::Months {
935    //                 number: 1,
936    //                 roll: Some(RollDay::Day { day: 30 }),
937    //             },
938    //             ndt(2022, 7, 30),
939    //             ndt(2022, 10, 15),
940    //             true,
941    //             Some(ndt(2022, 9, 30)),
942    //         ),
943    //         (
944    //             Frequency::Months {
945    //                 number: 1,
946    //                 roll: Some(RollDay::Day { day: 30 }),
947    //             },
948    //             ndt(2022, 7, 30),
949    //             ndt(2022, 10, 15),
950    //             false,
951    //             Some(ndt(2022, 8, 30)),
952    //         ),
953    //     ];
954    //
955    //     for option in options.iter() {
956    //         assert_eq!(
957    //             option.4,
958    //             option
959    //                 .0
960    //                 .try_infer_uback_stub(&option.1, &option.2, option.3)
961    //                 .unwrap()
962    //         );
963    //     }
964    // }
965    //
966    // #[test]
967    // fn test_infer_uback_err() {
968    //     let options: Vec<(Frequency, NaiveDateTime, NaiveDateTime, bool)> = vec![
969    //         (
970    //             Frequency::Months {
971    //                 number: 1,
972    //                 roll: Some(RollDay::Day { day: 30 }),
973    //             },
974    //             ndt(2022, 7, 30),
975    //             ndt(2022, 8, 15),
976    //             true,
977    //         ),
978    //         (
979    //             Frequency::Months {
980    //                 number: 1,
981    //                 roll: Some(RollDay::Day { day: 30 }),
982    //             },
983    //             ndt(2022, 7, 30),
984    //             ndt(2022, 9, 15),
985    //             false,
986    //         ),
987    //     ];
988    //
989    //     for option in options.iter() {
990    //         let result = option
991    //             .0
992    //             .try_infer_uback_stub(&option.1, &option.2, option.3)
993    //             .is_err();
994    //         assert_eq!(true, result);
995    //     }
996    // }
997    //
998    #[test]
999    fn test_try_vec_from() {
1000        let options: Vec<(Frequency, Vec<NaiveDateTime>, Vec<Frequency>)> = vec![
1001            (
1002                Frequency::Months {
1003                    number: 1,
1004                    roll: None,
1005                },
1006                vec![ndt(2022, 7, 30)],
1007                vec![Frequency::Months {
1008                    number: 1,
1009                    roll: Some(RollDay::Day(30)),
1010                }],
1011            ),
1012            (
1013                Frequency::Months {
1014                    number: 1,
1015                    roll: None,
1016                },
1017                vec![ndt(2022, 2, 28)],
1018                vec![
1019                    Frequency::Months {
1020                        number: 1,
1021                        roll: Some(RollDay::Day(28)),
1022                    },
1023                    Frequency::Months {
1024                        number: 1,
1025                        roll: Some(RollDay::Day(29)),
1026                    },
1027                    Frequency::Months {
1028                        number: 1,
1029                        roll: Some(RollDay::Day(30)),
1030                    },
1031                    Frequency::Months {
1032                        number: 1,
1033                        roll: Some(RollDay::Day(31)),
1034                    },
1035                ],
1036            ),
1037            (
1038                Frequency::CalDays { number: 1 },
1039                vec![ndt(2022, 2, 28)],
1040                vec![Frequency::CalDays { number: 1 }],
1041            ),
1042        ];
1043
1044        for option in options.iter() {
1045            let result = option.0.try_vec_from(&option.1).unwrap();
1046            assert_eq!(option.2, result);
1047        }
1048    }
1049
1050    #[test]
1051    fn test_try_vec_from_err() {
1052        let options: Vec<(Frequency, Vec<NaiveDateTime>)> = vec![(
1053            Frequency::Months {
1054                number: 1,
1055                roll: Some(RollDay::IMM {}),
1056            },
1057            vec![ndt(2022, 7, 30)],
1058        )];
1059
1060        for option in options.iter() {
1061            assert_eq!(true, option.0.try_vec_from(&option.1).is_err());
1062        }
1063    }
1064
1065    #[test]
1066    fn test_coupons_per_annum() {
1067        let options: Vec<(Frequency, f64)> = vec![
1068            (Frequency::CalDays { number: 365 }, 1.0),
1069            (Frequency::CalDays { number: 182 }, 2.0),
1070            (Frequency::CalDays { number: 183 }, 2.0),
1071            (Frequency::CalDays { number: 91 }, 4.02),
1072            (Frequency::CalDays { number: 28 }, 13.04),
1073            (Frequency::CalDays { number: 7 }, 52.18),
1074            (
1075                Frequency::BusDays {
1076                    number: 5,
1077                    calendar: Cal::new(vec![], vec![5, 6]).into(),
1078                },
1079                52.18,
1080            ),
1081            (
1082                Frequency::BusDays {
1083                    number: 63,
1084                    calendar: Cal::new(vec![], vec![5, 6]).into(),
1085                },
1086                4.14,
1087            ),
1088            (
1089                Frequency::BusDays {
1090                    number: 62,
1091                    calendar: Cal::new(vec![], vec![5, 6]).into(),
1092                },
1093                4.2,
1094            ),
1095            (
1096                Frequency::Months {
1097                    number: 1,
1098                    roll: None,
1099                },
1100                12.0,
1101            ),
1102            (
1103                Frequency::Months {
1104                    number: 2,
1105                    roll: None,
1106                },
1107                6.0,
1108            ),
1109            (
1110                Frequency::Months {
1111                    number: 3,
1112                    roll: None,
1113                },
1114                4.0,
1115            ),
1116            (
1117                Frequency::Months {
1118                    number: 4,
1119                    roll: None,
1120                },
1121                3.0,
1122            ),
1123            (
1124                Frequency::Months {
1125                    number: 6,
1126                    roll: None,
1127                },
1128                2.0,
1129            ),
1130            (
1131                Frequency::Months {
1132                    number: 9,
1133                    roll: None,
1134                },
1135                1.34,
1136            ),
1137            (
1138                Frequency::Months {
1139                    number: 12,
1140                    roll: None,
1141                },
1142                1.0,
1143            ),
1144            (
1145                Frequency::Months {
1146                    number: 24,
1147                    roll: None,
1148                },
1149                0.5,
1150            ),
1151            (
1152                Frequency::Months {
1153                    number: 3,
1154                    roll: Some(RollDay::IMM()),
1155                },
1156                4.0,
1157            ),
1158            (Frequency::Zero {}, 0.01),
1159        ];
1160        for option in options {
1161            let result = option.0.periods_per_annum();
1162            assert_eq!(result, option.1);
1163        }
1164    }
1165}