use std::fs;
use std::path::Path;
use ctddump::handle_dispatch;
use polars::prelude::*;
fn dispatch(args: &[&str]) {
handle_dispatch(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
.expect("filter should succeed");
}
fn write_fixture(path: &Path) {
let mut df = DataFrame::new(vec![
Series::new("platform_code".into(), vec!["A", "A", "A", "A", "B", "B", "B"]),
Series::new("profile_no".into(), vec![1u32, 1, 2, 2, 1, 1, 2]),
Series::new("observation_no".into(), vec![1u32, 2, 1, 2, 1, 2, 1]),
Series::new("longitude".into(), vec![0.0f32, 0.0, 50.0, 50.0, 10.0, 10.0, f32::NAN]),
Series::new("latitude".into(), vec![0.0f32, 0.0, 50.0, 50.0, 10.0, 10.0, f32::NAN]),
Series::new("temp".into(), vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]),
])
.unwrap();
let f = fs::File::create(path).unwrap();
ParquetWriter::new(f).finish(&mut df).unwrap();
}
fn profiles(path: &Path) -> Vec<(String, u32)> {
let df = ParquetReader::new(fs::File::open(path).unwrap()).finish().unwrap();
let pc = df.column("platform_code").unwrap().str().unwrap();
let pn = df.column("profile_no").unwrap().u32().unwrap();
let mut pairs: Vec<(String, u32)> = pc
.into_iter()
.zip(pn)
.map(|(c, n)| (c.unwrap().to_string(), n.unwrap()))
.collect();
pairs.sort();
pairs.dedup();
pairs
}
#[test]
fn test_filter_area_include() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.parquet");
let out = dir.path().join("out.parquet");
write_fixture(&src);
dispatch(&[
"filter",
"--min-lon", "-5", "--max-lon", "5",
"--min-lat", "-5", "--max-lat", "5",
src.to_str().unwrap(), out.to_str().unwrap(),
]);
assert_eq!(profiles(&out), vec![("A".to_string(), 1)]);
}
#[test]
fn test_filter_area_exclude_keeps_nan() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.parquet");
let out = dir.path().join("out.parquet");
write_fixture(&src);
dispatch(&[
"filter", "--mode", "exclude",
"--min-lon", "-5", "--max-lon", "5",
"--min-lat", "-5", "--max-lat", "5",
src.to_str().unwrap(), out.to_str().unwrap(),
]);
assert_eq!(
profiles(&out),
vec![("A".to_string(), 2), ("B".to_string(), 1), ("B".to_string(), 2)]
);
}
#[test]
fn test_filter_area_default_mode_is_include() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.parquet");
let out = dir.path().join("out.parquet");
write_fixture(&src);
dispatch(&[
"filter",
"--min-lon", "-5", "--max-lon", "15",
"--min-lat", "-5", "--max-lat", "15",
src.to_str().unwrap(), out.to_str().unwrap(),
]);
assert_eq!(profiles(&out), vec![("A".to_string(), 1), ("B".to_string(), 1)]);
}
#[test]
fn test_filter_area_streaming_is_chunk_independent() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.parquet");
let out = dir.path().join("out.parquet");
write_fixture(&src);
std::env::set_var("CTDDUMP_CHUNK_ROWS", "2");
dispatch(&[
"filter",
"--min-lon", "-5", "--max-lon", "15",
"--min-lat", "-5", "--max-lat", "15",
src.to_str().unwrap(), out.to_str().unwrap(),
]);
std::env::remove_var("CTDDUMP_CHUNK_ROWS");
assert_eq!(profiles(&out), vec![("A".to_string(), 1), ("B".to_string(), 1)]);
}
#[test]
fn test_filter_area_rejects_inverted_box() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.parquet");
let out = dir.path().join("out.parquet");
write_fixture(&src);
let err = handle_dispatch(
&["filter", "--min-lon", "10", "--max-lon", "-10",
"--min-lat", "0", "--max-lat", "5",
src.to_str().unwrap(), out.to_str().unwrap()]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
);
assert!(err.is_err(), "min-lon > max-lon must be rejected");
}