1use chrono::{Datelike, NaiveDate, Weekday};
2use std::str::FromStr;
3use strum::IntoEnumIterator;
4use strum_macros::{Display, EnumIter, EnumString};
5
6#[repr(u16)]
9#[derive(EnumIter, EnumString, Display, PartialEq, Debug)]
10pub enum IMMMonth {
11 F = 1,
12 G = 2,
13 H = 3,
14 J = 4,
15 K = 5,
16 M = 6,
17 N = 7,
18 Q = 8,
19 U = 9,
20 V = 10,
21 X = 11,
22 Z = 12,
23}
24
25pub struct IMM;
27
28impl IMM {
29 pub fn is_imm_date(&self, date: NaiveDate, main_cycle: bool) -> bool {
35 if date.weekday() != Weekday::Wed {
36 return false;
37 }
38
39 let d = date.day();
40 if !(15..=21).contains(&d) {
41 return false;
42 }
43
44 if !main_cycle {
45 return true;
46 }
47
48 matches!(date.month(), 3 | 6 | 9 | 12)
49 }
50
51 pub fn is_imm_code(&self, imm_code: String, main_cycle: bool) -> bool {
53 if imm_code.len() != 2 {
54 return false;
55 }
56
57 let imm_year = imm_code
58 .chars()
59 .nth(1)
60 .expect("already asserted length of 2");
61
62 if !"0123456789".contains(imm_year) {
63 return false;
64 }
65
66 let str = if main_cycle {
67 "hmzuHMZU".to_string()
68 } else {
69 "fghjkmnquvxzFGHJKMNQUVXZ".to_string()
70 };
71
72 let imm_month = imm_code
73 .chars()
74 .nth(0)
75 .expect("already asserted length of 2");
76
77 if !str.contains(imm_month) {
78 return false;
79 }
80
81 true
82 }
83
84 pub fn code(&self, date: NaiveDate) -> Option<String> {
86 if !self.is_imm_date(date, false) {
87 None
88 } else {
89 let y = date.year() % 10;
90 let mut month = IMMMonth::iter()
91 .nth((date.month() - 1) as usize)
92 .expect("month is within range")
93 .to_string();
94 month.push_str(y.to_string().as_str());
95 Some(month)
96 }
97 }
98
99 pub fn date(&self, imm_code: String, ref_date: Option<NaiveDate>) -> Option<NaiveDate> {
101 if !self.is_imm_code(imm_code.clone(), false) {
102 None
103 } else {
104 let ref_date = ref_date.unwrap_or(chrono::offset::Utc::now().date_naive());
105 let month = imm_code.chars().nth(0).unwrap();
106 let mut year = imm_code.chars().nth(1).unwrap().to_digit(10).unwrap() as i32;
107 let imm_month = IMMMonth::from_str(&month.to_string()).unwrap() as u32;
108 if year == 0 && ref_date.year() <= 1909 {
109 year += 10
110 }
111 let ref_year = ref_date.year() % 10;
112 year += ref_date.year() - ref_year;
113 let result =
114 self.next_date(NaiveDate::from_ymd_opt(year, imm_month, 1).unwrap(), false);
115 if result < ref_date {
116 Some(self.next_date(
117 NaiveDate::from_ymd_opt(year + 10, imm_month, 1).unwrap(),
118 false,
119 ))
120 } else {
121 Some(result)
122 }
123 }
124 }
125
126 pub fn next_date(&self, date: NaiveDate, main_cycle: bool) -> NaiveDate {
128 let mut month = date.month();
129 let mut year = date.year();
130 let offset = if main_cycle { 3 } else { 1 };
131 let mut skip_months = offset - (date.month() % offset);
132 if skip_months != offset || date.day() > 21 {
133 skip_months += date.month();
134 if skip_months > 12 {
135 month -= 12;
136 year += 1;
137 }
138 }
139 let mut result = self.nth_weekday(3, Weekday::Wed, month, year).unwrap();
140 if result <= date {
141 result = self.next_date(
142 NaiveDate::from_ymd_opt(year, month, 22).unwrap(),
143 main_cycle,
144 );
145 }
146 result
147 }
148
149 fn nth_weekday(&self, nth: i32, day_of_week: Weekday, m: u32, y: i32) -> Option<NaiveDate> {
150 if !(0..=6).contains(&nth) {
151 None
152 } else {
153 let first = NaiveDate::from_ymd_opt(y, m, 1).unwrap().weekday();
154 let skip = nth
155 - (if day_of_week.num_days_from_monday() >= first.num_days_from_monday() {
156 1
157 } else {
158 0
159 });
160 NaiveDate::from_ymd_opt(
161 y,
162 m,
163 1 + day_of_week.num_days_from_monday() + skip as u32 * 7
164 - first.num_days_from_monday(),
165 )
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::{IMM, IMMMonth};
173 use chrono::NaiveDate;
174 use std::str::FromStr;
175 use strum::IntoEnumIterator;
176
177 #[test]
178 fn test_imm_month() {
179 assert_eq!(IMMMonth::iter().nth(5).unwrap().to_string(), "M");
180 assert_eq!(IMMMonth::from_str("F").unwrap() as u16, 1);
181 }
182 #[test]
183 fn test_imm_code() {
184 assert_eq!(IMM.is_imm_code("more_than_2".to_string(), false), false);
185 assert_eq!(IMM.is_imm_code("1".to_string(), false), false);
186 assert_eq!(IMM.is_imm_code("".to_string(), false), false);
187 assert_eq!(IMM.is_imm_code("1F".to_string(), false), false);
188 assert_eq!(IMM.is_imm_code("F1".to_string(), true), false);
189 assert_eq!(IMM.is_imm_code("F1".to_string(), false), true);
190 }
191
192 #[test]
193 fn test_generate_code() {
194 assert_eq!(
195 IMM.code(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()),
196 Some(String::from("U3".to_string()))
197 );
198 }
199
200 #[test]
201 fn test_imm_code_to_date() {
202 assert_eq!(
203 IMM.date("X3".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
204 NaiveDate::from_ymd_opt(2023, 11, 15)
205 );
206 assert_eq!(
207 IMM.date("Z3".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
208 NaiveDate::from_ymd_opt(2023, 12, 20)
209 );
210 assert_eq!(
211 IMM.date("F4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
212 NaiveDate::from_ymd_opt(2024, 1, 17)
213 );
214 assert_eq!(
215 IMM.date("G4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
216 NaiveDate::from_ymd_opt(2024, 2, 21)
217 );
218 assert_eq!(
219 IMM.date("H4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
220 NaiveDate::from_ymd_opt(2024, 3, 20)
221 );
222 assert_eq!(
223 IMM.date("J4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
224 NaiveDate::from_ymd_opt(2024, 4, 17)
225 );
226 assert_eq!(
227 IMM.date("M4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
228 NaiveDate::from_ymd_opt(2024, 6, 19)
229 );
230 assert_eq!(
231 IMM.date("U4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
232 NaiveDate::from_ymd_opt(2024, 9, 18)
233 );
234 assert_eq!(
235 IMM.date("Z4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
236 NaiveDate::from_ymd_opt(2024, 12, 18)
237 );
238 assert_eq!(
239 IMM.date("H5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
240 NaiveDate::from_ymd_opt(2025, 3, 19)
241 );
242 assert_eq!(
243 IMM.date("M5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
244 NaiveDate::from_ymd_opt(2025, 6, 18)
245 );
246 assert_eq!(
247 IMM.date("U5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
248 NaiveDate::from_ymd_opt(2025, 9, 17)
249 );
250 }
251}