use super::{Observation, Orient};
pub fn observations(text: &str, agc_orient: Orient) -> Vec<Observation> {
let Some(cols) = column_index(text) else {
return Vec::new();
};
let cn0_i = cols.iter().position(|c| c == "Cn0DbHz");
let agc_i = cols.iter().position(|c| c == "AgcDb");
let mut out = Vec::new();
for line in text.lines() {
let line = line.trim();
if !line.starts_with("Raw,") {
continue;
}
let fields: Vec<&str> = line.split(',').collect();
if let Some(i) = cn0_i {
if let Some(v) = fields.get(i).and_then(|s| s.trim().parse::<f64>().ok()) {
out.push(Observation::new("cn0", v, Orient::Negate));
}
}
if let Some(i) = agc_i {
if let Some(v) = fields.get(i).and_then(|s| s.trim().parse::<f64>().ok()) {
out.push(Observation::new("agc", v, agc_orient));
}
}
}
out
}
fn column_index(text: &str) -> Option<Vec<String>> {
for line in text.lines() {
let t = line.trim_start();
if let Some(rest) = t.strip_prefix('#') {
let rest = rest.trim_start();
if rest.starts_with("Raw,") {
return Some(rest.split(',').map(|s| s.trim().to_string()).collect());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const CSV: &str = "\
# Header Description:
# Raw,Svid,TimeNanos,Cn0DbHz,ConstellationType,AgcDb,CarrierFrequencyHz
Fix,GPS,1,2,3
Raw,5,123456789,42.5,1,18.0,1575420030
Raw,12,123456789,30.0,1,9.5,1575420030
";
#[test]
fn extracts_cn0_negated_and_agc_per_raw_row() {
let obs = observations(CSV, Orient::Negate);
assert_eq!(obs.len(), 4);
assert_eq!(obs[0].detector, "cn0");
assert_eq!(obs[0].raw, 42.5);
assert_eq!(obs[0].score, -42.5);
assert_eq!(obs[1].detector, "agc");
assert_eq!(obs[1].raw, 18.0);
assert_eq!(obs[1].score, -18.0);
}
#[test]
fn jammed_row_outscores_clean_row_on_both_observables() {
let obs = observations(CSV, Orient::Negate);
let cn0_clean = obs[0].score; let cn0_jammed = obs[2].score; let agc_clean = obs[1].score; let agc_jammed = obs[3].score; assert!(cn0_jammed > cn0_clean);
assert!(agc_jammed > agc_clean);
}
#[test]
fn agc_orientation_is_a_parameter() {
let raw = observations(CSV, Orient::Raw);
assert_eq!(raw[1].detector, "agc");
assert_eq!(raw[1].score, 18.0); }
#[test]
fn missing_agc_column_still_yields_cn0() {
let csv = "\
# Raw,Svid,Cn0DbHz
Raw,5,42.5
";
let obs = observations(csv, Orient::Negate);
assert_eq!(obs.len(), 1);
assert_eq!(obs[0].detector, "cn0");
}
#[test]
fn no_raw_header_yields_nothing() {
assert!(observations("# Fix,lat,lon\nFix,1,2\n", Orient::Negate).is_empty());
}
}