1use chrono::prelude::*;
2use indexmap::IndexSet;
3use pyo3::exceptions::PyValueError;
4use pyo3::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::cmp::{Eq, PartialEq};
7
8use crate::scheduling::{Adjuster, Adjustment, Calendar, Imm};
9
10#[pyclass(module = "rateslib.rs", eq)]
12#[derive(Debug, Copy, Hash, Clone, PartialEq, Eq, Deserialize, Serialize)]
13pub enum RollDay {
14 Day(u32),
16 IMM(),
18}
19
20impl RollDay {
21 pub fn vec_from(udates: &Vec<NaiveDateTime>) -> Vec<Self> {
49 let mut set: IndexSet<RollDay> = IndexSet::new();
50
51 for udate in udates {
52 let mut v: Vec<Self> = vec![RollDay::Day(udate.day())];
54 if Imm::Eom.validate(udate) {
56 let mut day = udate.day() + 1;
57 while day < 32 {
58 v.push(RollDay::Day(day));
59 day = day + 1;
60 }
61 }
62 if Imm::Wed3.validate(udate) {
64 v.push(RollDay::IMM())
65 }
66 set.append(&mut IndexSet::<RollDay>::from_iter(v));
68 }
69 set.into_iter().collect()
70 }
71
72 pub fn try_udate(&self, udate: &NaiveDateTime) -> Result<NaiveDateTime, PyErr> {
84 let msg = "`udate` does not align with given `RollDay`.".to_string();
85 match self {
86 RollDay::Day(31) => {
87 if Imm::Eom.validate(udate) {
88 Ok(*udate)
89 } else {
90 Err(PyValueError::new_err(msg))
91 }
92 }
93 RollDay::Day(30) => {
94 if (Imm::Eom.validate(udate) && udate.day() < 30) || udate.day() == 30 {
95 Ok(*udate)
96 } else {
97 Err(PyValueError::new_err(msg))
98 }
99 }
100 RollDay::Day(29) => {
101 if (Imm::Eom.validate(udate) && udate.day() < 29) || udate.day() == 29 {
102 Ok(*udate)
103 } else {
104 Err(PyValueError::new_err(msg))
105 }
106 }
107 RollDay::IMM() => {
108 if Imm::Wed3.validate(udate) {
109 Ok(*udate)
110 } else {
111 Err(PyValueError::new_err(msg))
112 }
113 }
114 RollDay::Day(value) => {
115 if udate.day() == *value {
116 Ok(*udate)
117 } else {
118 Err(PyValueError::new_err(msg))
119 }
120 }
121 }
122 }
123
124 pub fn try_uadd(&self, udate: &NaiveDateTime, months: i32) -> Result<NaiveDateTime, PyErr> {
139 let _ = self.try_udate(udate)?;
140 Ok(self.uadd(udate, months))
141 }
142
143 pub fn uadd(&self, udate: &NaiveDateTime, months: i32) -> NaiveDateTime {
152 let mut yr_roll = (months.abs() / 12) * months.signum();
154 let rem_months = months - yr_roll * 12;
155
156 let mut new_month = i32::try_from(udate.month()).unwrap() + rem_months;
158 if new_month <= 0 {
159 yr_roll -= 1;
160 new_month = new_month.rem_euclid(12);
161 } else if new_month >= 13 {
162 yr_roll += 1;
163 new_month = new_month.rem_euclid(12);
164 }
165 if new_month == 0 {
166 new_month = 12;
167 }
168
169 self.try_from_ym(udate.year() + yr_roll, new_month.try_into().unwrap())
171 .unwrap()
172 }
173
174 pub fn try_from_ym(&self, year: i32, month: u32) -> Result<NaiveDateTime, PyErr> {
184 match self {
185 RollDay::Day(value) => Ok(get_roll_by_day(year, month, *value)),
186 RollDay::IMM {} => Imm::Wed3.from_ym_opt(year, month),
187 }
188 }
189}
190
191pub(crate) fn get_unadjusteds(
196 date: &NaiveDateTime,
197 adjuster: &Adjuster,
198 calendar: &Calendar,
199) -> Vec<NaiveDateTime> {
200 let mut udates: Vec<NaiveDateTime> = vec![];
201
202 udates.push(*date);
204
205 let reversals: Vec<NaiveDateTime> = adjuster
207 .reverse(date, calendar)
208 .into_iter()
209 .filter(|v| v != date)
210 .collect();
211 udates.extend(reversals);
212 udates
213}
214
215fn get_roll_by_day(year: i32, month: u32, day: u32) -> NaiveDateTime {
217 let d = NaiveDate::from_ymd_opt(year, month, day);
218 match d {
219 Some(date) => NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
220 None => {
221 if day > 28 {
222 get_roll_by_day(year, month, day - 1)
223 } else {
224 panic!("Unexpected error in `get_roll_by_day`")
225 }
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::scheduling::{ndt, Cal};
234
235 fn fixture_bus_cal() -> Calendar {
236 Cal::try_from_name("bus").unwrap().into()
237 }
238
239 #[test]
240 fn test_rollday_equality() {
241 let rd1 = RollDay::IMM();
242 let rd2 = RollDay::IMM();
243 assert_eq!(rd1, rd2);
244
245 let rd1 = RollDay::IMM();
246 let rd2 = RollDay::Day(21);
247 assert_ne!(rd1, rd2);
248
249 let rd1 = RollDay::Day(20);
250 let rd2 = RollDay::Day(20);
251 assert_eq!(rd1, rd2);
252
253 let rd1 = RollDay::Day(21);
254 let rd2 = RollDay::Day(9);
255 assert_ne!(rd1, rd2);
256 }
257
258 #[test]
259 fn test_rollday_try_udate() {
260 let options: Vec<(RollDay, NaiveDateTime)> = vec![
261 (RollDay::Day(15), ndt(2000, 3, 15)),
262 (RollDay::Day(31), ndt(2000, 3, 31)),
263 (RollDay::Day(31), ndt(2022, 2, 28)),
264 (RollDay::Day(30), ndt(2024, 2, 29)),
265 (RollDay::Day(31), ndt(2024, 2, 29)),
266 ];
267 for option in options {
268 assert_eq!(false, option.0.try_udate(&option.1).is_err());
269 }
270 }
271
272 #[test]
273 fn test_get_unadjusteds() {
274 let options: Vec<(NaiveDateTime, Vec<NaiveDateTime>)> = vec![
275 (ndt(2000, 2, 29), vec![ndt(2000, 2, 29)]),
276 (
277 ndt(2025, 11, 28),
278 vec![ndt(2025, 11, 28), ndt(2025, 11, 29), ndt(2025, 11, 30)],
279 ),
280 (
281 ndt(2025, 2, 3),
282 vec![ndt(2025, 2, 3), ndt(2025, 2, 2), ndt(2025, 2, 1)],
283 ),
284 ];
285
286 for option in options {
287 let result = get_unadjusteds(
288 &option.0,
289 &Adjuster::ModifiedFollowing {},
290 &fixture_bus_cal(),
291 );
292
293 assert_eq!(result, option.1);
294 }
295 }
296
297 #[test]
298 fn test_vec_from() {
299 let options: Vec<(Vec<NaiveDateTime>, Vec<RollDay>)> = vec![
300 (
301 vec![ndt(2000, 2, 29)],
302 vec![RollDay::Day(29), RollDay::Day(30), RollDay::Day(31)],
303 ),
304 (vec![ndt(2025, 11, 28)], vec![RollDay::Day(28)]),
305 (
306 vec![ndt(2025, 3, 19)],
307 vec![RollDay::Day(19), RollDay::IMM {}],
308 ),
309 (vec![ndt(2025, 9, 15)], vec![RollDay::Day(15)]),
310 ];
311
312 for option in options {
313 let result = RollDay::vec_from(&option.0);
314 assert_eq!(result, option.1);
315 }
316 }
317
318 #[test]
319 fn test_vec_from_multiple() {
320 let options: Vec<(Vec<NaiveDateTime>, Vec<RollDay>)> = vec![
321 (
322 vec![ndt(2000, 2, 29)],
323 vec![RollDay::Day(29), RollDay::Day(30), RollDay::Day(31)],
324 ),
325 (
326 vec![ndt(2025, 11, 28), ndt(2025, 11, 29), ndt(2025, 11, 30)],
327 vec![
328 RollDay::Day(28),
329 RollDay::Day(29),
330 RollDay::Day(30),
331 RollDay::Day(31),
332 ],
333 ),
334 (
335 vec![ndt(2025, 3, 19)],
336 vec![RollDay::Day(19), RollDay::IMM()],
337 ),
338 (
339 vec![ndt(2025, 9, 15), ndt(2025, 9, 14), ndt(2025, 9, 13)],
340 vec![RollDay::Day(15), RollDay::Day(14), RollDay::Day(13)],
341 ),
342 ];
343
344 for option in options {
345 let result = RollDay::vec_from(&option.0);
346 assert_eq!(result, option.1);
347 }
348 }
349}