1use std::fmt::Display;
4
5use humantime::format_rfc3339_seconds;
6use tor_units::IntegerMinutes;
7use web_time_compat::{Duration, SystemTime};
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Deserialize, Serialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
22pub struct TimePeriod {
23 pub(crate) interval_num: u64,
25 pub(crate) length: IntegerMinutes<u32>,
29 pub(crate) epoch_offset_in_sec: u32,
34}
35
36impl PartialOrd for TimePeriod {
39 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
40 if self.length == other.length && self.epoch_offset_in_sec == other.epoch_offset_in_sec {
41 Some(self.interval_num.cmp(&other.interval_num))
42 } else {
43 None
44 }
45 }
46}
47
48impl Display for TimePeriod {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "#{} ", self.interval_num())?;
51 match self.range() {
52 Ok(r) => {
53 let mins = self.length().as_minutes();
54 write!(
55 f,
56 "{}..+{}:{:02}",
57 format_rfc3339_seconds(r.start),
58 mins / 60,
59 mins % 60
60 )
61 }
62 Err(_) => write!(f, "overflow! {self:?}"),
63 }
64 }
65}
66
67impl TimePeriod {
68 pub fn new(
79 length: Duration,
80 when: SystemTime,
81 epoch_offset: Duration,
82 ) -> Result<Self, TimePeriodError> {
83 let length_in_sec =
85 u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?;
86 if length_in_sec % 60 != 0 || length.subsec_nanos() != 0 {
87 return Err(TimePeriodError::IntervalInvalid);
88 }
89 let length_in_minutes = length_in_sec / 60;
90 let length = IntegerMinutes::new(length_in_minutes);
91 let epoch_offset_in_sec =
92 u32::try_from(epoch_offset.as_secs()).map_err(|_| TimePeriodError::OffsetInvalid)?;
93 let interval_num = when
94 .duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
95 .map_err(|_| TimePeriodError::OutOfRange)?
96 .as_secs()
97 / u64::from(length_in_sec);
98 Ok(TimePeriod {
99 interval_num,
100 length,
101 epoch_offset_in_sec,
102 })
103 }
104
105 pub fn from_parts(length: u32, interval_num: u64, epoch_offset_in_sec: u32) -> Self {
115 let length_in_sec = length * 60;
116
117 Self {
118 interval_num,
119 length: length.into(),
120 epoch_offset_in_sec,
121 }
122 }
123
124 pub fn next(&self) -> Option<Self> {
128 Some(TimePeriod {
129 interval_num: self.interval_num.checked_add(1)?,
130 ..*self
131 })
132 }
133 pub fn prev(&self) -> Option<Self> {
137 Some(TimePeriod {
138 interval_num: self.interval_num.checked_sub(1)?,
139 ..*self
140 })
141 }
142 pub fn contains(&self, when: SystemTime) -> bool {
149 match self.range() {
150 Ok(r) => r.contains(&when),
151 Err(_) => false,
152 }
153 }
154 pub fn range(&self) -> Result<std::ops::Range<SystemTime>, TimePeriodError> {
160 (|| {
161 let length_in_sec = u64::from(self.length.as_minutes()) * 60;
162 let start_sec = length_in_sec.checked_mul(self.interval_num)?;
163 let end_sec = start_sec.checked_add(length_in_sec)?;
164 let epoch_offset = Duration::new(self.epoch_offset_in_sec.into(), 0);
165 let start = (SystemTime::UNIX_EPOCH + epoch_offset)
166 .checked_add(Duration::from_secs(start_sec))?;
167 let end = (SystemTime::UNIX_EPOCH + epoch_offset)
168 .checked_add(Duration::from_secs(end_sec))?;
169 Some(start..end)
170 })()
171 .ok_or(TimePeriodError::OutOfRange)
172 }
173
174 pub fn interval_num(&self) -> u64 {
179 self.interval_num
180 }
181
182 pub fn length(&self) -> IntegerMinutes<u32> {
187 self.length
188 }
189
190 pub fn epoch_offset_in_sec(&self) -> u32 {
195 self.epoch_offset_in_sec
196 }
197}
198
199#[derive(Clone, Debug, thiserror::Error)]
201#[non_exhaustive]
202pub enum TimePeriodError {
203 #[error("Time period out was out of range")]
206 OutOfRange,
207
208 #[error("Invalid time period interval")]
214 IntervalInvalid,
215
216 #[error("Invalid time period offset")]
220 OffsetInvalid,
221}
222
223#[cfg(test)]
224mod test {
225 #![allow(clippy::bool_assert_comparison)]
227 #![allow(clippy::clone_on_copy)]
228 #![allow(clippy::dbg_macro)]
229 #![allow(clippy::mixed_attributes_style)]
230 #![allow(clippy::print_stderr)]
231 #![allow(clippy::print_stdout)]
232 #![allow(clippy::single_char_pattern)]
233 #![allow(clippy::unwrap_used)]
234 #![allow(clippy::unchecked_time_subtraction)]
235 #![allow(clippy::useless_vec)]
236 #![allow(clippy::needless_pass_by_value)]
237 use super::*;
240 use humantime::{parse_duration, parse_rfc3339};
241
242 fn assert_eq_from_parts(period: TimePeriod) {
244 assert_eq!(
245 period,
246 TimePeriod::from_parts(
247 period.length().as_minutes(),
248 period.interval_num(),
249 period.epoch_offset_in_sec()
250 )
251 );
252 }
253
254 #[test]
255 fn check_testvec() {
256 let offset = Duration::new(12 * 60 * 60, 0);
258 let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
259 let one_day = parse_duration("1day").unwrap();
260 let period = TimePeriod::new(one_day, time, offset).unwrap();
261 assert_eq!(period.interval_num, 16903);
262 assert!(period.contains(time));
263 assert_eq_from_parts(period);
264
265 let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
266 let period = TimePeriod::new(one_day, time, offset).unwrap();
267 assert_eq!(period.interval_num, 16903); assert!(period.contains(time));
269 assert_eq_from_parts(period);
270
271 assert_eq!(period.prev().unwrap().interval_num, 16902);
272 assert_eq!(period.next().unwrap().interval_num, 16904);
273
274 let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
275 let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
276 assert_eq!(period2.interval_num, 16904);
277 assert!(period < period2);
278 assert!(period2 > period);
279 assert_eq!(period.next().unwrap(), period2);
280 assert_eq!(period2.prev().unwrap(), period);
281 assert!(period2.contains(time2));
282 assert!(!period2.contains(time));
283 assert!(!period.contains(time2));
284
285 assert_eq!(
286 period.range().unwrap(),
287 parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
288 ..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
289 );
290 assert_eq!(
291 period2.range().unwrap(),
292 parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
293 ..parse_rfc3339("2016-04-14T12:00:00Z").unwrap()
294 );
295 assert_eq_from_parts(period2);
296 }
297}