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}