use chrono::Datelike;
use crate::lifecycle::config::TimeBucket;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeasonalityKind {
Monthly,
Quarterly,
Weekly,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SeasonalityModel {
pub multipliers: Vec<f64>,
pub kind: SeasonalityKind,
}
impl SeasonalityModel {
pub fn multiplier_for(&self, bucket: &TimeBucket) -> f64 {
let index = match self.kind {
SeasonalityKind::Monthly => bucket.start.month0() as usize, SeasonalityKind::Quarterly => (bucket.start.month0() / 3) as usize, SeasonalityKind::Weekly => {
bucket.start.weekday().num_days_from_monday() as usize }
};
self.multipliers.get(index).copied().unwrap_or(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
fn bucket_starting(y: i32, m: u32, d: u32) -> TimeBucket {
let start = NaiveDate::from_ymd_opt(y, m, d).expect("valid date");
TimeBucket {
index: 0,
start,
end: start,
}
}
fn monthly() -> SeasonalityModel {
SeasonalityModel {
multipliers: vec![1.0, 0.7, 0.85, 1.0, 1.1, 0.8, 0.7, 0.85, 1.2, 1.4, 1.8, 2.5],
kind: SeasonalityKind::Monthly,
}
}
#[test]
fn test_seasonality_december_multiplier() {
let m = monthly();
assert_eq!(m.multiplier_for(&bucket_starting(2024, 12, 1)), 2.5);
}
#[test]
fn test_seasonality_july_multiplier() {
let m = monthly();
assert_eq!(m.multiplier_for(&bucket_starting(2024, 7, 1)), 0.7);
}
#[test]
fn test_seasonality_january_multiplier() {
let m = monthly();
assert_eq!(m.multiplier_for(&bucket_starting(2024, 1, 15)), 1.0);
}
#[test]
fn test_seasonality_quarterly() {
let m = SeasonalityModel {
multipliers: vec![1.0, 1.5, 0.8, 2.0],
kind: SeasonalityKind::Quarterly,
};
assert_eq!(m.multiplier_for(&bucket_starting(2024, 2, 1)), 1.0); assert_eq!(m.multiplier_for(&bucket_starting(2024, 5, 1)), 1.5); assert_eq!(m.multiplier_for(&bucket_starting(2024, 8, 1)), 0.8); assert_eq!(m.multiplier_for(&bucket_starting(2024, 11, 1)), 2.0); }
#[test]
fn test_seasonality_weekly() {
let m = SeasonalityModel {
multipliers: vec![1.0, 1.0, 1.0, 1.0, 1.2, 1.5, 0.5],
kind: SeasonalityKind::Weekly,
};
assert_eq!(m.multiplier_for(&bucket_starting(2024, 1, 1)), 1.0);
assert_eq!(m.multiplier_for(&bucket_starting(2024, 1, 6)), 1.5);
assert_eq!(m.multiplier_for(&bucket_starting(2024, 1, 7)), 0.5);
}
#[test]
fn test_seasonality_out_of_range_is_neutral() {
let m = SeasonalityModel {
multipliers: vec![1.0, 2.0], kind: SeasonalityKind::Monthly,
};
assert_eq!(m.multiplier_for(&bucket_starting(2024, 12, 1)), 1.0);
}
}