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