oxihuman_export/
sensor_log_export.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct SensorReading {
11 pub timestamp_ms: u64,
12 pub sensor_id: String,
13 pub value: f64,
14 pub unit: String,
15}
16
17#[allow(dead_code)]
19pub struct SensorLog {
20 pub readings: Vec<SensorReading>,
21}
22
23impl SensorLog {
24 #[allow(dead_code)]
25 pub fn new() -> Self {
26 Self {
27 readings: Vec::new(),
28 }
29 }
30}
31
32impl Default for SensorLog {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38#[allow(dead_code)]
40pub fn add_reading(
41 log: &mut SensorLog,
42 timestamp_ms: u64,
43 sensor_id: &str,
44 value: f64,
45 unit: &str,
46) {
47 log.readings.push(SensorReading {
48 timestamp_ms,
49 sensor_id: sensor_id.to_string(),
50 value,
51 unit: unit.to_string(),
52 });
53}
54
55#[allow(dead_code)]
57pub fn export_sensor_log_csv(log: &SensorLog) -> String {
58 let mut out = String::from("timestamp_ms,sensor_id,value,unit\n");
59 for r in &log.readings {
60 out.push_str(&format!(
61 "{},{},{},{}\n",
62 r.timestamp_ms, r.sensor_id, r.value, r.unit
63 ));
64 }
65 out
66}
67
68#[allow(dead_code)]
70pub fn reading_count(log: &SensorLog) -> usize {
71 log.readings.len()
72}
73
74#[allow(dead_code)]
76pub fn log_duration_ms(log: &SensorLog) -> u64 {
77 if log.readings.is_empty() {
78 return 0;
79 }
80 let min_t = log
81 .readings
82 .iter()
83 .map(|r| r.timestamp_ms)
84 .min()
85 .unwrap_or(0);
86 let max_t = log
87 .readings
88 .iter()
89 .map(|r| r.timestamp_ms)
90 .max()
91 .unwrap_or(0);
92 max_t.saturating_sub(min_t)
93}
94
95#[allow(dead_code)]
97pub fn average_sensor_value(log: &SensorLog, sensor_id: &str) -> f64 {
98 let values: Vec<f64> = log
99 .readings
100 .iter()
101 .filter(|r| r.sensor_id == sensor_id)
102 .map(|r| r.value)
103 .collect();
104 if values.is_empty() {
105 return 0.0;
106 }
107 values.iter().sum::<f64>() / values.len() as f64
108}
109
110#[allow(dead_code)]
112pub fn peak_sensor_value(log: &SensorLog, sensor_id: &str) -> f64 {
113 log.readings
114 .iter()
115 .filter(|r| r.sensor_id == sensor_id)
116 .map(|r| r.value.abs())
117 .fold(f64::NEG_INFINITY, f64::max)
118}
119
120#[allow(dead_code)]
122pub fn unique_sensor_ids(log: &SensorLog) -> Vec<String> {
123 let mut ids: Vec<String> = log.readings.iter().map(|r| r.sensor_id.clone()).collect();
124 ids.sort();
125 ids.dedup();
126 ids
127}
128
129#[allow(dead_code)]
131pub fn filter_by_sensor(log: &SensorLog, sensor_id: &str) -> Vec<SensorReading> {
132 log.readings
133 .iter()
134 .filter(|r| r.sensor_id == sensor_id)
135 .cloned()
136 .collect()
137}
138
139#[allow(dead_code)]
141pub fn sort_by_timestamp(log: &mut SensorLog) {
142 log.readings.sort_by_key(|r| r.timestamp_ms);
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn sample_log() -> SensorLog {
150 let mut log = SensorLog::new();
151 add_reading(&mut log, 0, "temp", 20.0, "C");
152 add_reading(&mut log, 100, "temp", 21.5, "C");
153 add_reading(&mut log, 200, "pressure", 1013.0, "hPa");
154 log
155 }
156
157 #[test]
158 fn reading_count_correct() {
159 let log = sample_log();
160 assert_eq!(reading_count(&log), 3);
161 }
162
163 #[test]
164 fn log_duration_200ms() {
165 let log = sample_log();
166 assert_eq!(log_duration_ms(&log), 200);
167 }
168
169 #[test]
170 fn average_temp_correct() {
171 let log = sample_log();
172 let avg = average_sensor_value(&log, "temp");
173 assert!((avg - 20.75).abs() < 1e-5);
174 }
175
176 #[test]
177 fn csv_header_present() {
178 let log = sample_log();
179 let csv = export_sensor_log_csv(&log);
180 assert!(csv.starts_with("timestamp_ms,sensor_id,value,unit"));
181 }
182
183 #[test]
184 fn csv_line_count() {
185 let log = sample_log();
186 let csv = export_sensor_log_csv(&log);
187 let lines: Vec<&str> = csv.trim().split('\n').collect();
188 assert_eq!(lines.len(), 4);
189 }
190
191 #[test]
192 fn unique_sensor_ids_correct() {
193 let log = sample_log();
194 let ids = unique_sensor_ids(&log);
195 assert_eq!(ids.len(), 2);
196 assert!(ids.contains(&String::from("pressure")));
197 }
198
199 #[test]
200 fn filter_by_sensor_correct() {
201 let log = sample_log();
202 let temp_readings = filter_by_sensor(&log, "temp");
203 assert_eq!(temp_readings.len(), 2);
204 }
205
206 #[test]
207 fn peak_sensor_value_correct() {
208 let log = sample_log();
209 let peak = peak_sensor_value(&log, "temp");
210 assert!((peak - 21.5).abs() < 1e-5);
211 }
212
213 #[test]
214 fn sort_by_timestamp_ordered() {
215 let mut log = SensorLog::new();
216 add_reading(&mut log, 300, "x", 1.0, "m");
217 add_reading(&mut log, 100, "x", 2.0, "m");
218 add_reading(&mut log, 200, "x", 3.0, "m");
219 sort_by_timestamp(&mut log);
220 assert_eq!(log.readings[0].timestamp_ms, 100);
221 }
222
223 #[test]
224 fn empty_log_duration_zero() {
225 let log = SensorLog::new();
226 assert_eq!(log_duration_ms(&log), 0);
227 }
228}