use std::fmt::Display;
use humantime::format_rfc3339_seconds;
use tor_units::IntegerMinutes;
use web_time_compat::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct TimePeriod {
pub(crate) interval_num: u64,
pub(crate) length: IntegerMinutes<u32>,
pub(crate) epoch_offset_in_sec: u32,
}
impl PartialOrd for TimePeriod {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.length == other.length && self.epoch_offset_in_sec == other.epoch_offset_in_sec {
Some(self.interval_num.cmp(&other.interval_num))
} else {
None
}
}
}
impl Display for TimePeriod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{} ", self.interval_num())?;
match self.range() {
Ok(r) => {
let mins = self.length().as_minutes();
write!(
f,
"{}..+{}:{:02}",
format_rfc3339_seconds(r.start),
mins / 60,
mins % 60
)
}
Err(_) => write!(f, "overflow! {self:?}"),
}
}
}
impl TimePeriod {
pub fn new(
length: Duration,
when: SystemTime,
epoch_offset: Duration,
) -> Result<Self, TimePeriodError> {
let length_in_sec =
u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?;
if length_in_sec % 60 != 0 || length.subsec_nanos() != 0 {
return Err(TimePeriodError::IntervalInvalid);
}
let length_in_minutes = length_in_sec / 60;
let length = IntegerMinutes::new(length_in_minutes);
let epoch_offset_in_sec =
u32::try_from(epoch_offset.as_secs()).map_err(|_| TimePeriodError::OffsetInvalid)?;
let interval_num = when
.duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
.map_err(|_| TimePeriodError::OutOfRange)?
.as_secs()
/ u64::from(length_in_sec);
Ok(TimePeriod {
interval_num,
length,
epoch_offset_in_sec,
})
}
pub fn from_parts(length: u32, interval_num: u64, epoch_offset_in_sec: u32) -> Self {
let length_in_sec = length * 60;
Self {
interval_num,
length: length.into(),
epoch_offset_in_sec,
}
}
pub fn next(&self) -> Option<Self> {
Some(TimePeriod {
interval_num: self.interval_num.checked_add(1)?,
..*self
})
}
pub fn prev(&self) -> Option<Self> {
Some(TimePeriod {
interval_num: self.interval_num.checked_sub(1)?,
..*self
})
}
pub fn contains(&self, when: SystemTime) -> bool {
match self.range() {
Ok(r) => r.contains(&when),
Err(_) => false,
}
}
pub fn range(&self) -> Result<std::ops::Range<SystemTime>, TimePeriodError> {
(|| {
let length_in_sec = u64::from(self.length.as_minutes()) * 60;
let start_sec = length_in_sec.checked_mul(self.interval_num)?;
let end_sec = start_sec.checked_add(length_in_sec)?;
let epoch_offset = Duration::new(self.epoch_offset_in_sec.into(), 0);
let start = (SystemTime::UNIX_EPOCH + epoch_offset)
.checked_add(Duration::from_secs(start_sec))?;
let end = (SystemTime::UNIX_EPOCH + epoch_offset)
.checked_add(Duration::from_secs(end_sec))?;
Some(start..end)
})()
.ok_or(TimePeriodError::OutOfRange)
}
pub fn interval_num(&self) -> u64 {
self.interval_num
}
pub fn length(&self) -> IntegerMinutes<u32> {
self.length
}
pub fn epoch_offset_in_sec(&self) -> u32 {
self.epoch_offset_in_sec
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TimePeriodError {
#[error("Time period out was out of range")]
OutOfRange,
#[error("Invalid time period interval")]
IntervalInvalid,
#[error("Invalid time period offset")]
OffsetInvalid,
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use humantime::{parse_duration, parse_rfc3339};
fn assert_eq_from_parts(period: TimePeriod) {
assert_eq!(
period,
TimePeriod::from_parts(
period.length().as_minutes(),
period.interval_num(),
period.epoch_offset_in_sec()
)
);
}
#[test]
fn check_testvec() {
let offset = Duration::new(12 * 60 * 60, 0);
let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
let one_day = parse_duration("1day").unwrap();
let period = TimePeriod::new(one_day, time, offset).unwrap();
assert_eq!(period.interval_num, 16903);
assert!(period.contains(time));
assert_eq_from_parts(period);
let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
let period = TimePeriod::new(one_day, time, offset).unwrap();
assert_eq!(period.interval_num, 16903); assert!(period.contains(time));
assert_eq_from_parts(period);
assert_eq!(period.prev().unwrap().interval_num, 16902);
assert_eq!(period.next().unwrap().interval_num, 16904);
let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
assert_eq!(period2.interval_num, 16904);
assert!(period < period2);
assert!(period2 > period);
assert_eq!(period.next().unwrap(), period2);
assert_eq!(period2.prev().unwrap(), period);
assert!(period2.contains(time2));
assert!(!period2.contains(time));
assert!(!period.contains(time2));
assert_eq!(
period.range().unwrap(),
parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
);
assert_eq!(
period2.range().unwrap(),
parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
..parse_rfc3339("2016-04-14T12:00:00Z").unwrap()
);
assert_eq_from_parts(period2);
}
}