px_native/events/
calibrate.rs1use std::collections::{BTreeMap, BTreeSet};
11
12use serde::{Deserialize, Serialize};
13
14use crate::events::model::SensorEvent;
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct CalibrationReport {
18 pub missing_tags: Vec<String>,
20 pub extra_tags: Vec<String>,
22 pub per_tag: BTreeMap<String, TagDiff>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct TagDiff {
28 pub observed_keys: Vec<String>,
29 pub synthesised_keys: Vec<String>,
30 pub missing_keys: Vec<String>,
31}
32
33pub fn calibrate(observed: &[SensorEvent], synthesised: &[SensorEvent]) -> CalibrationReport {
34 let observed_by_tag = group_by_tag(observed);
35 let synth_by_tag = group_by_tag(synthesised);
36
37 let observed_tags: BTreeSet<&String> = observed_by_tag.keys().collect();
38 let synth_tags: BTreeSet<&String> = synth_by_tag.keys().collect();
39
40 let mut report = CalibrationReport::default();
41 for t in observed_tags.difference(&synth_tags) {
42 report.missing_tags.push((*t).clone());
43 }
44 for t in synth_tags.difference(&observed_tags) {
45 report.extra_tags.push((*t).clone());
46 }
47 for tag in observed_tags.intersection(&synth_tags) {
48 let obs_keys = union_keys(observed_by_tag.get(*tag).unwrap_or(&Vec::new()));
49 let syn_keys = union_keys(synth_by_tag.get(*tag).unwrap_or(&Vec::new()));
50 let mut missing: Vec<String> = obs_keys.difference(&syn_keys).cloned().collect();
51 missing.sort();
52 report.per_tag.insert(
53 (*tag).clone(),
54 TagDiff {
55 observed_keys: sorted(obs_keys),
56 synthesised_keys: sorted(syn_keys),
57 missing_keys: missing,
58 },
59 );
60 }
61 report
62}
63
64fn group_by_tag(events: &[SensorEvent]) -> BTreeMap<String, Vec<&SensorEvent>> {
65 let mut by_tag: BTreeMap<String, Vec<&SensorEvent>> = BTreeMap::new();
66 for ev in events {
67 by_tag.entry(ev.t.clone()).or_default().push(ev);
68 }
69 by_tag
70}
71
72fn union_keys(events: &[&SensorEvent]) -> BTreeSet<String> {
73 let mut keys: BTreeSet<String> = BTreeSet::new();
74 for ev in events {
75 for k in ev.d.keys() {
76 keys.insert(k.clone());
77 }
78 }
79 keys
80}
81
82fn sorted(s: BTreeSet<String>) -> Vec<String> {
83 s.into_iter().collect()
84}
85
86#[cfg(test)]
87#[allow(clippy::expect_used)]
88mod tests {
89 use super::*;
90 use crate::events::SensorEvent;
91
92 #[test]
93 fn empty_captures_yield_empty_report() {
94 let r = calibrate(&[], &[]);
95 assert!(r.missing_tags.is_empty());
96 assert!(r.extra_tags.is_empty());
97 }
98
99 #[test]
100 fn reports_missing_and_extra_tags() {
101 let obs = vec![SensorEvent::new("PXobs").with("k1", "v")];
102 let syn = vec![SensorEvent::new("PXsyn").with("k2", "v")];
103 let r = calibrate(&obs, &syn);
104 assert_eq!(r.missing_tags, vec!["PXobs"]);
105 assert_eq!(r.extra_tags, vec!["PXsyn"]);
106 }
107
108 #[test]
109 fn flags_missing_keys_within_shared_tag() {
110 let obs = vec![
111 SensorEvent::new("PX561")
112 .with("AzNweUZUfEs=", 1u64)
113 .with("EwNgCVZlZDw=", "ua")
114 .with("MISSING_KEY", "x"),
115 ];
116 let syn = vec![
117 SensorEvent::new("PX561")
118 .with("AzNweUZUfEs=", 1u64)
119 .with("EwNgCVZlZDw=", "ua"),
120 ];
121 let r = calibrate(&obs, &syn);
122 let diff = r.per_tag.get("PX561").expect("PX561 entry");
123 assert_eq!(diff.missing_keys, vec!["MISSING_KEY"]);
124 }
125}