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