konnektoren_core/analytics/metrics/
success_rate.rs

1use super::super::Trend;
2use super::metric::Metric;
3use crate::challenges::ChallengeHistory;
4use crate::challenges::Performance;
5use chrono::{DateTime, Duration, Utc};
6
7#[derive(Clone, PartialEq, Default)]
8pub struct SuccessRateMetric {
9    history: ChallengeHistory,
10    reference_time: DateTime<Utc>,
11}
12
13impl SuccessRateMetric {
14    pub fn new(history: ChallengeHistory) -> Self {
15        // Use the most recent challenge end time as reference, or current time if no challenges
16        let reference_time = history
17            .challenges
18            .iter()
19            .filter_map(|c| c.end_time)
20            .max()
21            .unwrap_or_else(Utc::now);
22
23        Self {
24            history,
25            reference_time,
26        }
27    }
28
29    fn calculate(&self) -> f64 {
30        let total_challenges = self.history.len();
31        if total_challenges == 0 {
32            return 0.0;
33        }
34
35        let successful_challenges = self
36            .history
37            .challenges
38            .iter()
39            .filter(|challenge| {
40                challenge.performance(&challenge.challenge_result) >= 80 // Consider 80% as success
41            })
42            .count();
43
44        (successful_challenges as f64 / total_challenges as f64) * 100.0
45    }
46
47    pub fn get_trend(&self, time_window: Duration) -> Trend {
48        let window_start = self.reference_time - time_window;
49
50        // Split challenges into recent and older, sorting by start time
51        let mut challenges: Vec<_> = self.history.challenges.iter().collect();
52        challenges.sort_by_key(|c| c.start_time);
53
54        let (older_challenges, recent_challenges): (Vec<_>, Vec<_>) = challenges
55            .into_iter()
56            .partition(|c| c.start_time.is_none_or(|t| t < window_start));
57
58        // If no recent challenges, return Stable
59        if recent_challenges.is_empty() {
60            return Trend::Stable;
61        }
62
63        // Calculate success rate for recent challenges
64        let recent_success_count = recent_challenges
65            .iter()
66            .filter(|c| c.performance(&c.challenge_result) >= 80)
67            .count();
68        let recent_success_rate = recent_success_count as f64 / recent_challenges.len() as f64;
69
70        // Calculate success rate for older challenges
71        let older_success_rate = if older_challenges.is_empty() {
72            recent_success_rate // If no older challenges, use recent rate as baseline
73        } else {
74            let older_success_count = older_challenges
75                .iter()
76                .filter(|c| c.performance(&c.challenge_result) >= 80)
77                .count();
78            older_success_count as f64 / older_challenges.len() as f64
79        };
80
81        // Calculate trend value as the difference between recent and older success rates
82        let trend_value = recent_success_rate - older_success_rate;
83        Trend::from_value(trend_value)
84    }
85}
86
87impl Metric for SuccessRateMetric {
88    fn name(&self) -> &str {
89        "Success Rate"
90    }
91
92    fn value(&self) -> f64 {
93        self.calculate()
94    }
95
96    fn description(&self) -> &str {
97        "The percentage of successful challenges out of all completed challenges."
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::challenges::challenge_type::tests::{
105        BASE_TIMESTAMP, create_successful_challenge, create_unsuccessful_challenge,
106    };
107    use chrono::{Duration, TimeZone, Utc};
108
109    #[test]
110    fn test_success_rate_calculation() {
111        let mut history = ChallengeHistory::new();
112
113        // Add one successful and one unsuccessful challenge
114        history.add_challenge(create_successful_challenge());
115        history.add_challenge(create_unsuccessful_challenge());
116
117        let metric = SuccessRateMetric::new(history);
118        assert_eq!(
119            metric.value(),
120            50.0,
121            "Should have 50% success rate with one successful and one failed challenge"
122        );
123    }
124
125    #[test]
126    fn test_all_successful() {
127        let mut history = ChallengeHistory::new();
128        history.add_challenge(create_successful_challenge());
129        history.add_challenge(create_successful_challenge());
130
131        let metric = SuccessRateMetric::new(history);
132        assert_eq!(
133            metric.value(),
134            100.0,
135            "Should have 100% success rate with all successful challenges"
136        );
137    }
138
139    #[test]
140    fn test_all_unsuccessful() {
141        let mut history = ChallengeHistory::new();
142        history.add_challenge(create_unsuccessful_challenge());
143        history.add_challenge(create_unsuccessful_challenge());
144
145        let metric = SuccessRateMetric::new(history);
146        assert_eq!(
147            metric.value(),
148            0.0,
149            "Should have 0% success rate with all unsuccessful challenges"
150        );
151    }
152
153    #[test]
154    fn test_trend_calculation() {
155        let mut history = ChallengeHistory::new();
156
157        // Add an older unsuccessful challenge (3 days ago)
158        let mut old_challenge = create_unsuccessful_challenge();
159        old_challenge.start_time = Some(
160            Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600, 0)
161                .unwrap(),
162        );
163        old_challenge.end_time = Some(
164            Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600 + 3600, 0)
165                .unwrap(),
166        );
167        history.add_challenge(old_challenge);
168
169        // Add recent successful challenges (within last day)
170        let mut recent_challenge1 = create_successful_challenge();
171        recent_challenge1.start_time =
172            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 12 * 3600, 0).unwrap());
173        recent_challenge1.end_time =
174            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 11 * 3600, 0).unwrap());
175        history.add_challenge(recent_challenge1);
176
177        let mut recent_challenge2 = create_successful_challenge();
178        recent_challenge2.start_time =
179            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 6 * 3600, 0).unwrap());
180        recent_challenge2.end_time = Some(Utc.timestamp_opt(BASE_TIMESTAMP - 5 * 3600, 0).unwrap());
181        history.add_challenge(recent_challenge2);
182
183        let metric = SuccessRateMetric::new(history);
184        let trend = metric.get_trend(Duration::days(2));
185        assert_eq!(
186            trend,
187            Trend::Improving,
188            "Trend should be improving with recent successful challenges"
189        );
190    }
191
192    #[test]
193    fn test_declining_trend() {
194        let mut history = ChallengeHistory::new();
195
196        // Add older successful challenge (3 days ago)
197        let mut old_challenge = create_successful_challenge();
198        old_challenge.start_time = Some(
199            Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600, 0)
200                .unwrap(),
201        );
202        old_challenge.end_time = Some(
203            Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600 + 3600, 0)
204                .unwrap(),
205        );
206        history.add_challenge(old_challenge);
207
208        // Add recent unsuccessful challenges (within last day)
209        let mut recent_challenge1 = create_unsuccessful_challenge();
210        recent_challenge1.start_time =
211            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 12 * 3600, 0).unwrap());
212        recent_challenge1.end_time =
213            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 11 * 3600, 0).unwrap());
214        history.add_challenge(recent_challenge1);
215
216        let mut recent_challenge2 = create_unsuccessful_challenge();
217        recent_challenge2.start_time =
218            Some(Utc.timestamp_opt(BASE_TIMESTAMP - 6 * 3600, 0).unwrap());
219        recent_challenge2.end_time = Some(Utc.timestamp_opt(BASE_TIMESTAMP - 5 * 3600, 0).unwrap());
220        history.add_challenge(recent_challenge2);
221
222        let metric = SuccessRateMetric::new(history);
223        let trend = metric.get_trend(Duration::days(2));
224        assert_eq!(
225            trend,
226            Trend::Declining,
227            "Trend should be declining with recent unsuccessful challenges"
228        );
229    }
230
231    #[test]
232    fn test_stable_trend() {
233        let mut history = ChallengeHistory::new();
234        let window = Duration::days(2);
235
236        // First period - older challenges
237        let mut old_successful = create_successful_challenge();
238        let old_time = BASE_TIMESTAMP - 3 * 24 * 3600;
239        old_successful.start_time = Some(Utc.timestamp_opt(old_time, 0).unwrap());
240        old_successful.end_time = Some(Utc.timestamp_opt(old_time + 3600, 0).unwrap());
241        history.add_challenge(old_successful);
242
243        let mut old_unsuccessful = create_unsuccessful_challenge();
244        old_unsuccessful.start_time = Some(Utc.timestamp_opt(old_time + 4 * 3600, 0).unwrap());
245        old_unsuccessful.end_time = Some(Utc.timestamp_opt(old_time + 5 * 3600, 0).unwrap());
246        history.add_challenge(old_unsuccessful);
247
248        // Second period - recent challenges with same pattern
249        let mut recent_successful = create_successful_challenge();
250        let recent_time = BASE_TIMESTAMP - 12 * 3600;
251        recent_successful.start_time = Some(Utc.timestamp_opt(recent_time, 0).unwrap());
252        recent_successful.end_time = Some(Utc.timestamp_opt(recent_time + 3600, 0).unwrap());
253        history.add_challenge(recent_successful);
254
255        let mut recent_unsuccessful = create_unsuccessful_challenge();
256        recent_unsuccessful.start_time =
257            Some(Utc.timestamp_opt(recent_time + 4 * 3600, 0).unwrap());
258        recent_unsuccessful.end_time = Some(Utc.timestamp_opt(recent_time + 5 * 3600, 0).unwrap());
259        history.add_challenge(recent_unsuccessful);
260
261        let metric = SuccessRateMetric::new(history);
262
263        let trend = metric.get_trend(window);
264        assert_eq!(
265            trend,
266            Trend::Stable,
267            "Trend should be stable with same success ratio"
268        );
269    }
270
271    #[test]
272    fn test_empty_history() {
273        let history = ChallengeHistory::new();
274        let metric = SuccessRateMetric::new(history);
275        assert_eq!(
276            metric.value(),
277            0.0,
278            "Empty history should have 0% success rate"
279        );
280        assert_eq!(
281            metric.get_trend(Duration::days(7)),
282            Trend::Stable,
283            "Empty history should have stable trend"
284        );
285    }
286}