chronix_analytics/preprocess/
gap.rs1pub struct GapDetector {
6 pub expected_interval_ns: i64,
8 pub tolerance: f64,
10}
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct Gap {
15 pub before_idx: usize,
17 pub after_idx: usize,
19 pub before_ts: i64,
21 pub after_ts: i64,
23 pub missing_count: usize,
25}
26
27impl GapDetector {
28 pub fn new(expected_interval_ns: i64, tolerance: f64) -> Option<Self> {
38 if expected_interval_ns <= 0 || tolerance <= 0.0 {
39 return None;
40 }
41 Some(Self {
42 expected_interval_ns,
43 tolerance,
44 })
45 }
46
47 pub fn detect(&self, timestamps: &[i64]) -> Vec<Gap> {
49 if timestamps.len() < 2 {
50 return Vec::new();
51 }
52
53 let threshold = (self.expected_interval_ns as f64 * self.tolerance) as i64;
54 let mut gaps = Vec::new();
55
56 for i in 1..timestamps.len() {
57 let delta = timestamps[i] - timestamps[i - 1];
58 if delta > threshold {
59 let missing = ((delta as f64 / self.expected_interval_ns as f64).round() as usize)
60 .saturating_sub(1);
61 if missing > 0 {
62 gaps.push(Gap {
63 before_idx: i - 1,
64 after_idx: i,
65 before_ts: timestamps[i - 1],
66 after_ts: timestamps[i],
67 missing_count: missing,
68 });
69 }
70 }
71 }
72 gaps
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn no_gaps() {
82 let ts: Vec<i64> = (0..10).map(|i| i * 1_000_000_000).collect(); let detector = GapDetector::new(1_000_000_000, 1.5).unwrap();
84 assert!(detector.detect(&ts).is_empty());
85 }
86
87 #[test]
88 fn single_gap() {
89 let ts = vec![
91 0,
92 1_000_000_000,
93 2_000_000_000,
94 5_000_000_000,
95 6_000_000_000,
96 ];
97 let detector = GapDetector::new(1_000_000_000, 1.5).unwrap();
98 let gaps = detector.detect(&ts);
99 assert_eq!(gaps.len(), 1);
100 assert_eq!(gaps[0].before_idx, 2);
101 assert_eq!(gaps[0].after_idx, 3);
102 assert_eq!(gaps[0].missing_count, 2);
103 }
104
105 #[test]
106 fn multiple_gaps() {
107 let ts = vec![
108 0,
109 1_000_000_000,
110 5_000_000_000,
111 6_000_000_000,
112 10_000_000_000,
113 ];
114 let detector = GapDetector::new(1_000_000_000, 1.5).unwrap();
115 let gaps = detector.detect(&ts);
116 assert_eq!(gaps.len(), 2);
117 }
118
119 #[test]
120 fn empty_and_single() {
121 let detector = GapDetector::new(1_000_000_000, 1.5).unwrap();
122 assert!(detector.detect(&[]).is_empty());
123 assert!(detector.detect(&[42]).is_empty());
124 }
125
126 #[test]
127 fn invalid_params_return_none() {
128 assert!(GapDetector::new(0, 1.5).is_none());
129 assert!(GapDetector::new(-1, 1.5).is_none());
130 assert!(GapDetector::new(1_000, 0.0).is_none());
131 assert!(GapDetector::new(1_000, -1.0).is_none());
132 }
133}