Skip to content

DEV: add skeleton for Schedule #689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 72 additions & 16 deletions rust/calendars/dateroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,59 @@
IMM {},
}

// impl RollDay {
// /// Validate whether a given date is a possible variant of the RollDay.
// ///
// /// Returns an error string if invalid or None if it is valid.
// pub(crate) fn validate_date(&self, date: &NaiveDateTime) -> Option<String> {
// match self {
// RollDay::Unspecified{} => None, // any date satisfies unspecified RollDay
// RollDay::Int{day: 31} | RollDay::Eom{} => {
//
// }
// RollDay::IMM {} => if is_imm(date) { None } else {Some("`date` does not align with given `roll`.".to_string())},
// RollDay::Int {day: value} => if date.day() == *value {None} else {Some("`date` does not align with given `roll`.".to_string())}
// RollDay::SoM {} => if date.day() == 1 {None} else {Some("`date` does not align with given `roll`.".to_string())}
// }
// }
// }
impl RollDay {
/// Validate whether a given `date` is a possible variant of the RollDay.
///
/// Returns an error string if invalid or None if it is valid.
pub(crate) fn validate_date(&self, date: &NaiveDateTime) -> Option<String> {

Check failure on line 29 in rust/calendars/dateroll.rs

View workflow job for this annotation

GitHub Actions / build (3.13)

method `validate_date` is never used
let msg = "`date` does not align with given `roll`.".to_string();
match self {
RollDay::Unspecified {} => None, // any date satisfies unspecified RollDay
RollDay::Int { day: 31 } | RollDay::EoM {} => {
if is_eom(date) {
None
} else {
Some(msg)
}
}
RollDay::Int { day: 30 } => {
if (is_eom(date) && date.day() < 30) || date.day() == 30 {
None
} else {
Some(msg)
}
}
RollDay::Int { day: 29 } => {
if (is_eom(date) && date.day() < 29) || date.day() == 29 {
None
} else {
Some(msg)
}
}
RollDay::IMM {} => {
if is_imm(date) {
None
} else {
Some(msg)
}
}
RollDay::Int { day: value } => {
if date.day() == *value {
None
} else {
Some(msg)
}
}
RollDay::SoM {} => {
if date.day() == 1 {
None
} else {
Some(msg)
}
}
}
}
}

/// A rule to adjust a non-business day to a business day.
#[pyclass(module = "rateslib.rs", eq, eq_int)]
Expand Down Expand Up @@ -836,4 +873,23 @@
assert_eq!(true, is_leap_year(2024));
assert_eq!(false, is_leap_year(2022));
}

#[test]
fn test_rollday_validate_date() {
let options: Vec<(RollDay, NaiveDateTime)> = vec![
(RollDay::Int { day: 15 }, ndt(2000, 3, 15)),
(RollDay::Int { day: 31 }, ndt(2000, 3, 31)),
(RollDay::Int { day: 31 }, ndt(2022, 2, 28)),
(RollDay::EoM {}, ndt(2000, 3, 31)),
(RollDay::EoM {}, ndt(2022, 2, 28)),
(RollDay::Int { day: 30 }, ndt(2024, 2, 29)),
(RollDay::Int { day: 30 }, ndt(2024, 2, 29)),
(RollDay::EoM {}, ndt(2024, 2, 29)),
(RollDay::EoM {}, ndt(2024, 2, 29)),
(RollDay::EoM {}, ndt(2024, 2, 29)),
];
for option in options {
assert_eq!(None, option.0.validate_date(&option.1));
}
}
}
78 changes: 77 additions & 1 deletion rust/scheduling/enums.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::calendars::{is_leap_year, CalType, Modifier, RollDay};
use crate::calendars::{is_leap_year, Cal, CalType, DateRoll, Modifier, RollDay};
use chrono::prelude::*;
use pyo3::pyclass;

Expand Down Expand Up @@ -72,6 +72,25 @@ impl Frequency {
let months = end.month() - start.month();
(months % self.months()) == 0_u32
}

/// Calculate the next unadjusted period date in a schedule given a valid `ueffective` date.
///
/// Note `ueffective` should be valid relative to the roll. If unsure, call
/// ``roll.validate_date(&ueffective)``.
pub fn next_period(&self, ueffective: &NaiveDateTime, roll: &RollDay) -> NaiveDateTime {
let cal = Cal::new(vec![], vec![]);
match self {
Frequency::Monthly => cal.add_months(ueffective, 1, &Modifier::Act, roll, true),
Frequency::BiMonthly => cal.add_months(ueffective, 2, &Modifier::Act, roll, true),
Frequency::Quarterly => cal.add_months(ueffective, 3, &Modifier::Act, roll, true),
Frequency::TriAnnually => cal.add_months(ueffective, 4, &Modifier::Act, roll, true),
Frequency::SemiAnnually => cal.add_months(ueffective, 6, &Modifier::Act, roll, true),
Frequency::Annually => cal.add_months(ueffective, 12, &Modifier::Act, roll, true),
Frequency::Zero => {
panic!("`next_period` is undefined for Frequency::Zero.")
}
}
}
}

/// Date categories to infer rolls on dates.
Expand Down Expand Up @@ -373,4 +392,61 @@ mod tests {
);
}
}

#[test]
fn test_get_next_period() {
let options: Vec<(Frequency, NaiveDateTime, RollDay, NaiveDateTime)> = vec![
(
Frequency::Monthly,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2022, 8, 30),
),
(
Frequency::BiMonthly,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2022, 9, 30),
),
(
Frequency::Quarterly,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2022, 10, 30),
),
(
Frequency::TriAnnually,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2022, 11, 30),
),
(
Frequency::SemiAnnually,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2023, 1, 30),
),
(
Frequency::Annually,
ndt(2022, 7, 30),
RollDay::Unspecified {},
ndt(2023, 7, 30),
),
(
Frequency::Monthly,
ndt(2022, 6, 30),
RollDay::EoM {},
ndt(2022, 7, 31),
),
(
Frequency::Monthly,
ndt(2022, 6, 15),
RollDay::IMM {},
ndt(2022, 7, 20),
),
];
for option in options.iter() {
assert_eq!(option.3, option.0.next_period(&option.1, &option.2));
}
}
}
116 changes: 91 additions & 25 deletions rust/scheduling/schedule.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
// use crate::calendars::{is_leap_year, CalType, Modifier, RollDay};
// use chrono::prelude::*;
// use pyo3::exceptions::PyValueError;
// use pyo3::{pyclass, PyErr};
// use std::cmp::{Ordering, PartialEq};
//
// use crate::scheduling::enums::Frequency;

// pub struct Schedule {
// ueffective: NaiveDateTime,
// utermination: NaiveDateTime,
// frequency: Frequency,
// front_stub: Option<NaiveDateTime>,
// back_stub: Option<NaiveDateTime>,
// roll: RollDay,
// modifier: Modifier,
// calendar: CalType,
// payment_lag: i8,
//
// // created data objects
// uschedule: Vec<NaiveDateTime>,
// aschedule: Vec<NaiveDateTime>,
// pschedule: Vec<NaiveDateTime>,
// }
use crate::calendars::{is_leap_year, CalType, Modifier, RollDay};
use crate::scheduling::enums::{Frequency, Stub};
use chrono::prelude::*;
use pyo3::exceptions::PyValueError;
use pyo3::{pyclass, PyErr};
use std::cmp::{Ordering, PartialEq};

pub struct Schedule {
ueffective: NaiveDateTime,
utermination: NaiveDateTime,
frequency: Frequency,
roll: RollDay,
front_stub: Option<NaiveDateTime>,
back_stub: Option<NaiveDateTime>,
modifier: Modifier,
calendar: CalType,
payment_lag: i8,

// created data objects
uschedule: Vec<NaiveDateTime>,
aschedule: Vec<NaiveDateTime>,
pschedule: Vec<NaiveDateTime>,
}

// impl Schedule {
// pub fn try_new(
Expand All @@ -37,10 +36,34 @@
// calendar: CalType,
// payment_lag: i8,
// ) -> Result<Self, PyErr> {
// OK()
// Ok()
// }
// }

pub(crate) fn regular_unadjusted_schedule(
ueffective: &NaiveDateTime,
utermination: &NaiveDateTime,
frequency: &Frequency,
roll: &RollDay,
) -> Vec<NaiveDateTime> {
// validation tests
if roll.validate_date(&ueffective).is_some() {
panic!("`ueffective` is not valid.")
}
if roll.validate_date(&utermination).is_some() {
panic!("`utermination` is not valid.")
}
if !frequency.is_divisible(&ueffective, &utermination) {
panic!("`frequency` is not valid on dates.")
}

let mut ret: Vec<NaiveDateTime> = vec![ueffective.clone()];
while ret.last().unwrap() < utermination {
ret.push(frequency.next_period(ret.last().unwrap(), roll));
}
ret
}

// UNIT TESTS
#[cfg(test)]
mod tests {
Expand All @@ -51,4 +74,47 @@ mod tests {
// let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; // Saturday and Monday
// Cal::new(hols, vec![5, 6])
// }

#[test]
fn test_regular_unadjusted_schedule() {
let result = regular_unadjusted_schedule(
&ndt(2000, 1, 1),
&ndt(2001, 1, 1),
&Frequency::Quarterly,
&RollDay::SoM {},
);
assert_eq!(
result,
vec![
ndt(2000, 1, 1),
ndt(2000, 4, 1),
ndt(2000, 7, 1),
ndt(2000, 10, 1),
ndt(2001, 1, 1)
]
);
}

#[test]
fn test_regular_unadjusted_schedule_imm() {
// test the example given in Coding Interest Rates
let result = regular_unadjusted_schedule(
&ndt(2023, 3, 15),
&ndt(2023, 9, 20),
&Frequency::Monthly,
&RollDay::IMM {},
);
assert_eq!(
result,
vec![
ndt(2023, 3, 15),
ndt(2023, 4, 19),
ndt(2023, 5, 17),
ndt(2023, 6, 21),
ndt(2023, 7, 19),
ndt(2023, 8, 16),
ndt(2023, 9, 20)
]
);
}
}
2 changes: 1 addition & 1 deletion rust/scheduling/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::calendars::RollDay;
use crate::scheduling::enums::{Frequency, RollDayCategory, ValidateSchedule};
use crate::scheduling::enums::{Frequency, RollDayCategory};
use chrono::prelude::*;

/// Infer a RollDay from given dates of a regular schedule.
Expand Down
Loading