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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(rename_all = "snake_case")]
13pub enum TransitionCurve {
14 Linear,
16 Exponential,
18 Sigmoid,
20}
21
22impl Default for TransitionCurve {
23 fn default() -> Self {
24 TransitionCurve::Linear
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31pub struct TimeSchedule {
32 #[cfg_attr(feature = "schema", schemars(with = "String"))]
34 pub start_time: DateTime<Utc>,
35 #[cfg_attr(feature = "schema", schemars(with = "String"))]
37 pub end_time: DateTime<Utc>,
38 pub start_ratio: f64,
40 pub end_ratio: f64,
42 #[serde(default)]
44 pub curve: TransitionCurve,
45}
46
47impl TimeSchedule {
48 pub fn new(
50 start_time: DateTime<Utc>,
51 end_time: DateTime<Utc>,
52 start_ratio: f64,
53 end_ratio: f64,
54 ) -> Self {
55 Self {
56 start_time,
57 end_time,
58 start_ratio: start_ratio.clamp(0.0, 1.0),
59 end_ratio: end_ratio.clamp(0.0, 1.0),
60 curve: TransitionCurve::Linear,
61 }
62 }
63
64 pub fn with_curve(
66 start_time: DateTime<Utc>,
67 end_time: DateTime<Utc>,
68 start_ratio: f64,
69 end_ratio: f64,
70 curve: TransitionCurve,
71 ) -> Self {
72 Self {
73 start_time,
74 end_time,
75 start_ratio: start_ratio.clamp(0.0, 1.0),
76 end_ratio: end_ratio.clamp(0.0, 1.0),
77 curve,
78 }
79 }
80
81 pub fn calculate_ratio(&self, current_time: DateTime<Utc>) -> f64 {
88 if current_time < self.start_time {
90 return self.start_ratio;
91 }
92
93 if current_time > self.end_time {
95 return self.end_ratio;
96 }
97
98 let total_duration = self.end_time - self.start_time;
100 let elapsed = current_time - self.start_time;
101
102 let progress = if total_duration.num_seconds() == 0 {
103 1.0
104 } else {
105 elapsed.num_seconds() as f64 / total_duration.num_seconds() as f64
106 };
107
108 let curved_progress = match self.curve {
110 TransitionCurve::Linear => progress,
111 TransitionCurve::Exponential => {
112 let k = 2.0;
115 (progress * k).exp() - 1.0 / (k.exp() - 1.0)
116 }
117 TransitionCurve::Sigmoid => {
118 let k = 10.0;
121 1.0 / (1.0 + (-k * (progress - 0.5)).exp())
122 }
123 };
124
125 self.start_ratio + (self.end_ratio - self.start_ratio) * curved_progress
127 }
128
129 pub fn is_active(&self, current_time: DateTime<Utc>) -> bool {
131 current_time >= self.start_time && current_time <= self.end_time
132 }
133
134 pub fn duration(&self) -> Duration {
136 self.end_time - self.start_time
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_time_schedule_before_start() {
146 let start = Utc::now();
147 let end = start + Duration::days(30);
148 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
149
150 let before_start = start - Duration::days(1);
151 assert_eq!(schedule.calculate_ratio(before_start), 0.0);
152 }
153
154 #[test]
155 fn test_time_schedule_after_end() {
156 let start = Utc::now();
157 let end = start + Duration::days(30);
158 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
159
160 let after_end = end + Duration::days(1);
161 assert_eq!(schedule.calculate_ratio(after_end), 1.0);
162 }
163
164 #[test]
165 fn test_time_schedule_linear_midpoint() {
166 let start = Utc::now();
167 let end = start + Duration::days(30);
168 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Linear);
169
170 let midpoint = start + Duration::days(15);
171 let ratio = schedule.calculate_ratio(midpoint);
172 assert!((ratio - 0.5).abs() < 0.01);
174 }
175
176 #[test]
177 fn test_time_schedule_is_active() {
178 let start = Utc::now();
179 let end = start + Duration::days(30);
180 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
181
182 assert!(!schedule.is_active(start - Duration::days(1)));
183 assert!(schedule.is_active(start + Duration::days(15)));
184 assert!(!schedule.is_active(end + Duration::days(1)));
185 }
186
187 #[test]
188 fn test_time_schedule_duration() {
189 let start = Utc::now();
190 let end = start + Duration::days(30);
191 let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
192
193 assert_eq!(schedule.duration().num_days(), 30);
194 }
195
196 #[test]
197 fn test_exponential_curve() {
198 let start = Utc::now();
199 let end = start + Duration::days(30);
200 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Exponential);
201
202 let midpoint = start + Duration::days(15);
203 let ratio = schedule.calculate_ratio(midpoint);
204 assert!(ratio < 0.5);
206 }
207
208 #[test]
209 fn test_sigmoid_curve() {
210 let start = Utc::now();
211 let end = start + Duration::days(30);
212 let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Sigmoid);
213
214 let midpoint = start + Duration::days(15);
215 let ratio = schedule.calculate_ratio(midpoint);
216 assert!((ratio - 0.5).abs() < 0.1);
218 }
219
220 #[test]
221 fn test_ratio_clamping() {
222 let start = Utc::now();
223 let end = start + Duration::days(30);
224 let schedule = TimeSchedule::new(start, end, -0.5, 1.5);
225
226 assert_eq!(schedule.start_ratio, 0.0);
228 assert_eq!(schedule.end_ratio, 1.0);
229 }
230}