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 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 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 #[test]
127 fn hours_matches_morning_range_utc_winter() {
128 let hours = Hours::new(6, 22, Stockholm);
129 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 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 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 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 let timestamp = Utc.dt(2025, 1, 15, 13, 0, 0);
163 assert!(hours.matches(timestamp));
164 }
165
166 #[test]
168 fn hours_matches_morning_range_utc_summer() {
169 let hours = Hours::new(6, 22, Stockholm);
170 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 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 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 let timestamp = Utc.dt(2025, 6, 15, 21, 0, 0);
196 assert!(!hours.matches(timestamp));
197 }
198
199 #[test]
201 fn hours_matches_wraparound_late_evening_utc_winter() {
202 let hours = Hours::new(22, 6, Stockholm);
203 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 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 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 let timestamp = Utc.dt(2025, 1, 15, 6, 0, 0);
229 assert!(!hours.matches(timestamp));
230 }
231
232 #[test]
234 fn hours_matches_midnight_start_utc_winter() {
235 let hours = Hours::new(0, 5, Stockholm);
236 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 let timestamp = Utc.dt(2025, 1, 15, 2, 0, 0);
246 assert!(hours.matches(timestamp));
247 }
248}