grid_tariffs/
hours.rs

1use chrono::{DateTime, Timelike};
2use serde::Serialize;
3
4use crate::{Language, Timezone};
5
6// A definition of hours (inclusive)
7#[derive(Debug, Clone, Copy, Serialize)]
8#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9pub struct Hours(u8, u8, Timezone);
10
11impl Hours {
12    pub const fn new(from: u8, to_inclusive: u8, timezone: Timezone) -> Self {
13        if from > 23 {
14            panic!("Hours `from` arg too high");
15        }
16        if to_inclusive > 23 {
17            panic!("Hours `to_inclusive` arg too high");
18        }
19        Self(from, to_inclusive, timezone)
20    }
21
22    pub(crate) const fn from(&self) -> u8 {
23        self.0
24    }
25
26    #[allow(clippy::wrong_self_convention)]
27    pub(crate) const fn to_inclusive(&self) -> u8 {
28        self.1
29    }
30
31    pub(crate) fn translate(&self, language: Language) -> String {
32        match language {
33            // TODO: am/pm?
34            Language::En => format!("{}-{}", self.from(), self.to_inclusive()),
35            Language::Sv => format!("Kl {}-{}", self.from(), self.to_inclusive()),
36        }
37    }
38
39    const fn start(&self) -> u8 {
40        self.0
41    }
42
43    const fn end(&self) -> u8 {
44        self.1
45    }
46
47    const fn tz(&self) -> chrono_tz::Tz {
48        self.2.to_tz()
49    }
50
51    pub(crate) fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
52        let timestamp = timestamp.with_timezone(&self.tz());
53        let hour = timestamp.hour() as u8;
54        if self.start() <= self.end() {
55            (self.start()..=self.end()).contains(&hour)
56        } else {
57            (self.start()..=23).contains(&hour) || (0..=self.end()).contains(&hour)
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    use crate::{Stockholm, Utc};
67
68    #[test]
69    fn hours_matches_exact_start() {
70        let hours = Hours::new(6, 22, Stockholm);
71        let timestamp = Stockholm.dt(2025, 1, 15, 6, 0, 0);
72        assert!(hours.matches(timestamp));
73    }
74
75    #[test]
76    fn hours_matches_exact_end() {
77        let hours = Hours::new(6, 22, Stockholm);
78        let timestamp = Stockholm.dt(2025, 1, 15, 22, 59, 59);
79        assert!(hours.matches(timestamp));
80    }
81
82    #[test]
83    fn hours_matches_middle() {
84        let hours = Hours::new(6, 22, Stockholm);
85        let timestamp = Stockholm.dt(2025, 1, 15, 14, 30, 0);
86        assert!(hours.matches(timestamp));
87    }
88
89    #[test]
90    fn hours_does_not_match_before() {
91        let hours = Hours::new(6, 22, Stockholm);
92        let timestamp = Stockholm.dt(2025, 1, 15, 5, 59, 59);
93        assert!(!hours.matches(timestamp));
94    }
95
96    #[test]
97    fn hours_does_not_match_after() {
98        let hours = Hours::new(6, 22, Stockholm);
99        let timestamp = Stockholm.dt(2025, 1, 15, 23, 0, 0);
100        assert!(!hours.matches(timestamp));
101    }
102
103    #[test]
104    fn hours_matches_midnight_range() {
105        let hours = Hours::new(0, 5, Stockholm);
106        let timestamp = Stockholm.dt(2025, 1, 15, 3, 0, 0);
107        assert!(hours.matches(timestamp));
108    }
109
110    #[test]
111    fn hours_matches_all_day() {
112        let hours = Hours::new(0, 23, Stockholm);
113        let timestamp = Stockholm.dt(2025, 1, 15, 12, 0, 0);
114        assert!(hours.matches(timestamp));
115    }
116
117    #[test]
118    fn hours_matches_midnight_range_utc_near_end() {
119        let hours = Hours::new(0, 5, Stockholm);
120        let timestamp = Utc.dt(2025, 1, 15, 4, 59, 59);
121        assert!(hours.matches(timestamp));
122    }
123
124    #[test]
125    fn hours_matches_midnight_range_utc_at_end() {
126        let hours = Hours::new(0, 5, Stockholm);
127        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
128        assert!(!hours.matches(timestamp))
129    }
130
131    // Stockholm to UTC tests - Winter time (UTC+1)
132    #[test]
133    fn hours_matches_morning_range_utc_winter() {
134        let hours = Hours::new(6, 22, Stockholm);
135        // 06:00 Stockholm = 05:00 UTC in winter
136        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
137        assert!(hours.matches(timestamp));
138    }
139
140    #[test]
141    fn hours_does_not_match_before_start_utc_winter() {
142        let hours = Hours::new(6, 22, Stockholm);
143        // 05:59 Stockholm = 04:59 UTC in winter
144        let timestamp = Utc.dt(2025, 1, 15, 4, 59, 59);
145        assert!(!hours.matches(timestamp));
146    }
147
148    #[test]
149    fn hours_matches_at_end_utc_winter() {
150        let hours = Hours::new(6, 22, Stockholm);
151        // 22:59 Stockholm = 21:59 UTC in winter
152        let timestamp = Utc.dt(2025, 1, 15, 21, 59, 59);
153        assert!(hours.matches(timestamp));
154    }
155
156    #[test]
157    fn hours_does_not_match_after_end_utc_winter() {
158        let hours = Hours::new(6, 22, Stockholm);
159        // 23:00 Stockholm = 22:00 UTC in winter
160        let timestamp = Utc.dt(2025, 1, 15, 22, 0, 0);
161        assert!(!hours.matches(timestamp));
162    }
163
164    #[test]
165    fn hours_matches_middle_utc_winter() {
166        let hours = Hours::new(6, 22, Stockholm);
167        // 14:00 Stockholm = 13:00 UTC in winter
168        let timestamp = Utc.dt(2025, 1, 15, 13, 0, 0);
169        assert!(hours.matches(timestamp));
170    }
171
172    // Stockholm to UTC tests - Summer time (UTC+2)
173    #[test]
174    fn hours_matches_morning_range_utc_summer() {
175        let hours = Hours::new(6, 22, Stockholm);
176        // 06:00 Stockholm = 04:00 UTC in summer
177        let timestamp = Utc.dt(2025, 6, 15, 4, 0, 0);
178        assert!(hours.matches(timestamp));
179    }
180
181    #[test]
182    fn hours_does_not_match_before_start_utc_summer() {
183        let hours = Hours::new(6, 22, Stockholm);
184        // 05:59 Stockholm = 03:59 UTC in summer
185        let timestamp = Utc.dt(2025, 6, 15, 3, 59, 59);
186        assert!(!hours.matches(timestamp));
187    }
188
189    #[test]
190    fn hours_matches_at_end_utc_summer() {
191        let hours = Hours::new(6, 22, Stockholm);
192        // 22:59 Stockholm = 20:59 UTC in summer
193        let timestamp = Utc.dt(2025, 6, 15, 20, 59, 59);
194        assert!(hours.matches(timestamp));
195    }
196
197    #[test]
198    fn hours_does_not_match_after_end_utc_summer() {
199        let hours = Hours::new(6, 22, Stockholm);
200        // 23:00 Stockholm = 21:00 UTC in summer
201        let timestamp = Utc.dt(2025, 6, 15, 21, 0, 0);
202        assert!(!hours.matches(timestamp));
203    }
204
205    // Midnight wrap-around tests with UTC
206    #[test]
207    fn hours_matches_wraparound_late_evening_utc_winter() {
208        let hours = Hours::new(22, 6, Stockholm);
209        // 22:00 Stockholm = 21:00 UTC in winter
210        let timestamp = Utc.dt(2025, 1, 15, 21, 0, 0);
211        assert!(hours.matches(timestamp));
212    }
213
214    #[test]
215    fn hours_matches_wraparound_midnight_utc_winter() {
216        let hours = Hours::new(22, 6, Stockholm);
217        // 00:00 Stockholm = 23:00 UTC (previous day) in winter
218        let timestamp = Utc.dt(2025, 1, 14, 23, 0, 0);
219        assert!(hours.matches(timestamp));
220    }
221
222    #[test]
223    fn hours_matches_wraparound_early_morning_utc_winter() {
224        let hours = Hours::new(22, 6, Stockholm);
225        // 06:00 Stockholm = 05:00 UTC in winter
226        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
227        assert!(hours.matches(timestamp));
228    }
229
230    #[test]
231    fn hours_does_not_match_wraparound_outside_range_utc_winter() {
232        let hours = Hours::new(22, 6, Stockholm);
233        // 07:00 Stockholm = 06:00 UTC in winter
234        let timestamp = Utc.dt(2025, 1, 15, 6, 0, 0);
235        assert!(!hours.matches(timestamp));
236    }
237
238    // Edge case: midnight start with UTC
239    #[test]
240    fn hours_matches_midnight_start_utc_winter() {
241        let hours = Hours::new(0, 5, Stockholm);
242        // 00:00 Stockholm = 23:00 UTC (previous day) in winter
243        let timestamp = Utc.dt(2025, 1, 14, 23, 0, 0);
244        assert!(hours.matches(timestamp));
245    }
246
247    #[test]
248    fn hours_matches_midnight_start_early_hour_utc_winter() {
249        let hours = Hours::new(0, 5, Stockholm);
250        // 03:00 Stockholm = 02:00 UTC in winter
251        let timestamp = Utc.dt(2025, 1, 15, 2, 0, 0);
252        assert!(hours.matches(timestamp));
253    }
254}