use netcdf_reader::{NcFile, NcFormat, NcSliceInfo, NcSliceInfoElem};
struct AttrSpec {
name: &'static str,
nc_type: u32,
payload: Vec<u8>,
nelems: u32,
}
fn double_attr(name: &'static str, value: f64) -> AttrSpec {
AttrSpec {
name,
nc_type: 6, nelems: 1,
payload: value.to_be_bytes().to_vec(),
}
}
fn short_attr(name: &'static str, value: i16) -> AttrSpec {
let mut payload = value.to_be_bytes().to_vec();
payload.extend_from_slice(&[0, 0]);
AttrSpec {
name,
nc_type: 3, nelems: 1,
payload,
}
}
fn build_cdf1_short_var(var_name: &str, values: &[i16], attrs: Vec<AttrSpec>) -> Vec<u8> {
let dim_size = values.len() as u32;
let mut buf = Vec::new();
buf.extend_from_slice(b"CDF\x01");
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0x0000_000Au32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes());
buf.push(b'x');
buf.extend_from_slice(&[0, 0, 0]); buf.extend_from_slice(&dim_size.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes());
let name_bytes = var_name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as u32).to_be_bytes());
buf.extend_from_slice(name_bytes);
let name_pad = (4 - (name_bytes.len() % 4)) % 4;
buf.extend_from_slice(&vec![0u8; name_pad]);
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
if attrs.is_empty() {
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes());
} else {
buf.extend_from_slice(&0x0000_000Cu32.to_be_bytes()); buf.extend_from_slice(&(attrs.len() as u32).to_be_bytes());
for attr in &attrs {
let aname = attr.name.as_bytes();
buf.extend_from_slice(&(aname.len() as u32).to_be_bytes());
buf.extend_from_slice(aname);
let aname_pad = (4 - (aname.len() % 4)) % 4;
buf.extend_from_slice(&vec![0u8; aname_pad]);
buf.extend_from_slice(&attr.nc_type.to_be_bytes());
buf.extend_from_slice(&attr.nelems.to_be_bytes());
buf.extend_from_slice(&attr.payload);
}
}
buf.extend_from_slice(&3u32.to_be_bytes());
let raw_bytes = dim_size as usize * 2; let vsize = raw_bytes.div_ceil(4) * 4;
buf.extend_from_slice(&(vsize as u32).to_be_bytes());
let data_offset = buf.len() as u32 + 4;
buf.extend_from_slice(&data_offset.to_be_bytes());
for &v in values {
buf.extend_from_slice(&v.to_be_bytes());
}
let data_pad = (4 - (raw_bytes % 4)) % 4;
buf.extend_from_slice(&vec![0u8; data_pad]);
buf
}
fn build_cdf1_float_var(var_name: &str, values: &[f32], attrs: Vec<AttrSpec>) -> Vec<u8> {
let dim_size = values.len() as u32;
let mut buf = Vec::new();
buf.extend_from_slice(b"CDF\x01");
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0x0000_000Au32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes()); buf.push(b'x');
buf.extend_from_slice(&[0, 0, 0]); buf.extend_from_slice(&dim_size.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0x0000_000Bu32.to_be_bytes()); buf.extend_from_slice(&1u32.to_be_bytes());
let name_bytes = var_name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as u32).to_be_bytes());
buf.extend_from_slice(name_bytes);
let name_pad = (4 - (name_bytes.len() % 4)) % 4;
buf.extend_from_slice(&vec![0u8; name_pad]);
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes());
if attrs.is_empty() {
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
} else {
buf.extend_from_slice(&0x0000_000Cu32.to_be_bytes()); buf.extend_from_slice(&(attrs.len() as u32).to_be_bytes());
for attr in &attrs {
let aname = attr.name.as_bytes();
buf.extend_from_slice(&(aname.len() as u32).to_be_bytes());
buf.extend_from_slice(aname);
let aname_pad = (4 - (aname.len() % 4)) % 4;
buf.extend_from_slice(&vec![0u8; aname_pad]);
buf.extend_from_slice(&attr.nc_type.to_be_bytes());
buf.extend_from_slice(&attr.nelems.to_be_bytes());
buf.extend_from_slice(&attr.payload);
}
}
buf.extend_from_slice(&5u32.to_be_bytes());
let vsize = dim_size as usize * 4;
buf.extend_from_slice(&(vsize as u32).to_be_bytes());
let data_offset = buf.len() as u32 + 4;
buf.extend_from_slice(&data_offset.to_be_bytes());
for &v in values {
buf.extend_from_slice(&v.to_be_bytes());
}
buf
}
#[test]
fn test_read_i16_variable_as_f64() {
let values: Vec<i16> = vec![100, 200, -300, 0, 32767];
let data = build_cdf1_short_var("temp", &values, vec![]);
let file = NcFile::from_bytes(&data).unwrap();
assert_eq!(file.format(), NcFormat::Classic);
let arr = file.read_variable_as_f64("temp").unwrap();
assert_eq!(arr.shape(), &[5]);
assert_eq!(arr[[0]], 100.0);
assert_eq!(arr[[1]], 200.0);
assert_eq!(arr[[2]], -300.0);
assert_eq!(arr[[3]], 0.0);
assert_eq!(arr[[4]], 32767.0);
}
#[test]
fn test_read_variable_unpacked_scale_and_offset() {
let values: Vec<i16> = vec![0, 100, 1000, -500, 2000];
let attrs = vec![
double_attr("scale_factor", 0.01),
double_attr("add_offset", 273.15),
];
let data = build_cdf1_short_var("temperature", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_unpacked("temperature").unwrap();
assert_eq!(arr.shape(), &[5]);
let expected = [
0.0 * 0.01 + 273.15, 100.0 * 0.01 + 273.15, 1000.0 * 0.01 + 273.15, -500.0 * 0.01 + 273.15, 2000.0 * 0.01 + 273.15, ];
for (i, &exp) in expected.iter().enumerate() {
assert!(
(arr[[i]] - exp).abs() < 1e-10,
"index {}: got {}, expected {}",
i,
arr[[i]],
exp
);
}
}
#[test]
fn test_read_variable_masked_fill_value() {
let fill: i16 = -9999;
let values: Vec<i16> = vec![10, -9999, 30, -9999, 50];
let attrs = vec![short_attr("_FillValue", fill)];
let data = build_cdf1_short_var("obs", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_masked("obs").unwrap();
assert_eq!(arr.shape(), &[5]);
assert_eq!(arr[[0]], 10.0);
assert!(arr[[1]].is_nan(), "index 1 should be NaN (fill value)");
assert_eq!(arr[[2]], 30.0);
assert!(arr[[3]].is_nan(), "index 3 should be NaN (fill value)");
assert_eq!(arr[[4]], 50.0);
}
#[test]
fn test_read_variable_unpacked_masked_combined() {
let fill: i16 = -32768; let values: Vec<i16> = vec![1000, -32768, 2000, 3000, -32768];
let attrs = vec![
short_attr("_FillValue", fill),
double_attr("scale_factor", 0.1),
double_attr("add_offset", 20.0),
];
let data = build_cdf1_short_var("packed", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_unpacked_masked("packed").unwrap();
assert_eq!(arr.shape(), &[5]);
let expected_0 = 1000.0 * 0.1 + 20.0; let expected_2 = 2000.0 * 0.1 + 20.0; let expected_3 = 3000.0 * 0.1 + 20.0;
assert!(
(arr[[0]] - expected_0).abs() < 1e-10,
"index 0: got {}, expected {}",
arr[[0]],
expected_0
);
assert!(arr[[1]].is_nan(), "index 1 should be NaN (fill value)");
assert!(
(arr[[2]] - expected_2).abs() < 1e-10,
"index 2: got {}, expected {}",
arr[[2]],
expected_2
);
assert!(
(arr[[3]] - expected_3).abs() < 1e-10,
"index 3: got {}, expected {}",
arr[[3]],
expected_3
);
assert!(arr[[4]].is_nan(), "index 4 should be NaN (fill value)");
assert!(arr[[1]].is_nan());
}
#[test]
fn test_read_variable_slice_classic() {
let values: Vec<f32> = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
let data = build_cdf1_float_var("data", &values, vec![]);
let file = NcFile::from_bytes(&data).unwrap();
let selection = NcSliceInfo {
selections: vec![NcSliceInfoElem::Slice {
start: 1,
end: 4,
step: 1,
}],
};
let arr: ndarray::ArrayD<f32> = file.read_variable_slice("data", &selection).unwrap();
assert_eq!(arr.shape(), &[3]);
assert_eq!(arr[[0]], 20.0);
assert_eq!(arr[[1]], 30.0);
assert_eq!(arr[[2]], 40.0);
let selection_stride = NcSliceInfo {
selections: vec![NcSliceInfoElem::Slice {
start: 0,
end: 6,
step: 2,
}],
};
let arr2: ndarray::ArrayD<f32> = file.read_variable_slice("data", &selection_stride).unwrap();
assert_eq!(arr2.shape(), &[3]);
assert_eq!(arr2[[0]], 10.0);
assert_eq!(arr2[[1]], 30.0);
assert_eq!(arr2[[2]], 50.0);
let selection_idx = NcSliceInfo {
selections: vec![NcSliceInfoElem::Index(3)],
};
let arr3: ndarray::ArrayD<f32> = file.read_variable_slice("data", &selection_idx).unwrap();
assert_eq!(arr3.shape(), &[] as &[usize]);
assert_eq!(arr3[[]], 40.0);
}
#[test]
fn test_read_variable_masked_missing_value() {
let missing: i16 = -1;
let values: Vec<i16> = vec![5, -1, 15, 25, -1];
let attrs = vec![short_attr("missing_value", missing)];
let data = build_cdf1_short_var("sensor", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_masked("sensor").unwrap();
assert_eq!(arr.shape(), &[5]);
assert_eq!(arr[[0]], 5.0);
assert!(arr[[1]].is_nan());
assert_eq!(arr[[2]], 15.0);
assert_eq!(arr[[3]], 25.0);
assert!(arr[[4]].is_nan());
}
#[test]
fn test_read_variable_unpacked_scale_only() {
let values: Vec<i16> = vec![10, 20, 30];
let attrs = vec![double_attr("scale_factor", 0.5)];
let data = build_cdf1_short_var("scaled", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_unpacked("scaled").unwrap();
assert_eq!(arr.shape(), &[3]);
assert!((arr[[0]] - 5.0).abs() < 1e-10);
assert!((arr[[1]] - 10.0).abs() < 1e-10);
assert!((arr[[2]] - 15.0).abs() < 1e-10);
}
#[test]
fn test_read_variable_unpacked_offset_only() {
let values: Vec<i16> = vec![0, 100, -50];
let attrs = vec![double_attr("add_offset", 1000.0)];
let data = build_cdf1_short_var("offset_var", &values, attrs);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_unpacked("offset_var").unwrap();
assert_eq!(arr.shape(), &[3]);
assert!((arr[[0]] - 1000.0).abs() < 1e-10);
assert!((arr[[1]] - 1100.0).abs() < 1e-10);
assert!((arr[[2]] - 950.0).abs() < 1e-10);
}
#[test]
fn test_read_variable_unpacked_masked_no_attrs_returns_raw() {
let values: Vec<i16> = vec![1, 2, 3, 4];
let data = build_cdf1_short_var("plain", &values, vec![]);
let file = NcFile::from_bytes(&data).unwrap();
let arr = file.read_variable_unpacked("plain").unwrap();
assert_eq!(arr[[0]], 1.0);
assert_eq!(arr[[1]], 2.0);
assert_eq!(arr[[2]], 3.0);
assert_eq!(arr[[3]], 4.0);
let arr2 = file.read_variable_masked("plain").unwrap();
assert_eq!(arr2[[0]], 1.0);
assert_eq!(arr2[[3]], 4.0);
let arr3 = file.read_variable_unpacked_masked("plain").unwrap();
assert_eq!(arr3[[0]], 1.0);
assert_eq!(arr3[[3]], 4.0);
}