mockforge_core/reality_continuum/
schedule.rs1use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum TransitionCurve {
13 Linear,
15 Exponential,
17 Sigmoid,
19}
20
21impl Default for TransitionCurve {
22 fn default() -> Self {
23 TransitionCurve::Linear
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct TimeSchedule {
30 pub start_time: DateTime<Utc>,
32 pub end_time: DateTime<Utc>,
34 pub start_ratio: f64,
36 pub end_ratio: f64,
38 #[serde(default)]
40 pub curve: TransitionCurve,
41}
42
43impl TimeSchedule {
44 pub fn new(
46 start_time: DateTime<Utc>,
47 end_time: DateTime<Utc>,
48 start_ratio: f64,
49 end_ratio: f64,
50 ) -> Self {
51 Self {
52 start_time,
53 end_time,
54 start_ratio: start_ratio.clamp(0.0, 1.0),
55 end_ratio: end_ratio.clamp(0.0, 1.0),
56 curve: TransitionCurve::Linear,
57 }
58 }
59
60 pub fn with_curve(
62 start_time: DateTime<Utc>,
63 end_time: DateTime<Utc>,
64 start_ratio: f64,
65 end_ratio: f64,
66 curve: TransitionCurve,
67 ) -> Self {
68 Self {
69 start_time,
70 end_time,
71 start_ratio: start_ratio.clamp(0.0, 1.0),
72 end_ratio: end_ratio.clamp(0.0, 1.0),
73 curve,
74 }
75 }
76
77 pub fn calculate_ratio(&self, current_time: DateTime<Utc>) -> f64 {
84 if current_time < self.start_time {
86 return self.start_ratio;
87 }
88
89 if current_time > self.end_time {
91 return self.end_ratio;
92 }
93
94 let total_duration = self.end_time - self.start_time;
96 let elapsed = current_time - self.start_time;
97
98 let progress = if total_duration.num_seconds() == 0 {
99 1.0
100 } else {
101 elapsed.num_seconds() as f64 / total_duration.num_seconds() as f64
102 };
103
104 let curved_progress = match self.curve {
106 TransitionCurve::Linear => progress,
107 TransitionCurve::Exponential => {
108 let k = 2.0;
111 (progress * k).exp() - 1.0 / (k.exp() - 1.0)
112 }
113 TransitionCurve::Sigmoid => {
114 let k = 10.0;
117 1.0 / (1.0 + (-k * (progress - 0.5)).exp())
118 }
119 };
120
121 self.start_ratio + (self.end_ratio - self.start_ratio) * curved_progress
123 }
124
125 pub fn is_active(&self, current_time: DateTime<Utc>) -> bool {
127 current_time >= self.start_time && current_time <= self.end_time
128 }
129
130 pub fn duration(&self) -> Duration {
132 self.end_time - self.start_time
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_time_schedule_before_start() {
142 let start = Utc::now();
143 let end = start + Duration::days(30);
144 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
145
146 let before_start = start - Duration::days(1);
147 assert_eq!(schedule.calculate_ratio(before_start), 0.0);
148 }
149
150 #[test]
151 fn test_time_schedule_after_end() {
152 let start = Utc::now();
153 let end = start + Duration::days(30);
154 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
155
156 let after_end = end + Duration::days(1);
157 assert_eq!(schedule.calculate_ratio(after_end), 1.0);
158 }
159
160 #[test]
161 fn test_time_schedule_linear_midpoint() {
162 let start = Utc::now();
163 let end = start + Duration::days(30);
164 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Linear);
165
166 let midpoint = start + Duration::days(15);
167 let ratio = schedule.calculate_ratio(midpoint);
168 assert!((ratio - 0.5).abs() < 0.01);
170 }
171
172 #[test]
173 fn test_time_schedule_is_active() {
174 let start = Utc::now();
175 let end = start + Duration::days(30);
176 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
177
178 assert!(!schedule.is_active(start - Duration::days(1)));
179 assert!(schedule.is_active(start + Duration::days(15)));
180 assert!(!schedule.is_active(end + Duration::days(1)));
181 }
182
183 #[test]
184 fn test_time_schedule_duration() {
185 let start = Utc::now();
186 let end = start + Duration::days(30);
187 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
188
189 assert_eq!(schedule.duration().num_days(), 30);
190 }
191
192 #[test]
193 fn test_exponential_curve() {
194 let start = Utc::now();
195 let end = start + Duration::days(30);
196 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Exponential);
197
198 let midpoint = start + Duration::days(15);
199 let ratio = schedule.calculate_ratio(midpoint);
200 assert!(ratio < 0.5);
202 }
203
204 #[test]
205 fn test_sigmoid_curve() {
206 let start = Utc::now();
207 let end = start + Duration::days(30);
208 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Sigmoid);
209
210 let midpoint = start + Duration::days(15);
211 let ratio = schedule.calculate_ratio(midpoint);
212 assert!((ratio - 0.5).abs() < 0.1);
214 }
215
216 #[test]
217 fn test_ratio_clamping() {
218 let start = Utc::now();
219 let end = start + Duration::days(30);
220 let schedule = TimeSchedule::new(start, end, -0.5, 1.5);
221
222 assert_eq!(schedule.start_ratio, 0.0);
224 assert_eq!(schedule.end_ratio, 1.0);
225 }
226}