use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum TransitionCurve {
#[default]
Linear,
Exponential,
Sigmoid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TimeSchedule {
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub start_time: DateTime<Utc>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub end_time: DateTime<Utc>,
pub start_ratio: f64,
pub end_ratio: f64,
#[serde(default)]
pub curve: TransitionCurve,
}
impl TimeSchedule {
pub fn new(
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
start_ratio: f64,
end_ratio: f64,
) -> Self {
Self {
start_time,
end_time,
start_ratio: start_ratio.clamp(0.0, 1.0),
end_ratio: end_ratio.clamp(0.0, 1.0),
curve: TransitionCurve::Linear,
}
}
pub fn with_curve(
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
start_ratio: f64,
end_ratio: f64,
curve: TransitionCurve,
) -> Self {
Self {
start_time,
end_time,
start_ratio: start_ratio.clamp(0.0, 1.0),
end_ratio: end_ratio.clamp(0.0, 1.0),
curve,
}
}
pub fn calculate_ratio(&self, current_time: DateTime<Utc>) -> f64 {
if current_time < self.start_time {
return self.start_ratio;
}
if current_time > self.end_time {
return self.end_ratio;
}
let total_duration = self.end_time - self.start_time;
let elapsed = current_time - self.start_time;
let progress = if total_duration.num_seconds() == 0 {
1.0
} else {
elapsed.num_seconds() as f64 / total_duration.num_seconds() as f64
};
let curved_progress = match self.curve {
TransitionCurve::Linear => progress,
TransitionCurve::Exponential => {
let k = 2.0;
((progress * k).exp() - 1.0) / (k.exp() - 1.0)
}
TransitionCurve::Sigmoid => {
let k = 10.0;
1.0 / (1.0 + (-k * (progress - 0.5)).exp())
}
};
self.start_ratio + (self.end_ratio - self.start_ratio) * curved_progress
}
pub fn is_active(&self, current_time: DateTime<Utc>) -> bool {
current_time >= self.start_time && current_time <= self.end_time
}
pub fn duration(&self) -> Duration {
self.end_time - self.start_time
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_schedule_before_start() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
let before_start = start - Duration::days(1);
assert_eq!(schedule.calculate_ratio(before_start), 0.0);
}
#[test]
fn test_time_schedule_after_end() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
let after_end = end + Duration::days(1);
assert_eq!(schedule.calculate_ratio(after_end), 1.0);
}
#[test]
fn test_time_schedule_linear_midpoint() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Linear);
let midpoint = start + Duration::days(15);
let ratio = schedule.calculate_ratio(midpoint);
assert!((ratio - 0.5).abs() < 0.01);
}
#[test]
fn test_time_schedule_is_active() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
assert!(!schedule.is_active(start - Duration::days(1)));
assert!(schedule.is_active(start + Duration::days(15)));
assert!(!schedule.is_active(end + Duration::days(1)));
}
#[test]
fn test_time_schedule_duration() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
assert_eq!(schedule.duration().num_days(), 30);
}
#[test]
fn test_exponential_curve() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Exponential);
let midpoint = start + Duration::days(15);
let ratio = schedule.calculate_ratio(midpoint);
assert!(ratio < 0.5);
}
#[test]
fn test_sigmoid_curve() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Sigmoid);
let midpoint = start + Duration::days(15);
let ratio = schedule.calculate_ratio(midpoint);
assert!((ratio - 0.5).abs() < 0.1);
}
#[test]
fn test_ratio_clamping() {
let start = Utc::now();
let end = start + Duration::days(30);
let schedule = TimeSchedule::new(start, end, -0.5, 1.5);
assert_eq!(schedule.start_ratio, 0.0);
assert_eq!(schedule.end_ratio, 1.0);
}
}