1use chrono::{DateTime, Datelike};
2use serde::Serialize;
3
4use crate::{Language, Timezone};
5
6#[derive(Debug, Clone, Copy, Serialize)]
7#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
8#[repr(u8)]
9#[allow(dead_code)]
10pub enum Month {
11 January = 0,
12 February = 1,
13 March = 2,
14 April = 3,
15 May = 4,
16 June = 5,
17 July = 6,
18 August = 7,
19 September = 8,
20 October = 9,
21 November = 10,
22 December = 11,
23}
24
25impl Month {
26 pub(crate) const fn translate(&self, language: Language) -> &'static str {
27 match language {
28 Language::En => match self {
29 Self::January => "January",
30 Self::February => "February",
31 Self::March => "March",
32 Self::April => "April",
33 Self::May => "May",
34 Self::June => "June",
35 Self::July => "July",
36 Self::August => "August",
37 Self::September => "September",
38 Self::October => "October",
39 Self::November => "November",
40 Self::December => "December",
41 },
42 Language::Sv => match self {
43 Self::January => "januari",
44 Self::February => "februari",
45 Self::March => "mars",
46 Self::April => "april",
47 Self::May => "maj",
48 Self::June => "juni",
49 Self::July => "juli",
50 Self::August => "augusti",
51 Self::September => "september",
52 Self::October => "oktober",
53 Self::November => "november",
54 Self::December => "december",
55 },
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, Serialize)]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63pub struct Months(Month, Month, Timezone);
64
65impl Months {
66 pub const fn new(from: Month, to: Month, timezone: Timezone) -> Self {
67 Self(from, to, timezone)
68 }
69
70 pub(crate) fn translate(&self, language: Language) -> String {
71 format!(
72 "{} - {}",
73 self.0.translate(language),
74 self.1.translate(language)
75 )
76 }
77
78 pub(crate) fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
79 let start_month_index = self.0 as u32;
80 let end_month_index = self.1 as u32;
81 let month_index = ×tamp.month0();
82
83 if start_month_index <= end_month_index {
84 (start_month_index..=end_month_index).contains(month_index)
85 } else {
86 (start_month_index..=11).contains(month_index)
87 || (0..=end_month_index).contains(month_index)
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 use crate::Language;
97 use crate::{Stockholm, Utc};
98
99 #[test]
100 fn months_matches_winter_period_start() {
101 let months = Months::new(Month::November, Month::March, Stockholm);
102 let timestamp = Stockholm.dt(2025, 11, 1, 0, 0, 0);
103 assert!(months.matches(timestamp));
104 }
105
106 #[test]
107 fn months_matches_winter_period_end() {
108 let months = Months::new(Month::November, Month::March, Stockholm);
109 let timestamp = Stockholm.dt(2025, 3, 31, 23, 59, 59);
110 assert!(months.matches(timestamp));
111 }
112
113 #[test]
114 fn months_matches_winter_period_middle() {
115 let months = Months::new(Month::November, Month::March, Stockholm);
116 let timestamp = Stockholm.dt(2025, 1, 15, 12, 0, 0);
117 assert!(months.matches(timestamp));
118 }
119
120 #[test]
121 fn months_does_not_match_before_range() {
122 let months = Months::new(Month::November, Month::March, Stockholm);
123 let timestamp = Stockholm.dt(2025, 10, 31, 23, 59, 59);
124 assert!(!months.matches(timestamp));
125 }
126
127 #[test]
128 fn months_does_not_match_after_range() {
129 let months = Months::new(Month::November, Month::March, Stockholm);
130 let timestamp = Stockholm.dt(2025, 4, 1, 0, 0, 0);
131 assert!(!months.matches(timestamp));
132 }
133
134 #[test]
135 fn months_matches_summer_period() {
136 let months = Months::new(Month::May, Month::September, Stockholm);
137 let timestamp = Stockholm.dt(2025, 7, 15, 12, 0, 0);
138 assert!(months.matches(timestamp));
139 }
140
141 #[test]
142 fn months_matches_single_month_range() {
143 let months = Months::new(Month::June, Month::June, Stockholm);
144 let timestamp = Stockholm.dt(2025, 6, 15, 12, 0, 0);
145 assert!(months.matches(timestamp));
146 }
147
148 #[test]
149 fn months_matches_full_year() {
150 let months = Months::new(Month::January, Month::December, Stockholm);
151 let timestamp = Stockholm.dt(2025, 8, 15, 12, 0, 0);
152 assert!(months.matches(timestamp));
153 }
154
155 #[test]
157 fn month_translate_english() {
158 assert_eq!(Month::January.translate(Language::En), "January");
159 assert_eq!(Month::June.translate(Language::En), "June");
160 assert_eq!(Month::December.translate(Language::En), "December");
161 }
162
163 #[test]
164 fn month_translate_swedish() {
165 assert_eq!(Month::January.translate(Language::Sv), "januari");
166 assert_eq!(Month::June.translate(Language::Sv), "juni");
167 assert_eq!(Month::December.translate(Language::Sv), "december");
168 }
169
170 #[test]
171 fn months_translate_range_english() {
172 let months = Months::new(Month::November, Month::March, Utc);
173 assert_eq!(months.translate(Language::En), "November - March");
174 }
175
176 #[test]
177 fn months_translate_range_swedish() {
178 let months = Months::new(Month::November, Month::March, Utc);
179 assert_eq!(months.translate(Language::Sv), "november - mars");
180 }
181
182 #[test]
183 fn months_matches_winter_period_start_utc() {
184 let months = Months::new(Month::November, Month::March, Stockholm);
185 let timestamp = Utc.dt(2025, 11, 1, 0, 0, 0);
186 assert!(months.matches(timestamp));
187 }
188
189 #[test]
190 fn months_matches_winter_period_end_utc() {
191 let months = Months::new(Month::November, Month::March, Stockholm);
192 let timestamp = Utc.dt(2025, 3, 31, 23, 59, 59);
193 assert!(months.matches(timestamp));
194 }
195
196 #[test]
197 fn months_matches_winter_period_middle_utc() {
198 let months = Months::new(Month::November, Month::March, Stockholm);
199 let timestamp = Utc.dt(2025, 1, 15, 12, 0, 0);
200 assert!(months.matches(timestamp));
201 }
202
203 #[test]
204 fn months_does_not_match_before_range_utc() {
205 let months = Months::new(Month::November, Month::March, Stockholm);
206 let timestamp = Utc.dt(2025, 10, 31, 23, 59, 59);
207 assert!(!months.matches(timestamp));
208 }
209
210 #[test]
211 fn months_does_not_match_after_range_utc() {
212 let months = Months::new(Month::November, Month::March, Stockholm);
213 let timestamp = Utc.dt(2025, 4, 1, 0, 0, 0);
214 assert!(!months.matches(timestamp));
215 }
216
217 #[test]
218 fn months_matches_summer_period_utc() {
219 let months = Months::new(Month::May, Month::September, Stockholm);
220 let timestamp = Utc.dt(2025, 7, 15, 12, 0, 0);
221 assert!(months.matches(timestamp));
222 }
223
224 #[test]
225 fn months_matches_single_month_range_utc() {
226 let months = Months::new(Month::June, Month::June, Stockholm);
227 let timestamp = Utc.dt(2025, 6, 15, 12, 0, 0);
228 assert!(months.matches(timestamp));
229 }
230
231 #[test]
232 fn months_does_not_match_outside_single_month_utc() {
233 let months = Months::new(Month::June, Month::June, Stockholm);
234 let timestamp = Utc.dt(2025, 7, 1, 0, 0, 0);
235 assert!(!months.matches(timestamp));
236 }
237
238 #[test]
239 fn months_matches_full_year_utc() {
240 let months = Months::new(Month::January, Month::December, Stockholm);
241 let timestamp = Utc.dt(2025, 8, 15, 12, 0, 0);
242 assert!(months.matches(timestamp));
243 }
244
245 #[test]
246 fn months_matches_year_wrap_december_to_february_utc() {
247 let months = Months::new(Month::December, Month::February, Stockholm);
248
249 assert!(months.matches(Utc.dt(2025, 12, 15, 12, 0, 0)));
251
252 assert!(months.matches(Utc.dt(2025, 1, 15, 12, 0, 0)));
254
255 assert!(months.matches(Utc.dt(2025, 2, 15, 12, 0, 0)));
257
258 assert!(!months.matches(Utc.dt(2025, 11, 15, 12, 0, 0)));
260
261 assert!(!months.matches(Utc.dt(2025, 3, 15, 12, 0, 0)));
263 }
264
265 #[test]
266 fn months_boundary_at_midnight_utc() {
267 let months = Months::new(Month::April, Month::October, Stockholm);
268
269 assert!(!months.matches(Utc.dt(2025, 3, 31, 23, 59, 59)));
271
272 assert!(months.matches(Utc.dt(2025, 4, 1, 0, 0, 0)));
274
275 assert!(months.matches(Utc.dt(2025, 10, 31, 23, 59, 59)));
277
278 assert!(!months.matches(Utc.dt(2025, 11, 1, 0, 0, 0)));
280 }
281}