rateslib/scheduling/
schedule.rs

1use crate::scheduling::{
2    get_unadjusteds, Adjuster, Adjustment, Calendar, Frequency, RollDay, Scheduling,
3};
4use chrono::prelude::*;
5use itertools::iproduct;
6use pyo3::exceptions::PyValueError;
7use pyo3::{pyclass, PyErr};
8use serde::{Deserialize, Serialize};
9
10/// An indicator used by [`Schedule::try_new_inferred`] to instruct its inference logic.
11#[pyclass(module = "rateslib.rs", eq, eq_int)]
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum StubInference {
14    /// Short front stub inference.
15    ShortFront = 0,
16    /// Long front stub inference.
17    LongFront = 1,
18    /// Short back stub inference.
19    ShortBack = 2,
20    /// Long back stub inference.
21    LongBack = 3,
22}
23
24/// A generic financial schedule with regular contiguous periods and, possibly, stubs.
25///
26/// # Notes
27/// - A **regular** schedule has a [`Frequency`] that perfectly divides its ``ueffective`` and
28///   ``utermination`` dates, and has no stub dates.
29/// - An **irregular** schedule has a ``ufront_stub`` and/or ``uback_stub`` dates defining periods
30///   at the boundary of the schedule which are not a standard length of time defined by the
31///   [`Frequency`]. However, a regular schedule must exist between those interior dates.
32#[pyclass(module = "rateslib.rs", eq)]
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34#[serde(from = "ScheduleDataModel")]
35pub struct Schedule {
36    /// The unadjusted start date of the schedule.
37    pub ueffective: NaiveDateTime,
38    /// The unadjusted end date of the schedule.
39    pub utermination: NaiveDateTime,
40    /// The scheduling [`Frequency`] for regular periods.
41    pub frequency: Frequency,
42    /// The optional, unadjusted front stub date.
43    pub ufront_stub: Option<NaiveDateTime>,
44    /// The optional, unadjusted back stub date.
45    pub uback_stub: Option<NaiveDateTime>,
46    /// The [`Calendar`] for accrual and payment date adjustment.
47    pub calendar: Calendar,
48    /// The [`Adjuster`] to adjust the unadjusted schedule dates to adjusted period accrual dates.
49    pub accrual_adjuster: Adjuster,
50    /// The [`Adjuster`] to adjust the adjusted schedule dates to period payment dates.
51    pub payment_adjuster: Adjuster,
52    /// The vector of unadjusted period accrual dates.
53    #[serde(skip)]
54    pub uschedule: Vec<NaiveDateTime>,
55    /// The vector of adjusted period accrual dates.
56    #[serde(skip)]
57    pub aschedule: Vec<NaiveDateTime>,
58    /// The vector of payment dates associated with the adjusted accrual dates.
59    #[serde(skip)]
60    pub pschedule: Vec<NaiveDateTime>,
61}
62
63#[derive(Deserialize)]
64struct ScheduleDataModel {
65    ueffective: NaiveDateTime,
66    utermination: NaiveDateTime,
67    frequency: Frequency,
68    ufront_stub: Option<NaiveDateTime>,
69    uback_stub: Option<NaiveDateTime>,
70    calendar: Calendar,
71    accrual_adjuster: Adjuster,
72    payment_adjuster: Adjuster,
73}
74
75impl std::convert::From<ScheduleDataModel> for Schedule {
76    fn from(model: ScheduleDataModel) -> Self {
77        Self::try_new_defined(
78            model.ueffective,
79            model.utermination,
80            model.frequency,
81            model.ufront_stub,
82            model.uback_stub,
83            model.calendar,
84            model.accrual_adjuster,
85            model.payment_adjuster,
86        )
87        .expect("Data model for `Schedule` is corrupt or invalid.")
88    }
89}
90
91/// Check that right is greater than left if both Some, and that they do not create a 'dead stub'.
92fn validate_individual_dates(
93    left: &Option<NaiveDateTime>,
94    right: &Option<NaiveDateTime>,
95    accrual_adjuster: &Adjuster,
96    calendar: &Calendar,
97) -> Result<(), PyErr> {
98    match (left, right) {
99        (Some(_left), Some(_right)) => {}
100        _ => return Ok(()),
101    }
102    if left >= right {
103        return Err(PyValueError::new_err(
104            "Dates are invalid since they are repeated.",
105        ));
106    }
107    if accrual_adjuster.adjust(&left.unwrap(), calendar)
108        >= accrual_adjuster.adjust(&right.unwrap(), calendar)
109    {
110        return Err(PyValueError::new_err(
111            "Dates define dead stubs and are invalid",
112        ));
113    }
114    Ok(())
115}
116
117/// Ensure dates are ordered and that they do not define 'dead stubs', which are created when
118/// two scheduling dates are adjusted under some [Adjusted] and result in the same date.
119fn validate_date_ordering(
120    ueffective: &NaiveDateTime,
121    ufront_stub: &Option<NaiveDateTime>,
122    uback_stub: &Option<NaiveDateTime>,
123    utermination: &NaiveDateTime,
124    accrual_adjuster: &Adjuster,
125    calendar: &Calendar,
126) -> Result<(), PyErr> {
127    let _ = validate_individual_dates(&Some(*ueffective), ufront_stub, accrual_adjuster, calendar)?;
128    let _ = validate_individual_dates(&Some(*ueffective), uback_stub, accrual_adjuster, calendar)?;
129    let _ = validate_individual_dates(
130        &Some(*ueffective),
131        &Some(*utermination),
132        accrual_adjuster,
133        calendar,
134    )?;
135    // front and back stub dates can be equal if the schedule is defined only by two stubs
136    // let _ = validate_individual_dates(ufront_stub, uback_stub, accrual_adjuster, calendar)?;
137    let _ = validate_individual_dates(
138        ufront_stub,
139        &Some(*utermination),
140        accrual_adjuster,
141        calendar,
142    )?;
143    let _ =
144        validate_individual_dates(uback_stub, &Some(*utermination), accrual_adjuster, calendar)?;
145    Ok(())
146}
147
148/// Ensure that two dates can define a proper stub period, either short or long, front or back.
149fn validate_is_stub(
150    left: &NaiveDateTime,
151    right: &NaiveDateTime,
152    frequency: &Frequency,
153    front: bool,
154) -> Result<(), PyErr> {
155    if front {
156        if frequency.is_front_stub(left, right) {
157            Ok(())
158        } else {
159            Err(PyValueError::new_err(
160                "Dates intended to define a front stub do not permit a valid stub period.",
161            ))
162        }
163    } else {
164        if frequency.is_back_stub(left, right) {
165            Ok(())
166        } else {
167            Err(PyValueError::new_err(
168                "Dates intended to define a back stub do not permit a valid stub period.",
169            ))
170        }
171    }
172}
173
174impl Schedule {
175    /// Create a [`Schedule`] from well defined unadjusted dates and a [`Frequency`].
176    ///
177    /// # Notes
178    /// If provided arguments do not define a valid schedule pattern then an error is returned.
179    ///
180    /// # Examples
181    /// This is a valid schedule with a long back stub and regular monthly periods.
182    /// ```rust
183    /// # use rateslib::scheduling::{Schedule, ndt, Frequency, Adjuster, Calendar, Cal, RollDay};
184    /// let s = Schedule::try_new_defined(
185    ///     ndt(2024, 1, 3), ndt(2024, 4, 15),                  // ueffective, utermination
186    ///     Frequency::Months{number:1, roll: Some(RollDay::Day(3))}, // frequency
187    ///     None, Some(ndt(2024, 3, 3)),                        // ufront_stub, uback_stub
188    ///     Cal::new(vec![], vec![5,6]).into(),                 // calendar
189    ///     Adjuster::ModifiedFollowing{},                      // accrual_adjuster
190    ///     Adjuster::BusDaysLagSettle(3),                      // payment_adjuster
191    /// );
192    /// # let s = s.unwrap();
193    /// assert_eq!(s.uschedule, vec![ndt(2024, 1, 3), ndt(2024, 2, 3), ndt(2024, 3, 3), ndt(2024, 4, 15)]);
194    /// assert_eq!(s.aschedule, vec![ndt(2024, 1, 3), ndt(2024, 2, 5), ndt(2024, 3, 4), ndt(2024, 4, 15)]);
195    /// assert_eq!(s.pschedule, vec![ndt(2024, 1, 8), ndt(2024, 2, 8), ndt(2024, 3, 7), ndt(2024, 4, 18)]);
196    /// ```
197    /// This is not a valid schedule since there are no defined stubs and the dates do not align
198    /// with the [RollDay].
199    /// ```rust
200    /// # use rateslib::scheduling::{Schedule, ndt, Frequency, Adjuster, Calendar, Cal, RollDay};
201    /// let s = Schedule::try_new_defined(
202    ///     ndt(2024, 1, 6), ndt(2024, 4, 6),                  // ueffective, utermination
203    ///     Frequency::Months{number:1, roll: Some(RollDay::Day(3))}, // frequency
204    ///     None, None,                                         // ufront_stub, uback_stub
205    ///     Cal::new(vec![], vec![5,6]).into(),                 // calendar
206    ///     Adjuster::ModifiedFollowing{},                      // accrual_adjuster
207    ///     Adjuster::BusDaysLagSettle(3),                      // payment_adjuster
208    /// );
209    /// assert!(s.is_err());
210    /// ```
211    pub fn try_new_defined(
212        ueffective: NaiveDateTime,
213        utermination: NaiveDateTime,
214        frequency: Frequency,
215        ufront_stub: Option<NaiveDateTime>,
216        uback_stub: Option<NaiveDateTime>,
217        calendar: Calendar,
218        accrual_adjuster: Adjuster,
219        payment_adjuster: Adjuster,
220    ) -> Result<Self, PyErr> {
221        // validate date ordering
222        let _ = validate_date_ordering(
223            &ueffective,
224            &ufront_stub,
225            &uback_stub,
226            &utermination,
227            &accrual_adjuster,
228            &calendar,
229        )?;
230
231        let uschedule: Vec<NaiveDateTime>;
232
233        match (ufront_stub, uback_stub) {
234            (None, None) => {
235                // then schedule is defined only by ueffective and utermination
236                let uregular = frequency.try_uregular(&ueffective, &utermination);
237                if uregular.is_ok() {
238                    // case 1) schedule must be a regular schedule
239                    uschedule = uregular.unwrap();
240                } else if frequency.is_front_stub(&ueffective, &utermination)
241                    || frequency.is_back_stub(&ueffective, &utermination)
242                {
243                    //case 2) schedule must be a single period stub
244                    uschedule = vec![ueffective, utermination];
245                } else {
246                    return Err(PyValueError::new_err("`ueffective`, `utermination` and `frequency` do not define a regular schedule or a single period stub."));
247                }
248            }
249            (Some(regular_start), None) => {
250                // case 3) with a front stub
251                let uregular = frequency.try_uregular(&regular_start, &utermination)?;
252                let _ = validate_is_stub(&ueffective, &regular_start, &frequency, true)?;
253                uschedule = composite_uschedule(
254                    &ueffective,
255                    &utermination,
256                    &ufront_stub,
257                    &uback_stub,
258                    &uregular,
259                );
260            }
261            (None, Some(regular_end)) => {
262                // case 3) with a back stub
263                let uregular = frequency.try_uregular(&ueffective, &regular_end)?;
264                let _ = validate_is_stub(&regular_end, &utermination, &frequency, false)?;
265                uschedule = composite_uschedule(
266                    &ueffective,
267                    &utermination,
268                    &ufront_stub,
269                    &uback_stub,
270                    &uregular,
271                );
272            }
273            (Some(regular_start), Some(regular_end)) => {
274                let _ = validate_is_stub(&ueffective, &regular_start, &frequency, true)?;
275                let _ = validate_is_stub(&regular_end, &utermination, &frequency, false)?;
276                if regular_start == regular_end {
277                    // is only possible when stubs are both given and are equal, due to date validation
278                    // case 4) schedule must be two stubs
279                    uschedule = vec![ueffective, regular_start, utermination];
280                } else {
281                    // case 5) some regular component with stubs at both ends
282                    let uregular = frequency.try_uregular(&regular_start, &regular_end)?;
283                    uschedule = composite_uschedule(
284                        &ueffective,
285                        &utermination,
286                        &ufront_stub,
287                        &uback_stub,
288                        &uregular,
289                    );
290                }
291            }
292        }
293
294        let aschedule: Vec<NaiveDateTime> = accrual_adjuster.adjusts(&uschedule, &calendar);
295        let pschedule = payment_adjuster.adjusts(&aschedule, &calendar);
296
297        Ok(Self {
298            ueffective,
299            utermination,
300            frequency,
301            ufront_stub,
302            uback_stub,
303            calendar: calendar.clone(),
304            accrual_adjuster,
305            payment_adjuster,
306            uschedule,
307            aschedule,
308            pschedule,
309        })
310    }
311
312    /// Create a [`Schedule`] from unadjusted dates with specified [`StubInference`].
313    ///
314    /// # Notes
315    /// This method introduces the ``stub_inference`` argument.
316    /// If it is given as `None` then this method will revert to [Schedule::try_new_uschedule].
317    /// If ``stub_inference`` is given but it conflicts with an explicit ``stub`` date given then
318    /// an error will be returned.
319    /// If ``stub_inference`` is given but a ``stub`` date is not required then a valid [Schedule]
320    /// is returned without an inferred stub.
321    fn try_new_infer_stub(
322        ueffective: NaiveDateTime,
323        utermination: NaiveDateTime,
324        frequency: Frequency,
325        ufront_stub: Option<NaiveDateTime>,
326        uback_stub: Option<NaiveDateTime>,
327        calendar: Calendar,
328        accrual_adjuster: Adjuster,
329        payment_adjuster: Adjuster,
330        stub_inference: Option<StubInference>,
331    ) -> Result<Self, PyErr> {
332        // evaluate if schedule is valid as defined without stub inference
333        let temp_schedule = Schedule::try_new_defined(
334            ueffective,
335            utermination,
336            frequency.clone(),
337            ufront_stub,
338            uback_stub,
339            calendar.clone(),
340            accrual_adjuster,
341            payment_adjuster,
342        );
343
344        // validate inference is not blocked by user defined values.
345        let _ = validate_stub_dates_and_inference(&ufront_stub, &uback_stub, &stub_inference)?;
346
347        let stubs: (Option<NaiveDateTime>, Option<NaiveDateTime>);
348        if stub_inference.is_none() {
349            return temp_schedule;
350        } else {
351            let (interior_start, interior_end) =
352                match_interior_dates(&ueffective, &ufront_stub, &uback_stub, &utermination);
353            stubs = match stub_inference.unwrap() {
354                StubInference::ShortFront => {
355                    if temp_schedule.is_ok() {
356                        let test_schedule = temp_schedule.unwrap();
357                        if frequency.is_short_front_stub(
358                            &test_schedule.uschedule[0],
359                            &test_schedule.uschedule[1],
360                        ) {
361                            return Ok(test_schedule);
362                        } // already has a short front stub
363                    }
364                    (
365                        frequency.try_infer_ufront_stub(&interior_start, &interior_end, true)?,
366                        uback_stub,
367                    )
368                }
369                StubInference::LongFront => {
370                    if temp_schedule.is_ok() {
371                        let test_schedule = temp_schedule.unwrap();
372                        if frequency.is_long_front_stub(
373                            &test_schedule.uschedule[0],
374                            &test_schedule.uschedule[1],
375                        ) {
376                            return Ok(test_schedule);
377                        } // already has a long front stub
378                    }
379                    (
380                        frequency.try_infer_ufront_stub(&interior_start, &interior_end, false)?,
381                        uback_stub,
382                    )
383                }
384                StubInference::ShortBack => {
385                    if temp_schedule.is_ok() {
386                        let test_schedule = temp_schedule.unwrap();
387                        let n = test_schedule.uschedule.len();
388                        if frequency.is_short_back_stub(
389                            &test_schedule.uschedule[n - 1],
390                            &test_schedule.uschedule[n - 2],
391                        ) {
392                            return Ok(test_schedule);
393                        } // already has a short back stub
394                    }
395                    (
396                        ufront_stub,
397                        frequency.try_infer_uback_stub(&interior_start, &interior_end, true)?,
398                    )
399                }
400                StubInference::LongBack => {
401                    if temp_schedule.is_ok() {
402                        let test_schedule = temp_schedule.unwrap();
403                        let n = test_schedule.uschedule.len();
404                        if frequency.is_short_back_stub(
405                            &test_schedule.uschedule[n - 1],
406                            &test_schedule.uschedule[n - 2],
407                        ) {
408                            return Ok(test_schedule);
409                        } // already has a long back stub
410                    }
411                    (
412                        ufront_stub,
413                        frequency.try_infer_uback_stub(&interior_start, &interior_end, false)?,
414                    )
415                }
416            }
417        }
418        Self::try_new_defined(
419            ueffective,
420            utermination,
421            frequency,
422            stubs.0,
423            stubs.1,
424            calendar,
425            accrual_adjuster,
426            payment_adjuster,
427        )
428    }
429
430    /// Create a [`Schedule`] from unadjusted dates.
431    ///
432    /// # Notes
433    ///
434    /// An unadjusted regular schedule, that aligns with [Frequency], must be defined between
435    /// the relevant dates. If not an error is returned.
436    ///
437    /// This method uses [Scheduling::try_uregular](crate::scheduling::Scheduling::try_uregular)
438    /// to ascertain if the provided dates define a regular schedule or not.
439    fn try_new_uschedule_infer_frequency(
440        ueffective: NaiveDateTime,
441        utermination: NaiveDateTime,
442        frequency: Frequency,
443        ufront_stub: Option<NaiveDateTime>,
444        uback_stub: Option<NaiveDateTime>,
445        calendar: Calendar,
446        accrual_adjuster: Adjuster,
447        payment_adjuster: Adjuster,
448        eom: bool,
449        stub_inference: Option<StubInference>,
450    ) -> Result<Self, PyErr> {
451        // evaluate the Options and get the start and end of regular schedule component
452        let (regular_start, regular_end) =
453            match_interior_dates(&ueffective, &ufront_stub, &uback_stub, &utermination);
454
455        // get all possible Frequency variants. this will often only be 1 element
456        let frequencies = frequency.try_vec_from(&vec![regular_start, regular_end])?;
457
458        // find all possible schedules that are valid for frequencies
459        let uschedules: Vec<Schedule> = frequencies
460            .into_iter()
461            .filter_map(|f| {
462                Schedule::try_new_infer_stub(
463                    ueffective,
464                    utermination,
465                    f,
466                    ufront_stub,
467                    uback_stub,
468                    calendar.clone(),
469                    accrual_adjuster,
470                    payment_adjuster,
471                    stub_inference,
472                )
473                .ok()
474            })
475            .collect();
476
477        // error if no valid schedules were found
478        if uschedules.len() == 0 {
479            return Err(PyValueError::new_err(
480                "No valid Schedules could be created with given `udates` combinations and `frequency`.",
481            ));
482        }
483
484        // filter regular schedules
485        let regulars: Vec<Schedule> = uschedules
486            .iter()
487            .cloned()
488            .filter(|schedule| schedule.is_regular())
489            .collect();
490        if regulars.len() != 0 {
491            Ok(filter_schedules_by_eom(regulars, eom))
492        } else {
493            Ok(filter_schedules_by_eom(uschedules, eom))
494        }
495    }
496
497    /// Create a [`Schedule`] using inference if some of the parameters are not well defined.
498    ///
499    /// # Notes
500    /// If all parameters are well defined and dates are definitively known in their unadjusted
501    /// forms then the [`try_new_defined`](Schedule::try_new_defined) method
502    /// should be used instead.
503    ///
504    /// This method provides the additional features below:
505    /// - **Unadjusted date inference**: if *adjusted* dates are given then a neighbourhood of
506    ///   dates will be sub-sampled to determine
507    ///   any possibilities for *unadjusted* dates defined by the `accrual_adjuster` and `calendar`.
508    ///   Only the dates at either side of the regular schedule component are explored. Stub date
509    ///   boundaries are used as provided.
510    /// - **Frequency inference**: any [`Frequency`](crate::scheduling::Frequency) that contains
511    ///   optional elements, e.g. no [`RollDay`],
512    ///   will be explored for all possible alternatives that results in the most likely schedule,
513    ///   guided by the `eom` parameter.
514    /// - **Stub date inference**: one-sided stub date inference can be attempted guided by
515    ///   the `stub_inference` parameter.
516    pub fn try_new_inferred(
517        effective: NaiveDateTime,
518        termination: NaiveDateTime,
519        frequency: Frequency,
520        front_stub: Option<NaiveDateTime>,
521        back_stub: Option<NaiveDateTime>,
522        calendar: Calendar,
523        accrual_adjuster: Adjuster,
524        payment_adjuster: Adjuster,
525        eom: bool,
526        stub_inference: Option<StubInference>,
527    ) -> Result<Schedule, PyErr> {
528        // find all unadjusted combinations. only adjust the boundaries of the regular component.
529        let dates: (
530            Vec<NaiveDateTime>,
531            Vec<Option<NaiveDateTime>>,
532            Vec<Option<NaiveDateTime>>,
533            Vec<NaiveDateTime>,
534        ) = match (front_stub, back_stub) {
535            (None, None) => (
536                get_unadjusteds(&effective, &accrual_adjuster, &calendar),
537                vec![None],
538                vec![None],
539                get_unadjusteds(&termination, &accrual_adjuster, &calendar),
540            ),
541            (Some(d), None) => (
542                vec![effective],
543                get_unadjusteds(&d, &accrual_adjuster, &calendar)
544                    .into_iter()
545                    .map(Some)
546                    .collect(),
547                vec![None],
548                get_unadjusteds(&termination, &accrual_adjuster, &calendar),
549            ),
550            (None, Some(d)) => (
551                get_unadjusteds(&effective, &accrual_adjuster, &calendar),
552                vec![None],
553                get_unadjusteds(&d, &accrual_adjuster, &calendar)
554                    .into_iter()
555                    .map(Some)
556                    .collect(),
557                vec![termination],
558            ),
559            (Some(d), Some(d2)) => (
560                vec![effective],
561                get_unadjusteds(&d, &accrual_adjuster, &calendar)
562                    .into_iter()
563                    .map(Some)
564                    .collect(),
565                get_unadjusteds(&d2, &accrual_adjuster, &calendar)
566                    .into_iter()
567                    .map(Some)
568                    .collect(),
569                vec![termination],
570            ),
571        };
572
573        let combinations = iproduct!(dates.0, dates.1, dates.2, dates.3);
574        let schedules: Vec<Schedule> = combinations
575            .into_iter()
576            .filter_map(|(e, fs, bs, t)| {
577                Schedule::try_new_uschedule_infer_frequency(
578                    e,
579                    t,
580                    frequency.clone(),
581                    fs,
582                    bs,
583                    calendar.clone(),
584                    accrual_adjuster,
585                    payment_adjuster,
586                    eom,
587                    stub_inference,
588                )
589                .ok()
590            })
591            .collect();
592
593        if schedules.len() == 0 {
594            Err(PyValueError::new_err(
595                "A Schedule could not be generated from the parameter combinations.",
596            ))
597        } else {
598            // filter regular schedules
599            let regulars: Vec<Schedule> = schedules
600                .iter()
601                .cloned()
602                .filter(|schedule| schedule.is_regular())
603                .collect();
604            if regulars.len() != 0 {
605                Ok(filter_schedules_by_eom(regulars, eom))
606            } else {
607                Ok(filter_schedules_by_eom(schedules, eom))
608            }
609        }
610    }
611
612    /// Check if a [`Schedule`] contains only regular periods, and no stub periods.
613    pub fn is_regular(&self) -> bool {
614        let ucheck = self
615            .frequency
616            .try_uregular(&self.ueffective, &self.utermination);
617        if ucheck.is_ok() {
618            ucheck.unwrap() == self.uschedule
619        } else {
620            false
621        }
622    }
623}
624
625fn match_interior_dates(
626    ueffective: &NaiveDateTime,
627    ufront_stub: &Option<NaiveDateTime>,
628    uback_stub: &Option<NaiveDateTime>,
629    utermination: &NaiveDateTime,
630) -> (NaiveDateTime, NaiveDateTime) {
631    match (ufront_stub, uback_stub) {
632        (None, None) => (*ueffective, *utermination),
633        (Some(v), None) => (*v, *utermination),
634        (None, Some(v)) => (*ueffective, *v),
635        (Some(v), Some(w)) => (*v, *w),
636    }
637}
638
639/// Validate provided stubs do not conflict with the required [StubInference]
640fn validate_stub_dates_and_inference(
641    ufront_stub: &Option<NaiveDateTime>,
642    uback_stub: &Option<NaiveDateTime>,
643    stub_inference: &Option<StubInference>,
644) -> Result<(), PyErr> {
645    match (ufront_stub, uback_stub, stub_inference) {
646        (Some(_v), Some(_w), Some(_f)) => Err(PyValueError::new_err(
647            "Cannot infer stubs if they are explicitly given.",
648        )),
649        (Some(_v), None, Some(val))
650            if matches!(val, StubInference::ShortFront | StubInference::LongFront) =>
651        {
652            Err(PyValueError::new_err(
653                "Cannot infer stubs if they are explicitly given.",
654            ))
655        }
656        (None, Some(_w), Some(val))
657            if matches!(val, StubInference::ShortBack | StubInference::LongBack) =>
658        {
659            Err(PyValueError::new_err(
660                "Cannot infer stubs if they are explicitly given.",
661            ))
662        }
663        _ => Ok(()),
664    }
665}
666
667/// Get unadjusted schedule dates assuming all inputs are correct and pre-validated.
668fn composite_uschedule(
669    ueffective: &NaiveDateTime,
670    utermination: &NaiveDateTime,
671    ufront_stub: &Option<NaiveDateTime>,
672    uback_stub: &Option<NaiveDateTime>,
673    regular_uschedule: &Vec<NaiveDateTime>,
674) -> Vec<NaiveDateTime> {
675    let mut uschedule: Vec<NaiveDateTime> = vec![];
676    match (*ufront_stub, *uback_stub) {
677        (None, None) => {
678            uschedule.extend(regular_uschedule);
679        }
680        (Some(_v), None) => {
681            uschedule.push(*ueffective);
682            uschedule.extend(regular_uschedule);
683        }
684        (None, Some(_v)) => {
685            uschedule.extend(regular_uschedule);
686            uschedule.push(*utermination);
687        }
688        (Some(_v), Some(_w)) => {
689            uschedule.push(*ueffective);
690            uschedule.extend(regular_uschedule);
691            uschedule.push(*utermination);
692        }
693    }
694    uschedule
695}
696
697fn filter_schedules_by_eom(uschedules: Vec<Schedule>, eom: bool) -> Schedule {
698    // filter the found schedules. if `eom` then prefer the first schedule with RollDay::Day(31)
699    // else prefer the first found schedule.
700    let original = uschedules[0].clone();
701
702    if !eom {
703        // just return the first schedule
704        original
705    } else {
706        // scan for an eom possibility
707        let possibles: Vec<Schedule> = uschedules
708            .into_iter()
709            .filter(|s| {
710                matches!(
711                    s.frequency,
712                    Frequency::Months {
713                        number: _,
714                        roll: Some(RollDay::Day(31))
715                    }
716                )
717            })
718            .collect();
719        if possibles.len() >= 1 {
720            possibles[0].clone()
721        } else {
722            original
723        }
724    }
725}
726
727// UNIT TESTS
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use crate::scheduling::{ndt, Cal, RollDay};
732
733    #[test]
734    fn test_new_uschedule_defined_cases_1_and_2() {
735        let options: Vec<(NaiveDateTime, NaiveDateTime, Vec<NaiveDateTime>)> = vec![
736            (
737                ndt(2000, 1, 1), // regular schedule
738                ndt(2000, 3, 1),
739                vec![ndt(2000, 1, 1), ndt(2000, 2, 1), ndt(2000, 3, 1)],
740            ),
741            (
742                ndt(2000, 1, 1), // short single period sub
743                ndt(2000, 1, 20),
744                vec![ndt(2000, 1, 1), ndt(2000, 1, 20)],
745            ),
746            (
747                ndt(2000, 1, 1), // long single period stub
748                ndt(2000, 2, 15),
749                vec![ndt(2000, 1, 1), ndt(2000, 2, 15)],
750            ),
751        ];
752        for option in options {
753            let result = Schedule::try_new_defined(
754                option.0,
755                option.1,
756                Frequency::Months {
757                    number: 1,
758                    roll: Some(RollDay::Day(1)),
759                },
760                None,
761                None,
762                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
763                Adjuster::Following {},
764                Adjuster::Following {},
765            );
766            assert_eq!(result.unwrap().uschedule, option.2);
767        }
768    }
769
770    #[test]
771    fn test_new_uschedule_defined_cases_1_and_2_err() {
772        let options: Vec<(NaiveDateTime, NaiveDateTime, Frequency)> = vec![
773            (
774                ndt(2000, 1, 1), // regular schedule is not defined stub too long
775                ndt(2000, 3, 15),
776                Frequency::Months {
777                    number: 1,
778                    roll: Some(RollDay::Day(1)),
779                },
780            ),
781            (
782                ndt(2000, 1, 1), // undefined RollDay
783                ndt(2000, 3, 1),
784                Frequency::Months {
785                    number: 1,
786                    roll: None,
787                },
788            ),
789        ];
790        for option in options {
791            let result = Schedule::try_new_defined(
792                option.0,
793                option.1,
794                option.2,
795                None,
796                None,
797                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
798                Adjuster::Following {},
799                Adjuster::Following {},
800            );
801            assert!(result.is_err());
802        }
803    }
804
805    #[test]
806    fn test_new_uschedule_defined_cases_4() {
807        let options: Vec<(
808            NaiveDateTime,
809            NaiveDateTime,
810            NaiveDateTime,
811            Vec<NaiveDateTime>,
812        )> = vec![
813            (
814                ndt(2000, 1, 1), // Short then Short
815                ndt(2000, 1, 15),
816                ndt(2000, 2, 10),
817                vec![ndt(2000, 1, 1), ndt(2000, 1, 15), ndt(2000, 2, 10)],
818            ),
819            (
820                ndt(2000, 1, 1), // Short then Long
821                ndt(2000, 1, 15),
822                ndt(2000, 2, 25),
823                vec![ndt(2000, 1, 1), ndt(2000, 1, 15), ndt(2000, 2, 25)],
824            ),
825            (
826                ndt(2000, 1, 1), // Long then Short
827                ndt(2000, 2, 15),
828                ndt(2000, 2, 25),
829                vec![ndt(2000, 1, 1), ndt(2000, 2, 15), ndt(2000, 2, 25)],
830            ),
831            (
832                ndt(2000, 1, 1), // Long then Long
833                ndt(2000, 2, 15),
834                ndt(2000, 3, 20),
835                vec![ndt(2000, 1, 1), ndt(2000, 2, 15), ndt(2000, 3, 20)],
836            ),
837        ];
838        for option in options {
839            let result = Schedule::try_new_defined(
840                option.0,
841                option.2,
842                Frequency::Months {
843                    number: 1,
844                    roll: Some(RollDay::Day(15)),
845                }, // Zero also works, as does CalDays(30)
846                Some(option.1),
847                Some(option.1),
848                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
849                Adjuster::Following {},
850                Adjuster::Following {},
851            );
852            assert_eq!(result.unwrap().uschedule, option.3);
853        }
854    }
855
856    #[test]
857    fn test_new_uschedule_defined_cases_3() {
858        let options: Vec<(
859            NaiveDateTime,
860            Option<NaiveDateTime>,
861            Option<NaiveDateTime>,
862            NaiveDateTime,
863            Vec<NaiveDateTime>,
864        )> = vec![
865            (
866                ndt(2000, 1, 1), // Short then Regular
867                Some(ndt(2000, 1, 15)),
868                None,
869                ndt(2000, 3, 15),
870                vec![
871                    ndt(2000, 1, 1),
872                    ndt(2000, 1, 15),
873                    ndt(2000, 2, 15),
874                    ndt(2000, 3, 15),
875                ],
876            ),
877            (
878                ndt(2000, 1, 1), // Long then Regular
879                Some(ndt(2000, 2, 15)),
880                None,
881                ndt(2000, 4, 15),
882                vec![
883                    ndt(2000, 1, 1),
884                    ndt(2000, 2, 15),
885                    ndt(2000, 3, 15),
886                    ndt(2000, 4, 15),
887                ],
888            ),
889            (
890                ndt(2000, 1, 15), // Regular then Short
891                None,
892                Some(ndt(2000, 3, 15)),
893                ndt(2000, 4, 10),
894                vec![
895                    ndt(2000, 1, 15),
896                    ndt(2000, 2, 15),
897                    ndt(2000, 3, 15),
898                    ndt(2000, 4, 10),
899                ],
900            ),
901            (
902                ndt(2000, 1, 15), // Regular then Long
903                None,
904                Some(ndt(2000, 3, 15)),
905                ndt(2000, 4, 25),
906                vec![
907                    ndt(2000, 1, 15),
908                    ndt(2000, 2, 15),
909                    ndt(2000, 3, 15),
910                    ndt(2000, 4, 25),
911                ],
912            ),
913            (
914                ndt(2000, 1, 15), // Regular then 2 -period Long
915                None,
916                Some(ndt(2000, 3, 15)),
917                ndt(2000, 5, 15),
918                vec![
919                    ndt(2000, 1, 15),
920                    ndt(2000, 2, 15),
921                    ndt(2000, 3, 15),
922                    ndt(2000, 5, 15),
923                ],
924            ),
925        ];
926        for option in options {
927            let result = Schedule::try_new_defined(
928                option.0,
929                option.3,
930                Frequency::Months {
931                    number: 1,
932                    roll: Some(RollDay::Day(15)),
933                }, // Zero also works
934                option.1,
935                option.2,
936                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
937                Adjuster::Following {},
938                Adjuster::Following {},
939            );
940            assert_eq!(result.unwrap().uschedule, option.4);
941        }
942    }
943
944    #[test]
945    fn test_new_uschedule_defined_cases_3_err() {
946        let options: Vec<(
947            NaiveDateTime,
948            Option<NaiveDateTime>,
949            Option<NaiveDateTime>,
950            NaiveDateTime,
951        )> = vec![
952            (
953                ndt(2000, 1, 1), // Short then Regular misaligned
954                Some(ndt(2000, 1, 15)),
955                None,
956                ndt(2000, 3, 16),
957            ),
958            (
959                ndt(2000, 1, 1), // Front Stub is too long
960                Some(ndt(2000, 5, 15)),
961                None,
962                ndt(2000, 7, 15),
963            ),
964            (
965                ndt(2000, 1, 13), // Regular misaligned then Short
966                None,
967                Some(ndt(2000, 3, 15)),
968                ndt(2000, 4, 10),
969            ),
970            (
971                ndt(2000, 1, 15), // Back Stub is too long
972                None,
973                Some(ndt(2000, 3, 15)),
974                ndt(2000, 7, 25),
975            ),
976            (
977                ndt(2000, 1, 15), // Short stub cannot be a regular period
978                None,
979                Some(ndt(2000, 3, 15)),
980                ndt(2000, 4, 15),
981            ),
982        ];
983        for option in options {
984            let result = Schedule::try_new_defined(
985                option.0,
986                option.3,
987                Frequency::Months {
988                    number: 1,
989                    roll: Some(RollDay::Day(15)),
990                }, // Zero also works
991                option.1,
992                option.2,
993                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
994                Adjuster::Following {},
995                Adjuster::Following {},
996            );
997            assert!(result.is_err());
998        }
999    }
1000
1001    #[test]
1002    fn test_new_uschedule_defined_cases_5() {
1003        let options: Vec<(
1004            NaiveDateTime,
1005            Option<NaiveDateTime>,
1006            Option<NaiveDateTime>,
1007            NaiveDateTime,
1008            Vec<NaiveDateTime>,
1009        )> = vec![
1010            (
1011                ndt(2000, 1, 1), // Short Short
1012                Some(ndt(2000, 1, 15)),
1013                Some(ndt(2000, 3, 15)),
1014                ndt(2000, 4, 10),
1015                vec![
1016                    ndt(2000, 1, 1),
1017                    ndt(2000, 1, 15),
1018                    ndt(2000, 2, 15),
1019                    ndt(2000, 3, 15),
1020                    ndt(2000, 4, 10),
1021                ],
1022            ),
1023            (
1024                ndt(2000, 1, 1), // Short Long
1025                Some(ndt(2000, 1, 15)),
1026                Some(ndt(2000, 3, 15)),
1027                ndt(2000, 4, 25),
1028                vec![
1029                    ndt(2000, 1, 1),
1030                    ndt(2000, 1, 15),
1031                    ndt(2000, 2, 15),
1032                    ndt(2000, 3, 15),
1033                    ndt(2000, 4, 25),
1034                ],
1035            ),
1036            (
1037                ndt(2000, 1, 1), // Long Long
1038                Some(ndt(2000, 2, 15)),
1039                Some(ndt(2000, 3, 15)),
1040                ndt(2000, 4, 25),
1041                vec![
1042                    ndt(2000, 1, 1),
1043                    ndt(2000, 2, 15),
1044                    ndt(2000, 3, 15),
1045                    ndt(2000, 4, 25),
1046                ],
1047            ),
1048            (
1049                ndt(2000, 1, 1), // Long Short
1050                Some(ndt(2000, 2, 15)),
1051                Some(ndt(2000, 3, 15)),
1052                ndt(2000, 4, 10),
1053                vec![
1054                    ndt(2000, 1, 1),
1055                    ndt(2000, 2, 15),
1056                    ndt(2000, 3, 15),
1057                    ndt(2000, 4, 10),
1058                ],
1059            ),
1060        ];
1061        for option in options {
1062            let result = Schedule::try_new_defined(
1063                option.0,
1064                option.3,
1065                Frequency::Months {
1066                    number: 1,
1067                    roll: Some(RollDay::Day(15)),
1068                }, // Zero also works
1069                option.1,
1070                option.2,
1071                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1072                Adjuster::Following {},
1073                Adjuster::Following {},
1074            );
1075            assert_eq!(result.unwrap().uschedule, option.4);
1076        }
1077    }
1078
1079    #[test]
1080    fn test_new_uschedule_defined_cases_5_err() {
1081        let options: Vec<(
1082            NaiveDateTime,
1083            Option<NaiveDateTime>,
1084            Option<NaiveDateTime>,
1085            NaiveDateTime,
1086        )> = vec![(
1087            ndt(2000, 1, 1), // Regular is misaligned
1088            Some(ndt(2000, 1, 15)),
1089            Some(ndt(2000, 3, 16)),
1090            ndt(2000, 4, 10),
1091        )];
1092        for option in options {
1093            let result = Schedule::try_new_defined(
1094                option.0,
1095                option.3,
1096                Frequency::Months {
1097                    number: 1,
1098                    roll: Some(RollDay::Day(15)),
1099                }, // Zero also works
1100                option.1,
1101                option.2,
1102                Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1103                Adjuster::Following {},
1104                Adjuster::Following {},
1105            );
1106            assert!(result.is_err());
1107        }
1108    }
1109
1110    #[test]
1111    fn test_new_uschedule_defined_err() {
1112        // test that None RollDay produces errors even for a well defined schedule
1113        let result = Schedule::try_new_defined(
1114            ndt(2000, 1, 1),
1115            ndt(2001, 1, 1),
1116            Frequency::Months {
1117                number: 6,
1118                roll: None,
1119            },
1120            None,
1121            None,
1122            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1123            Adjuster::Actual {},
1124            Adjuster::Actual {},
1125        );
1126        assert!(result.is_err())
1127    }
1128
1129    #[test]
1130    fn test_try_new_uschedule_dead_stubs() {
1131        let s = Schedule::try_new_defined(
1132            ndt(2023, 1, 1),
1133            ndt(2024, 1, 2),
1134            Frequency::Months {
1135                number: 6,
1136                roll: Some(RollDay::Day(2)),
1137            },
1138            Some(ndt(2023, 1, 2)),
1139            None,
1140            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1141            Adjuster::ModifiedFollowing {},
1142            Adjuster::BusDaysLagSettle(1),
1143        );
1144        assert!(s.is_err()); // 1st Jan is adjusted to 2nd Jan aligning with front stub
1145
1146        let s = Schedule::try_new_defined(
1147            ndt(2022, 1, 1),
1148            ndt(2023, 1, 2),
1149            Frequency::Months {
1150                number: 6,
1151                roll: Some(RollDay::Day(1)),
1152            },
1153            None,
1154            Some(ndt(2023, 1, 1)),
1155            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1156            Adjuster::ModifiedFollowing {},
1157            Adjuster::BusDaysLagSettle(1),
1158        );
1159        assert!(s.is_err()); // 1st Jan is adjusted to 2nd Jan aligning with front stub
1160    }
1161
1162    #[test]
1163    fn test_try_new_uschedule_eom_parameter_selection() {
1164        let s = Schedule::try_new_uschedule_infer_frequency(
1165            ndt(2024, 2, 29),
1166            ndt(2024, 11, 30),
1167            Frequency::Months {
1168                number: 3,
1169                roll: None,
1170            },
1171            None,
1172            None,
1173            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1174            Adjuster::ModifiedFollowing {},
1175            Adjuster::BusDaysLagSettle(1),
1176            true,
1177            None,
1178        )
1179        .unwrap();
1180        assert_eq!(
1181            s.frequency,
1182            Frequency::Months {
1183                number: 3,
1184                roll: Some(RollDay::Day(31))
1185            }
1186        );
1187
1188        let s = Schedule::try_new_uschedule_infer_frequency(
1189            ndt(2024, 2, 29),
1190            ndt(2024, 11, 30),
1191            Frequency::Months {
1192                number: 3,
1193                roll: None,
1194            },
1195            None,
1196            None,
1197            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1198            Adjuster::ModifiedFollowing {},
1199            Adjuster::BusDaysLagSettle(1),
1200            false,
1201            None,
1202        )
1203        .unwrap();
1204        assert_eq!(
1205            s.frequency,
1206            Frequency::Months {
1207                number: 3,
1208                roll: Some(RollDay::Day(30))
1209            }
1210        );
1211
1212        let s = Schedule::try_new_uschedule_infer_frequency(
1213            ndt(2024, 2, 29),
1214            ndt(2024, 11, 29),
1215            Frequency::Months {
1216                number: 3,
1217                roll: None,
1218            },
1219            None,
1220            None,
1221            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1222            Adjuster::ModifiedFollowing {},
1223            Adjuster::BusDaysLagSettle(1),
1224            true,
1225            None,
1226        )
1227        .unwrap();
1228        assert_eq!(
1229            s.frequency,
1230            Frequency::Months {
1231                number: 3,
1232                roll: Some(RollDay::Day(29))
1233            }
1234        );
1235    }
1236
1237    #[test]
1238    fn test_try_new_uschedule_inferred_fails() {
1239        // fails because stub dates are given as well as an inference enum
1240        assert_eq!(
1241            true,
1242            Schedule::try_new_infer_stub(
1243                ndt(2000, 1, 1),
1244                ndt(2000, 2, 1),
1245                Frequency::CalDays { number: 100 },
1246                Some(ndt(2000, 1, 10)),
1247                Some(ndt(2000, 1, 16)),
1248                Calendar::Cal(Cal::new(vec![], vec![])),
1249                Adjuster::ModifiedFollowing {},
1250                Adjuster::BusDaysLagSettle(1),
1251                Some(StubInference::ShortBack)
1252            )
1253            .is_err()
1254        );
1255
1256        // fails because stub date is given as well as an inference enum
1257        assert_eq!(
1258            true,
1259            Schedule::try_new_infer_stub(
1260                ndt(2000, 1, 1),
1261                ndt(2000, 2, 1),
1262                Frequency::CalDays { number: 100 },
1263                None,
1264                Some(ndt(2000, 1, 16)),
1265                Calendar::Cal(Cal::new(vec![], vec![])),
1266                Adjuster::ModifiedFollowing {},
1267                Adjuster::BusDaysLagSettle(1),
1268                Some(StubInference::ShortBack)
1269            )
1270            .is_err()
1271        );
1272
1273        // fails because stub date is given as well as an inference enum
1274        assert_eq!(
1275            true,
1276            Schedule::try_new_infer_stub(
1277                ndt(2000, 1, 1),
1278                ndt(2000, 2, 1),
1279                Frequency::CalDays { number: 100 },
1280                None,
1281                Some(ndt(2000, 1, 16)),
1282                Calendar::Cal(Cal::new(vec![], vec![])),
1283                Adjuster::ModifiedFollowing {},
1284                Adjuster::BusDaysLagSettle(1),
1285                Some(StubInference::LongBack)
1286            )
1287            .is_err()
1288        );
1289
1290        // fails because stub date is given as well as an inference enum
1291        assert_eq!(
1292            true,
1293            Schedule::try_new_infer_stub(
1294                ndt(2000, 1, 1),
1295                ndt(2000, 2, 1),
1296                Frequency::CalDays { number: 100 },
1297                Some(ndt(2000, 1, 16)),
1298                None,
1299                Calendar::Cal(Cal::new(vec![], vec![])),
1300                Adjuster::ModifiedFollowing {},
1301                Adjuster::BusDaysLagSettle(1),
1302                Some(StubInference::ShortFront)
1303            )
1304            .is_err()
1305        );
1306
1307        // fails because stub date is given as well as an inference enum
1308        assert_eq!(
1309            true,
1310            Schedule::try_new_infer_stub(
1311                ndt(2000, 1, 1),
1312                ndt(2000, 2, 1),
1313                Frequency::CalDays { number: 100 },
1314                Some(ndt(2000, 1, 16)),
1315                None,
1316                Calendar::Cal(Cal::new(vec![], vec![])),
1317                Adjuster::ModifiedFollowing {},
1318                Adjuster::BusDaysLagSettle(1),
1319                Some(StubInference::LongFront)
1320            )
1321            .is_err()
1322        );
1323    }
1324
1325    #[test]
1326    fn test_try_new_schedule_short_period() {
1327        // test infer stub works when no stub is required for single period stub case
1328        let s = Schedule::try_new_uschedule_infer_frequency(
1329            ndt(2022, 7, 1),
1330            ndt(2022, 10, 1),
1331            Frequency::Months {
1332                number: 12,
1333                roll: None,
1334            },
1335            None,
1336            None,
1337            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1338            Adjuster::ModifiedFollowing {},
1339            Adjuster::BusDaysLagSettle(1),
1340            true,
1341            Some(StubInference::ShortFront),
1342        )
1343        .expect("short period");
1344        assert_eq!(s.uschedule, vec![ndt(2022, 7, 1), ndt(2022, 10, 1)]);
1345    }
1346
1347    #[test]
1348    fn test_try_new_schedule_infer_frequency_imm() {
1349        // test IMM frequency is inferred
1350        let s = Schedule::try_new_uschedule_infer_frequency(
1351            ndt(2025, 3, 19),
1352            ndt(2025, 9, 17),
1353            Frequency::Months {
1354                number: 3,
1355                roll: None,
1356            },
1357            None,
1358            None,
1359            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1360            Adjuster::ModifiedFollowing {},
1361            Adjuster::BusDaysLagSettle(1),
1362            true,
1363            None,
1364        )
1365        .expect("short period");
1366        assert_eq!(
1367            s.frequency,
1368            Frequency::Months {
1369                number: 3,
1370                roll: Some(RollDay::IMM())
1371            }
1372        );
1373    }
1374
1375    #[test]
1376    fn test_is_regular() {
1377        let s = Schedule::try_new_uschedule_infer_frequency(
1378            ndt(2025, 3, 19),
1379            ndt(2025, 9, 19),
1380            Frequency::Months {
1381                number: 3,
1382                roll: Some(RollDay::Day(19)),
1383            },
1384            None,
1385            None,
1386            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1387            Adjuster::ModifiedFollowing {},
1388            Adjuster::BusDaysLagSettle(1),
1389            true,
1390            None,
1391        )
1392        .expect("regular");
1393        assert!(s.is_regular());
1394
1395        let s = Schedule::try_new_uschedule_infer_frequency(
1396            ndt(2025, 3, 19),
1397            ndt(2025, 9, 25),
1398            Frequency::Months {
1399                number: 3,
1400                roll: Some(RollDay::Day(19)),
1401            },
1402            None,
1403            None,
1404            Calendar::Cal(Cal::new(vec![], vec![5, 6])),
1405            Adjuster::ModifiedFollowing {},
1406            Adjuster::BusDaysLagSettle(1),
1407            true,
1408            Some(StubInference::ShortBack),
1409        )
1410        .expect("regular");
1411        assert!(!s.is_regular());
1412    }
1413
1414    #[test]
1415    fn test_front_stub_inference() {
1416        let s = Schedule::try_new_inferred(
1417            ndt(2022, 1, 1),
1418            ndt(2022, 6, 1),
1419            Frequency::Months {
1420                number: 3,
1421                roll: None,
1422            },
1423            None,
1424            None,
1425            Calendar::Cal(Cal::new(vec![], vec![])),
1426            Adjuster::ModifiedFollowing {},
1427            Adjuster::BusDaysLagSettle(2),
1428            false,
1429            Some(StubInference::ShortFront),
1430        )
1431        .expect("schedule is valid");
1432        assert_eq!(
1433            s.uschedule,
1434            vec![ndt(2022, 1, 1), ndt(2022, 3, 1), ndt(2022, 6, 1)]
1435        );
1436    }
1437}