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