1use crate::source::SourceMeasurement;
2use crate::timestamp::NtpDuration;
3
4#[derive(Clone, Debug)]
6pub struct IntersectionResult {
7 pub truechimers: Vec<usize>,
9 pub falsetickers: Vec<usize>,
11 pub low: NtpDuration,
13 pub high: NtpDuration,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19enum EndpointType {
20 Low,
22 High,
24}
25
26#[derive(Clone, Copy, Debug)]
28struct Endpoint {
29 value: i128,
30 kind: EndpointType,
31}
32
33pub fn intersect(measurements: &[SourceMeasurement]) -> IntersectionResult {
46 let n = measurements.len();
47
48 if n == 0 {
49 return IntersectionResult {
50 truechimers: Vec::new(),
51 falsetickers: Vec::new(),
52 low: NtpDuration::ZERO,
53 high: NtpDuration::ZERO,
54 };
55 }
56
57 let mut endpoints = Vec::with_capacity(n * 2);
59 for m in measurements {
60 let lambda = m.root_distance();
61 let lo = (m.offset - lambda).raw();
62 let hi = (m.offset + lambda).raw();
63 endpoints.push(Endpoint {
64 value: lo,
65 kind: EndpointType::Low,
66 });
67 endpoints.push(Endpoint {
68 value: hi,
69 kind: EndpointType::High,
70 });
71 }
72
73 endpoints.sort_by(|a, b| {
76 a.value
77 .cmp(&b.value)
78 .then_with(|| match (&a.kind, &b.kind) {
79 (EndpointType::Low, EndpointType::High) => std::cmp::Ordering::Less,
80 (EndpointType::High, EndpointType::Low) => std::cmp::Ordering::Greater,
81 _ => std::cmp::Ordering::Equal,
82 })
83 });
84
85 let max_f = (n - 1) / 2;
89 let mut best_low = 0i128;
90 let mut best_high = 0i128;
91 let mut found = false;
92
93 for f in 0..=max_f {
94 let required = (n - f) as i32;
95 let mut count: i32 = 0;
96 let mut candidate_low = 0i128;
97 let mut have_low = false;
98
99 for ep in &endpoints {
100 match ep.kind {
101 EndpointType::Low => {
102 count += 1;
103 if count >= required && !have_low {
104 candidate_low = ep.value;
105 have_low = true;
106 }
107 }
108 EndpointType::High => {
109 if count >= required && have_low {
110 best_low = candidate_low;
112 best_high = ep.value;
113 found = true;
114 break;
115 }
116 count -= 1;
117 }
118 }
119 }
120
121 if found {
122 break;
123 }
124 }
125
126 if !found {
127 return IntersectionResult {
129 truechimers: Vec::new(),
130 falsetickers: (0..n).collect(),
131 low: NtpDuration::ZERO,
132 high: NtpDuration::ZERO,
133 };
134 }
135
136 let mut truechimers = Vec::new();
138 let mut falsetickers = Vec::new();
139
140 for (i, m) in measurements.iter().enumerate() {
141 let lambda = m.root_distance();
142 let src_lo = (m.offset - lambda).raw();
143 let src_hi = (m.offset + lambda).raw();
144
145 if src_lo <= best_high && src_hi >= best_low {
146 truechimers.push(i);
147 } else {
148 falsetickers.push(i);
149 }
150 }
151
152 IntersectionResult {
153 truechimers,
154 falsetickers,
155 low: NtpDuration::from_raw(best_low),
156 high: NtpDuration::from_raw(best_high),
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::clock::LeapIndicator;
164 use crate::source::SourceId;
165 use crate::timestamp::NtpTimestamp;
166 use std::net::SocketAddr;
167
168 fn make_measurement(
170 idx: u16,
171 offset_ms: f64,
172 delay_ms: f64,
173 root_delay_ms: f64,
174 root_dispersion_ms: f64,
175 ) -> SourceMeasurement {
176 let addr: SocketAddr = format!("127.0.0.{}:{}", idx, 123).parse().unwrap();
177 SourceMeasurement {
178 id: SourceId::Ntp {
179 address: addr,
180 reference_id: idx as u32,
181 },
182 offset: NtpDuration::from_seconds_f64(offset_ms / 1000.0),
183 delay: NtpDuration::from_seconds_f64(delay_ms / 1000.0),
184 dispersion: NtpDuration::from_seconds_f64(1.0 / 1000.0),
185 jitter: 0.001,
186 stratum: 2,
187 leap_indicator: LeapIndicator::NoWarning,
188 root_delay: NtpDuration::from_seconds_f64(root_delay_ms / 1000.0),
189 root_dispersion: NtpDuration::from_seconds_f64(root_dispersion_ms / 1000.0),
190 time: NtpTimestamp::ZERO,
191 }
192 }
193
194 #[test]
195 fn empty_input() {
196 let result = intersect(&[]);
197 assert!(result.truechimers.is_empty());
198 assert!(result.falsetickers.is_empty());
199 }
200
201 #[test]
202 fn single_source_is_truechimer() {
203 let m = vec![make_measurement(1, 10.0, 5.0, 10.0, 5.0)];
204 let result = intersect(&m);
205 assert_eq!(result.truechimers, vec![0]);
206 assert!(result.falsetickers.is_empty());
207 }
208
209 #[test]
210 fn three_sources_all_agree() {
211 let m = vec![
212 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
213 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
214 make_measurement(3, 9.0, 5.0, 10.0, 5.0),
215 ];
216 let result = intersect(&m);
217 assert_eq!(result.truechimers.len(), 3);
218 assert!(result.falsetickers.is_empty());
219 assert!(result.low.raw() <= result.high.raw());
220 }
221
222 #[test]
223 fn one_falseticker_out_of_three() {
224 let m = vec![
229 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
230 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
231 make_measurement(3, 500.0, 5.0, 10.0, 5.0),
232 ];
233 let result = intersect(&m);
234
235 assert!(result.truechimers.contains(&0));
236 assert!(result.truechimers.contains(&1));
237 assert!(result.falsetickers.contains(&2));
238 }
239
240 #[test]
241 fn five_sources_two_falsetickers() {
242 let m = vec![
243 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
244 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
245 make_measurement(3, 9.0, 5.0, 10.0, 5.0),
246 make_measurement(4, 500.0, 5.0, 10.0, 5.0),
247 make_measurement(5, -400.0, 5.0, 10.0, 5.0),
248 ];
249 let result = intersect(&m);
250
251 assert!(result.truechimers.contains(&0));
252 assert!(result.truechimers.contains(&1));
253 assert!(result.truechimers.contains(&2));
254 assert!(result.falsetickers.contains(&3));
255 assert!(result.falsetickers.contains(&4));
256 }
257
258 #[test]
259 fn intersection_bounds_are_ordered() {
260 let m = vec![
261 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
262 make_measurement(2, 12.0, 5.0, 10.0, 5.0),
263 make_measurement(3, 8.0, 5.0, 10.0, 5.0),
264 ];
265 let result = intersect(&m);
266 assert!(result.low.raw() <= result.high.raw());
267 }
268
269 #[test]
270 fn all_completely_disagree() {
271 let m = vec![
273 make_measurement(1, 0.0, 1.0, 1.0, 1.0),
274 make_measurement(2, 1000.0, 1.0, 1.0, 1.0),
275 make_measurement(3, -1000.0, 1.0, 1.0, 1.0),
276 ];
277 let result = intersect(&m);
278
279 let total = result.truechimers.len() + result.falsetickers.len();
282 assert_eq!(total, 3);
283 }
284
285 #[test]
286 fn two_sources_agree() {
287 let m = vec![
288 make_measurement(1, 10.0, 5.0, 10.0, 5.0),
289 make_measurement(2, 11.0, 5.0, 10.0, 5.0),
290 ];
291 let result = intersect(&m);
292 assert_eq!(result.truechimers.len(), 2);
293 assert!(result.falsetickers.is_empty());
294 }
295
296 #[test]
297 fn two_sources_disagree() {
298 let m = vec![
300 make_measurement(1, 0.0, 1.0, 1.0, 1.0),
301 make_measurement(2, 1000.0, 1.0, 1.0, 1.0),
302 ];
303 let result = intersect(&m);
304 assert!(result.truechimers.is_empty());
307 assert_eq!(result.falsetickers.len(), 2);
308 }
309}