grid_tariffs/
months.rs

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/// An inclusive range of months
61#[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 = &timestamp.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    // Tests using Utc
156    #[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        // December should match
250        assert!(months.matches(Utc.dt(2025, 12, 15, 12, 0, 0)));
251
252        // January should match
253        assert!(months.matches(Utc.dt(2025, 1, 15, 12, 0, 0)));
254
255        // February should match
256        assert!(months.matches(Utc.dt(2025, 2, 15, 12, 0, 0)));
257
258        // November should not match
259        assert!(!months.matches(Utc.dt(2025, 11, 15, 12, 0, 0)));
260
261        // March should not match
262        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        // Last second of March should not match
270        assert!(!months.matches(Utc.dt(2025, 3, 31, 23, 59, 59)));
271
272        // First second of April should match
273        assert!(months.matches(Utc.dt(2025, 4, 1, 0, 0, 0)));
274
275        // Last second of October should match
276        assert!(months.matches(Utc.dt(2025, 10, 31, 23, 59, 59)));
277
278        // First second of November should not match
279        assert!(!months.matches(Utc.dt(2025, 11, 1, 0, 0, 0)));
280    }
281}