use serde_json::Value;
const SYSTEMS: [char; 5] = ['G', 'E', 'B', 'Q', 'R'];
#[derive(Clone, Debug, PartialEq)]
pub struct TimedCn0 {
pub time: String,
pub system: char,
pub band: u8,
pub cn0: f64,
}
impl TimedCn0 {
pub fn detector(&self) -> String {
format!("cn0_{}{}", self.system, self.band)
}
}
pub fn cn0_series(json_text: &str) -> Result<Vec<TimedCn0>, String> {
let v: Value = serde_json::from_str(json_text).map_err(|e| format!("parse JSON: {e}"))?;
let obj = v
.as_object()
.ok_or("expected a JSON object at the top level")?;
let times = obj
.get("recordTime")
.and_then(Value::as_array)
.ok_or("missing recordTime array")?;
let mut out = Vec::new();
for sys in SYSTEMS {
for band in [1u8, 2] {
let key = format!("cn0_{sys}{band}");
let Some(arr) = obj.get(&key).and_then(Value::as_array) else {
continue;
};
for (i, per_sec) in arr.iter().enumerate() {
let (Some(slots), Some(time)) =
(per_sec.as_array(), times.get(i).and_then(Value::as_str))
else {
continue;
};
for slot in slots {
if let Some(c) = slot.as_f64() {
if c > 0.0 {
out.push(TimedCn0 {
time: time.to_string(),
system: sys,
band,
cn0: c,
});
}
}
}
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
const OBS: &str = r#"{
"recordTime": ["2023-12-21 12:00:00", "2023-12-21 12:00:01"],
"VSG": [[0.0, 5.0, 0.0], [0.0, 5.0, 12.0]],
"cn0_G1": [[0.0, 40.0, 0.0], [0.0, 38.0, 45.0]],
"cn0_E1": [[47.0, 0.0], [0.0, 0.0]],
"cn0_G2": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
}"#;
#[test]
fn extracts_nonzero_cn0_with_time_system_and_band() {
let s = cn0_series(OBS).unwrap();
assert_eq!(s.len(), 4);
let g1: Vec<&TimedCn0> = s
.iter()
.filter(|x| x.system == 'G' && x.band == 1)
.collect();
assert_eq!(g1.len(), 3);
assert_eq!(g1[0].time, "2023-12-21 12:00:00");
assert_eq!(g1[0].cn0, 40.0);
assert_eq!(g1[0].detector(), "cn0_G1");
let e1: Vec<&TimedCn0> = s.iter().filter(|x| x.system == 'E').collect();
assert_eq!(e1.len(), 1);
assert_eq!(e1[0].cn0, 47.0);
assert_eq!(e1[0].time, "2023-12-21 12:00:00");
}
#[test]
fn skips_untracked_zero_slots() {
let s = cn0_series(OBS).unwrap();
assert!(s.iter().all(|x| x.cn0 > 0.0));
}
#[test]
fn missing_recordtime_is_an_error() {
assert!(cn0_series(r#"{"cn0_G1": [[40.0]]}"#).is_err());
}
#[test]
fn non_object_json_is_an_error() {
assert!(cn0_series("[1, 2, 3]").is_err());
}
#[test]
fn a_file_with_no_cn0_columns_yields_empty() {
let s = cn0_series(r#"{"recordTime": ["2023-12-21 12:00:00"]}"#).unwrap();
assert!(s.is_empty());
}
}