use nutype::nutype;
use serde::{Deserialize, Serialize};
#[nutype(
validate(greater = 0, less_or_equal = 365),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Hash
)
)]
pub struct DaysBack(i64);
impl DaysBack {
pub fn week() -> Self {
Self::try_new(7).unwrap()
}
pub fn two_weeks() -> Self {
Self::try_new(14).unwrap()
}
pub fn month() -> Self {
Self::try_new(30).unwrap()
}
pub fn quarter() -> Self {
Self::try_new(90).unwrap()
}
pub fn year() -> Self {
Self::try_new(365).unwrap()
}
}
#[nutype(
validate(greater = 0, less_or_equal = 288), // Max every 5 minutes
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Hash
)
)]
pub struct PointsPerDay(usize);
impl PointsPerDay {
pub fn hourly() -> Self {
Self::try_new(24).unwrap()
}
pub fn six_hourly() -> Self {
Self::try_new(4).unwrap()
}
pub fn four_hourly() -> Self {
Self::try_new(6).unwrap()
}
pub fn three_hourly() -> Self {
Self::try_new(8).unwrap()
}
pub fn two_hourly() -> Self {
Self::try_new(12).unwrap()
}
pub fn hours_between_points(&self) -> f64 {
24.0 / self.into_inner() as f64
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimePeriod {
days_back: DaysBack,
points_per_day: PointsPerDay,
}
impl TimePeriod {
pub fn new(days_back: DaysBack, points_per_day: PointsPerDay) -> Self {
Self {
days_back,
points_per_day,
}
}
pub fn total_points(&self) -> usize {
self.days_back.into_inner() as usize * self.points_per_day.into_inner()
}
pub fn duration_hours(&self) -> f64 {
self.days_back.into_inner() as f64 * 24.0
}
pub fn data_density(&self) -> f64 {
self.points_per_day.into_inner() as f64 / 24.0
}
pub fn days_back(&self) -> DaysBack {
self.days_back
}
pub fn points_per_day(&self) -> PointsPerDay {
self.points_per_day
}
}
impl TimePeriod {
pub fn recent_detailed() -> Self {
Self::new(DaysBack::week(), PointsPerDay::hourly())
}
pub fn monthly_overview() -> Self {
Self::new(DaysBack::month(), PointsPerDay::six_hourly())
}
pub fn quarterly_trends() -> Self {
Self::new(DaysBack::quarter(), PointsPerDay::try_new(1).unwrap())
}
pub fn demo_period() -> Self {
Self::new(DaysBack::month(), PointsPerDay::six_hourly())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_days_back_validation() {
assert!(DaysBack::try_new(1).is_ok());
assert!(DaysBack::try_new(365).is_ok());
assert!(DaysBack::try_new(0).is_err());
assert!(DaysBack::try_new(366).is_err());
assert!(DaysBack::try_new(-1).is_err());
}
#[test]
fn test_points_per_day_validation() {
assert!(PointsPerDay::try_new(1).is_ok());
assert!(PointsPerDay::try_new(288).is_ok());
assert!(PointsPerDay::try_new(0).is_err());
assert!(PointsPerDay::try_new(289).is_err());
}
#[test]
fn test_time_period_calculations() {
let period = TimePeriod::new(DaysBack::week(), PointsPerDay::hourly());
assert_eq!(period.total_points(), 7 * 24);
assert_eq!(period.duration_hours(), 7.0 * 24.0);
assert_eq!(period.data_density(), 1.0); }
#[test]
fn test_preset_periods() {
let recent = TimePeriod::recent_detailed();
assert_eq!(recent.days_back(), DaysBack::week());
assert_eq!(recent.points_per_day(), PointsPerDay::hourly());
let monthly = TimePeriod::monthly_overview();
assert_eq!(monthly.days_back(), DaysBack::month());
assert_eq!(monthly.points_per_day(), PointsPerDay::six_hourly());
}
}