1use std::time::Duration;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Freshness {
24 Live,
26 Stale,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct Fix<T> {
34 pub value: T,
36 pub age: Duration,
38 pub freshness: Freshness,
40}
41
42impl<T> Fix<T> {
43 #[must_use]
45 pub fn is_live(&self) -> bool {
46 self.freshness == Freshness::Live
47 }
48}
49
50#[derive(Debug, Clone)]
52struct Entry<T> {
53 id: String,
54 rank: u8,
57 good_for: Duration,
61 latest: Option<(T, Duration)>,
62}
63
64#[derive(Debug, Clone)]
66pub struct Sources<T> {
67 entries: Vec<Entry<T>>,
68}
69
70impl<T> Default for Sources<T> {
75 fn default() -> Self {
76 Self {
77 entries: Vec::new(),
78 }
79 }
80}
81
82impl<T: Clone> Sources<T> {
83 #[must_use]
85 pub fn new() -> Self {
86 Self {
87 entries: Vec::new(),
88 }
89 }
90
91 #[must_use]
93 pub fn with(mut self, id: impl Into<String>, rank: u8, good_for: Duration) -> Self {
94 let id = id.into();
95 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == id) {
96 entry.rank = rank;
97 entry.good_for = good_for;
98 } else {
99 self.entries.push(Entry {
100 id,
101 rank,
102 good_for,
103 latest: None,
104 });
105 }
106 self
107 }
108
109 pub fn record(&mut self, id: &str, value: T, at: Duration) {
116 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == id) {
117 entry.latest = Some((value, at));
118 return;
119 }
120
121 let good_for = self
122 .entries
123 .last()
124 .map_or(Duration::from_secs(5), |entry| entry.good_for);
125 self.entries.push(Entry {
126 id: id.to_owned(),
127 rank: u8::MAX,
128 good_for,
129 latest: Some((value, at)),
130 });
131 }
132
133 #[must_use]
139 pub fn best(&self, now: Duration) -> Option<(&str, Fix<T>)> {
140 let mut live: Option<(&Entry<T>, Duration)> = None;
141 let mut newest: Option<(&Entry<T>, Duration)> = None;
142
143 for entry in &self.entries {
144 let Some((_, at)) = &entry.latest else {
145 continue;
146 };
147 let age = now.saturating_sub(*at);
148
149 if age <= entry.good_for && live.is_none_or(|(best, _)| entry.rank < best.rank) {
150 live = Some((entry, age));
151 }
152 if newest.is_none_or(|(_, seen)| age < seen) {
153 newest = Some((entry, age));
154 }
155 }
156
157 let (entry, age) = live.or(newest)?;
158 let freshness = if age <= entry.good_for {
159 Freshness::Live
160 } else {
161 Freshness::Stale
162 };
163 let (value, _) = entry.latest.as_ref()?;
164
165 Some((
166 entry.id.as_str(),
167 Fix {
168 value: value.clone(),
169 age,
170 freshness,
171 },
172 ))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 const BOAT: &str = "n2k.gps";
181 const COCKPIT: &str = "cockpit-gps";
182
183 fn secs(n: u64) -> Duration {
184 Duration::from_secs(n)
185 }
186
187 fn both() -> Sources<f64> {
191 Sources::new()
192 .with(BOAT, 0, secs(3))
193 .with(COCKPIT, 1, secs(3))
194 }
195
196 #[test]
197 fn with_nothing_heard_there_is_no_answer_rather_than_a_default_one() {
198 assert!(both().best(secs(10)).is_none());
199 }
200
201 #[test]
202 fn the_better_ranked_source_wins_while_it_is_live() {
203 let mut sources = both();
204 sources.record(COCKPIT, 1.0, secs(10));
205 sources.record(BOAT, 2.0, secs(10));
206
207 let (id, fix) = sources.best(secs(11)).expect("a fix");
208 assert_eq!(id, BOAT);
209 assert_eq!(fix.value, 2.0);
210 assert!(fix.is_live());
211 }
212
213 #[test]
214 fn when_the_network_goes_quiet_the_local_receiver_takes_over() {
215 let mut sources = both();
218 sources.record(BOAT, 2.0, secs(10));
219 sources.record(COCKPIT, 1.0, secs(13));
220
221 let (id, fix) = sources.best(secs(14)).expect("a fix");
222 assert_eq!(id, COCKPIT);
223 assert!(fix.is_live(), "the local receiver is current");
224 assert_eq!(fix.age, secs(1));
225 }
226
227 #[test]
228 fn the_network_takes_over_again_when_it_comes_back() {
229 let mut sources = both();
230 sources.record(BOAT, 2.0, secs(10));
231 sources.record(COCKPIT, 1.0, secs(13));
232 assert_eq!(sources.best(secs(14)).expect("a fix").0, COCKPIT);
233
234 sources.record(BOAT, 3.0, secs(15));
235 let (id, fix) = sources.best(secs(15)).expect("a fix");
236 assert_eq!(id, BOAT);
237 assert_eq!(fix.value, 3.0);
238 }
239
240 #[test]
241 fn when_everything_is_late_the_answer_still_comes_but_says_so() {
242 let mut sources = both();
245 sources.record(BOAT, 2.0, secs(10));
246 sources.record(COCKPIT, 1.0, secs(11));
247
248 let (id, fix) = sources.best(secs(20)).expect("a fix even when stale");
249 assert_eq!(id, COCKPIT, "the most recently heard of the two");
250 assert!(!fix.is_live());
251 assert_eq!(fix.age, secs(9));
252 }
253
254 #[test]
255 fn a_stale_better_source_does_not_outrank_a_live_worse_one() {
256 let mut sources = both();
259 sources.record(BOAT, 2.0, secs(1));
260 sources.record(COCKPIT, 1.0, secs(19));
261
262 let (id, fix) = sources.best(secs(20)).expect("a fix");
263 assert_eq!(id, COCKPIT);
264 assert!(fix.is_live());
265 }
266
267 #[test]
268 fn the_age_is_measured_against_the_receiving_clock() {
269 let mut sources = both();
270 sources.record(BOAT, 2.0, secs(100));
271 assert_eq!(sources.best(secs(102)).expect("a fix").1.age, secs(2));
272 }
273
274 #[test]
275 fn a_source_nobody_declared_is_still_heard() {
276 let mut sources = both();
280 sources.record("some-handheld", 42.0, secs(10));
281
282 let (id, fix) = sources.best(secs(10)).expect("a fix");
283 assert_eq!(id, "some-handheld");
284 assert_eq!(fix.value, 42.0);
285 }
286
287 #[test]
288 fn an_undeclared_source_gives_way_to_a_declared_one() {
289 let mut sources = both();
290 sources.record("some-handheld", 42.0, secs(10));
291 sources.record(BOAT, 2.0, secs(10));
292
293 assert_eq!(sources.best(secs(10)).expect("a fix").0, BOAT);
294 }
295
296 #[test]
297 fn re_declaring_a_source_changes_its_standing_and_keeps_its_reading() {
298 let mut sources = both();
299 sources.record(BOAT, 2.0, secs(10));
300
301 let sources = sources.with(BOAT, 9, secs(3));
303 let (id, fix) = sources.best(secs(10)).expect("a fix");
304 assert_eq!(id, BOAT, "still the only reading there is");
305 assert_eq!(fix.value, 2.0);
306 }
307}