1#![allow(non_camel_case_types)]
2
3use chrono::prelude::*;
4use chrono::Months;
5use pyo3::exceptions::PyValueError;
6use pyo3::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::cmp::{Eq, PartialEq};
9
10use crate::scheduling::ndt;
11
12#[pyclass(module = "rateslib.rs", eq)]
14#[derive(Debug, Copy, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum Imm {
16 Wed3_HMUZ = 0,
20 Wed3 = 1,
24 Day20_HMUZ = 2,
28 Day20_HU = 3,
32 Day20_MZ = 4,
36 Day20 = 5,
38 Fri2_HMUZ = 6,
42 Fri2 = 7,
46 Wed1_Post9_HMUZ = 10,
50 Wed1_Post9 = 11,
54 Eom = 8,
56 Leap = 9,
58}
59
60impl Imm {
61 pub fn validate(&self, date: &NaiveDateTime) -> bool {
63 let result = self.from_ym_opt(date.year(), date.month());
64 match result {
65 Ok(val) => *date == val,
66 Err(_) => false,
67 }
68 }
69
70 pub fn from_ym_opt(&self, year: i32, month: u32) -> Result<NaiveDateTime, PyErr> {
72 match self {
73 Imm::Wed3_HMUZ => {
74 if month == 3 || month == 6 || month == 9 || month == 12 {
75 Imm::Wed3.from_ym_opt(year, month)
76 } else {
77 Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
78 }
79 }
80 Imm::Fri2_HMUZ => {
81 if month == 3 || month == 6 || month == 9 || month == 12 {
82 Imm::Fri2.from_ym_opt(year, month)
83 } else {
84 Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
85 }
86 }
87 Imm::Wed1_Post9_HMUZ => {
88 if month == 3 || month == 6 || month == 9 || month == 12 {
89 Imm::Wed1_Post9.from_ym_opt(year, month)
90 } else {
91 Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
92 }
93 }
94 Imm::Wed3 => {
95 let w = ndt(year, month, 1).weekday() as u32;
96 let r = if w <= 2 { 17 - w } else { 24 - w };
97 Ok(ndt(year, month, r))
98 }
99 Imm::Fri2 => {
100 let w = ndt(year, month, 1).weekday() as u32;
101 let r = if w <= 4 { 12 - w } else { 19 - w };
102 Ok(ndt(year, month, r))
103 }
104 Imm::Wed1_Post9 => {
105 let w = ndt(year, month, 1).weekday() as u32;
106 let r = if w <= 0 { 10 - w } else { 17 - w };
107 Ok(ndt(year, month, r))
108 }
109 Imm::Day20_HMUZ => {
110 if month == 3 || month == 6 || month == 9 || month == 12 {
111 Ok(ndt(year, month, 20))
112 } else {
113 Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
114 }
115 }
116 Imm::Day20_HU => {
117 if month == 3 || month == 9 {
118 Ok(ndt(year, month, 20))
119 } else {
120 Err(PyValueError::new_err("Must be month Mar, or Sep."))
121 }
122 }
123 Imm::Day20_MZ => {
124 if month == 6 || month == 12 {
125 Ok(ndt(year, month, 20))
126 } else {
127 Err(PyValueError::new_err("Must be month Jun, or Dec."))
128 }
129 }
130 Imm::Day20 => Ok(ndt(year, month, 20)),
131 Imm::Eom => {
132 let mut day = 31;
133 let mut date = NaiveDate::from_ymd_opt(year, month, day);
134 while date == None {
135 day = day - 1;
136 date = NaiveDate::from_ymd_opt(year, month, day);
137 if day == 0 {
138 return Err(PyValueError::new_err("`year` or `month` out of range."));
139 }
140 }
141 Ok(date.unwrap().and_hms_opt(0, 0, 0).unwrap())
142 }
143 Imm::Leap => {
144 if month != 2 {
145 Err(PyValueError::new_err("Leap is only in `month`:2."))
146 } else {
147 let d = NaiveDate::from_ymd_opt(year, 2, 29);
148 match d {
149 None => Err(PyValueError::new_err("No Leap in given `year`.")),
150 Some(val) => Ok(val.and_hms_opt(0, 0, 0).unwrap()),
151 }
152 }
153 }
154 }
155 }
156
157 pub fn next(&self, date: &NaiveDateTime) -> NaiveDateTime {
159 let mut sample = *date;
160 let mut result = self.from_ym_opt(date.year(), date.month());
161 loop {
162 match result {
163 Ok(v) if v > *date => return v,
164 _ => {
165 sample = sample + Months::new(1);
166 result = self.from_ym_opt(sample.year(), sample.month());
167 }
168 }
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn imm_date_determination() {
179 let options: Vec<(Imm, NaiveDateTime, bool)> = vec![
180 (Imm::Wed3_HMUZ, ndt(2000, 3, 15), true),
181 (Imm::Wed3_HMUZ, ndt(2000, 3, 22), false),
182 (Imm::Wed3_HMUZ, ndt(2000, 3, 8), false),
183 (Imm::Wed3_HMUZ, ndt(2000, 2, 21), false),
184 (Imm::Wed3, ndt(2024, 2, 21), true),
185 (Imm::Wed3, ndt(2000, 3, 15), true),
186 (Imm::Wed3, ndt(2025, 3, 19), true),
187 (Imm::Wed3, ndt(2025, 3, 18), false),
188 (Imm::Day20_HMUZ, ndt(2000, 2, 21), false),
189 (Imm::Day20_HMUZ, ndt(2000, 2, 20), false),
190 (Imm::Day20_HMUZ, ndt(2000, 3, 20), true),
191 (Imm::Day20_HU, ndt(2000, 3, 20), true),
192 (Imm::Day20_HU, ndt(2000, 6, 20), false),
193 (Imm::Day20_MZ, ndt(2000, 3, 20), false),
194 (Imm::Day20_MZ, ndt(2000, 6, 20), true),
195 (Imm::Fri2, ndt(2024, 2, 9), true),
196 (Imm::Fri2, ndt(2024, 12, 13), true),
197 (Imm::Wed1_Post9, ndt(2025, 9, 10), true),
198 (Imm::Wed1_Post9, ndt(2026, 9, 16), true),
199 ];
200 for option in options {
201 assert_eq!(option.2, option.0.validate(&option.1));
202 }
203 }
204
205 #[test]
206 fn next_check() {
207 let options: Vec<(Imm, NaiveDateTime, NaiveDateTime)> = vec![
208 (Imm::Wed3_HMUZ, ndt(2024, 3, 20), ndt(2024, 6, 19)),
209 (Imm::Wed3_HMUZ, ndt(2024, 3, 19), ndt(2024, 3, 20)),
210 (Imm::Wed3, ndt(2024, 3, 21), ndt(2024, 4, 17)),
211 (Imm::Day20_HU, ndt(2024, 3, 21), ndt(2024, 9, 20)),
212 (Imm::Leap, ndt(2022, 1, 1), ndt(2024, 2, 29)),
213 ];
214 for option in options {
215 assert_eq!(option.2, option.0.next(&option.1));
216 }
217 }
218
219 #[test]
220 fn test_is_eom() {
221 assert_eq!(true, Imm::Eom.validate(&ndt(2025, 3, 31)));
222 assert_eq!(false, Imm::Eom.validate(&ndt(2025, 3, 30)));
223 }
224
225 #[test]
226 fn test_get_from() {
227 assert_eq!(ndt(2022, 2, 28), Imm::Eom.from_ym_opt(2022, 2).unwrap());
228 assert_eq!(ndt(2024, 2, 29), Imm::Eom.from_ym_opt(2024, 2).unwrap());
229 assert_eq!(ndt(2022, 4, 30), Imm::Eom.from_ym_opt(2022, 4).unwrap());
230 assert_eq!(ndt(2022, 3, 31), Imm::Eom.from_ym_opt(2022, 3).unwrap());
231 assert_eq!(ndt(2024, 2, 29), Imm::Leap.from_ym_opt(2024, 2).unwrap());
232 assert!(Imm::Leap.from_ym_opt(2022, 2).is_err());
233 }
234}