1use chrono::{DateTime, Timelike};
2use serde::Serialize;
3
4use crate::{Language, Timezone};
5
6#[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 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 #[test]
133 fn hours_matches_morning_range_utc_winter() {
134 let hours = Hours::new(6, 22, Stockholm);
135 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 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 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 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 let timestamp = Utc.dt(2025, 1, 15, 13, 0, 0);
169 assert!(hours.matches(timestamp));
170 }
171
172 #[test]
174 fn hours_matches_morning_range_utc_summer() {
175 let hours = Hours::new(6, 22, Stockholm);
176 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 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 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 let timestamp = Utc.dt(2025, 6, 15, 21, 0, 0);
202 assert!(!hours.matches(timestamp));
203 }
204
205 #[test]
207 fn hours_matches_wraparound_late_evening_utc_winter() {
208 let hours = Hours::new(22, 6, Stockholm);
209 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 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 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 let timestamp = Utc.dt(2025, 1, 15, 6, 0, 0);
235 assert!(!hours.matches(timestamp));
236 }
237
238 #[test]
240 fn hours_matches_midnight_start_utc_winter() {
241 let hours = Hours::new(0, 5, Stockholm);
242 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 let timestamp = Utc.dt(2025, 1, 15, 2, 0, 0);
252 assert!(hours.matches(timestamp));
253 }
254}