use crate::source::SourceMeasurement;
use crate::timestamp::NtpDuration;
#[derive(Clone, Debug)]
pub struct IntersectionResult {
pub truechimers: Vec<usize>,
pub falsetickers: Vec<usize>,
pub low: NtpDuration,
pub high: NtpDuration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EndpointType {
Low,
High,
}
#[derive(Clone, Copy, Debug)]
struct Endpoint {
value: i128,
kind: EndpointType,
}
pub fn intersect(measurements: &[SourceMeasurement]) -> IntersectionResult {
let n = measurements.len();
if n == 0 {
return IntersectionResult {
truechimers: Vec::new(),
falsetickers: Vec::new(),
low: NtpDuration::ZERO,
high: NtpDuration::ZERO,
};
}
let mut endpoints = Vec::with_capacity(n * 2);
for m in measurements {
let lambda = m.root_distance();
let lo = (m.offset - lambda).raw();
let hi = (m.offset + lambda).raw();
endpoints.push(Endpoint {
value: lo,
kind: EndpointType::Low,
});
endpoints.push(Endpoint {
value: hi,
kind: EndpointType::High,
});
}
endpoints.sort_by(|a, b| {
a.value
.cmp(&b.value)
.then_with(|| match (&a.kind, &b.kind) {
(EndpointType::Low, EndpointType::High) => std::cmp::Ordering::Less,
(EndpointType::High, EndpointType::Low) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
})
});
let max_f = (n - 1) / 2;
let mut best_low = 0i128;
let mut best_high = 0i128;
let mut found = false;
for f in 0..=max_f {
let required = (n - f) as i32;
let mut count: i32 = 0;
let mut candidate_low = 0i128;
let mut have_low = false;
for ep in &endpoints {
match ep.kind {
EndpointType::Low => {
count += 1;
if count >= required && !have_low {
candidate_low = ep.value;
have_low = true;
}
}
EndpointType::High => {
if count >= required && have_low {
best_low = candidate_low;
best_high = ep.value;
found = true;
break;
}
count -= 1;
}
}
}
if found {
break;
}
}
if !found {
return IntersectionResult {
truechimers: Vec::new(),
falsetickers: (0..n).collect(),
low: NtpDuration::ZERO,
high: NtpDuration::ZERO,
};
}
let mut truechimers = Vec::new();
let mut falsetickers = Vec::new();
for (i, m) in measurements.iter().enumerate() {
let lambda = m.root_distance();
let src_lo = (m.offset - lambda).raw();
let src_hi = (m.offset + lambda).raw();
if src_lo <= best_high && src_hi >= best_low {
truechimers.push(i);
} else {
falsetickers.push(i);
}
}
IntersectionResult {
truechimers,
falsetickers,
low: NtpDuration::from_raw(best_low),
high: NtpDuration::from_raw(best_high),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::LeapIndicator;
use crate::source::SourceId;
use crate::timestamp::NtpTimestamp;
use std::net::SocketAddr;
fn make_measurement(
idx: u16,
offset_ms: f64,
delay_ms: f64,
root_delay_ms: f64,
root_dispersion_ms: f64,
) -> SourceMeasurement {
let addr: SocketAddr = format!("127.0.0.{}:{}", idx, 123).parse().unwrap();
SourceMeasurement {
id: SourceId::Ntp {
address: addr,
reference_id: idx as u32,
},
offset: NtpDuration::from_seconds_f64(offset_ms / 1000.0),
delay: NtpDuration::from_seconds_f64(delay_ms / 1000.0),
dispersion: NtpDuration::from_seconds_f64(1.0 / 1000.0),
jitter: 0.001,
stratum: 2,
leap_indicator: LeapIndicator::NoWarning,
root_delay: NtpDuration::from_seconds_f64(root_delay_ms / 1000.0),
root_dispersion: NtpDuration::from_seconds_f64(root_dispersion_ms / 1000.0),
time: NtpTimestamp::ZERO,
}
}
#[test]
fn empty_input() {
let result = intersect(&[]);
assert!(result.truechimers.is_empty());
assert!(result.falsetickers.is_empty());
}
#[test]
fn single_source_is_truechimer() {
let m = vec![make_measurement(1, 10.0, 5.0, 10.0, 5.0)];
let result = intersect(&m);
assert_eq!(result.truechimers, vec![0]);
assert!(result.falsetickers.is_empty());
}
#[test]
fn three_sources_all_agree() {
let m = vec![
make_measurement(1, 10.0, 5.0, 10.0, 5.0),
make_measurement(2, 11.0, 5.0, 10.0, 5.0),
make_measurement(3, 9.0, 5.0, 10.0, 5.0),
];
let result = intersect(&m);
assert_eq!(result.truechimers.len(), 3);
assert!(result.falsetickers.is_empty());
assert!(result.low.raw() <= result.high.raw());
}
#[test]
fn one_falseticker_out_of_three() {
let m = vec![
make_measurement(1, 10.0, 5.0, 10.0, 5.0),
make_measurement(2, 11.0, 5.0, 10.0, 5.0),
make_measurement(3, 500.0, 5.0, 10.0, 5.0),
];
let result = intersect(&m);
assert!(result.truechimers.contains(&0));
assert!(result.truechimers.contains(&1));
assert!(result.falsetickers.contains(&2));
}
#[test]
fn five_sources_two_falsetickers() {
let m = vec![
make_measurement(1, 10.0, 5.0, 10.0, 5.0),
make_measurement(2, 11.0, 5.0, 10.0, 5.0),
make_measurement(3, 9.0, 5.0, 10.0, 5.0),
make_measurement(4, 500.0, 5.0, 10.0, 5.0),
make_measurement(5, -400.0, 5.0, 10.0, 5.0),
];
let result = intersect(&m);
assert!(result.truechimers.contains(&0));
assert!(result.truechimers.contains(&1));
assert!(result.truechimers.contains(&2));
assert!(result.falsetickers.contains(&3));
assert!(result.falsetickers.contains(&4));
}
#[test]
fn intersection_bounds_are_ordered() {
let m = vec![
make_measurement(1, 10.0, 5.0, 10.0, 5.0),
make_measurement(2, 12.0, 5.0, 10.0, 5.0),
make_measurement(3, 8.0, 5.0, 10.0, 5.0),
];
let result = intersect(&m);
assert!(result.low.raw() <= result.high.raw());
}
#[test]
fn all_completely_disagree() {
let m = vec![
make_measurement(1, 0.0, 1.0, 1.0, 1.0),
make_measurement(2, 1000.0, 1.0, 1.0, 1.0),
make_measurement(3, -1000.0, 1.0, 1.0, 1.0),
];
let result = intersect(&m);
let total = result.truechimers.len() + result.falsetickers.len();
assert_eq!(total, 3);
}
#[test]
fn two_sources_agree() {
let m = vec![
make_measurement(1, 10.0, 5.0, 10.0, 5.0),
make_measurement(2, 11.0, 5.0, 10.0, 5.0),
];
let result = intersect(&m);
assert_eq!(result.truechimers.len(), 2);
assert!(result.falsetickers.is_empty());
}
#[test]
fn two_sources_disagree() {
let m = vec![
make_measurement(1, 0.0, 1.0, 1.0, 1.0),
make_measurement(2, 1000.0, 1.0, 1.0, 1.0),
];
let result = intersect(&m);
assert!(result.truechimers.is_empty());
assert_eq!(result.falsetickers.len(), 2);
}
}