Skip to main content

oxihuman_export/
sensor_log_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Timestamped sensor reading export (CSV-like format).
6
7/// A single sensor reading.
8#[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/// A sensor log containing multiple readings.
18#[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/// Add a reading to the log.
39#[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/// Export sensor log to CSV string.
56#[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/// Reading count.
69#[allow(dead_code)]
70pub fn reading_count(log: &SensorLog) -> usize {
71    log.readings.len()
72}
73
74/// Duration covered by the log (max - min timestamp).
75#[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/// Average value for a given sensor ID.
96#[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/// Peak (maximum absolute) value for a sensor.
111#[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/// List unique sensor IDs.
121#[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/// Filter readings by sensor ID.
130#[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/// Sort readings by timestamp.
140#[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}