use std::collections::HashSet;
use std::path::Path;
use netcdf_reader::{NcFile, NcType, NcVariable};
use crate::error::{Error, Result};
use super::{ArrayData, Coord, DType, NativeDataset, NativeField, Reader, Selection};
#[derive(Debug, Default, Clone, Copy)]
pub struct NetcdfReader;
impl NetcdfReader {
pub fn new() -> Self {
Self
}
}
impl Reader for NetcdfReader {
fn formats(&self) -> &'static [&'static str] {
&["netcdf"]
}
fn extensions(&self) -> &'static [&'static str] {
&["nc", "nc4", "cdf"]
}
fn read_native(
&self,
blob_path: &Path,
variables: &[String],
_select: &Selection,
) -> Result<NativeDataset> {
let file = NcFile::open(blob_path).map_err(fmt_err)?;
decode(&file, variables)
}
}
fn decode(file: &NcFile, variables: &[String]) -> Result<NativeDataset> {
let vars: Vec<NcVariable> = file.variables().map_err(fmt_err)?.to_vec();
let want: HashSet<&str> = variables.iter().map(String::as_str).collect();
let mut out = NativeDataset::default();
for var in &vars {
let is_coord = var.is_coordinate_variable();
if !is_coord && !want.is_empty() && !want.contains(var.name()) {
continue;
}
let Some(class) = classify(var) else { continue };
let field = decode_field(file, var, class)?;
if is_coord {
out.coords.insert(
var.name().to_string(),
Coord {
field,
units: att_text(var, "units"),
calendar: att_text(var, "calendar"),
},
);
} else {
out.variables.insert(var.name().to_string(), field);
}
}
Ok(out)
}
enum FieldClass {
Float,
Int32,
Int64,
}
fn classify(var: &NcVariable) -> Option<FieldClass> {
if var.attribute("scale_factor").is_some() || var.attribute("add_offset").is_some() {
return Some(FieldClass::Float);
}
match var.dtype() {
NcType::Float | NcType::Double => Some(FieldClass::Float),
NcType::Byte | NcType::Short | NcType::Int | NcType::UByte | NcType::UShort => {
Some(FieldClass::Int32)
}
NcType::UInt | NcType::Int64 | NcType::UInt64 => Some(FieldClass::Int64),
_ => None,
}
}
fn decode_field(file: &NcFile, var: &NcVariable, class: FieldClass) -> Result<NativeField> {
let dims: Vec<String> = var.dimensions().iter().map(|d| d.name.clone()).collect();
let shape: Vec<usize> = var.dimensions().iter().map(|d| d.size as usize).collect();
let name = var.name();
match class {
FieldClass::Float => {
let arr = file.read_variable_unpacked_masked(name).map_err(fmt_err)?;
Ok(NativeField {
dtype: DType::Float64,
dims,
shape,
data: ArrayData::F64(arr.iter().copied().collect()),
fill_value: None, })
}
FieldClass::Int32 => {
let raw = file.read_variable_as_f64(name).map_err(fmt_err)?;
Ok(NativeField {
dtype: DType::Int32,
dims,
shape,
data: ArrayData::I32(raw.iter().map(|&v| v as i32).collect()),
fill_value: int_fill(var),
})
}
FieldClass::Int64 => {
let raw = file.read_variable_as_f64(name).map_err(fmt_err)?;
Ok(NativeField {
dtype: DType::Int64,
dims,
shape,
data: ArrayData::I64(raw.iter().map(|&v| v as i64).collect()),
fill_value: int_fill(var),
})
}
}
}
fn int_fill(var: &NcVariable) -> Option<f64> {
att_f64(var, "_FillValue").or_else(|| att_f64(var, "missing_value"))
}
fn att_f64(var: &NcVariable, name: &str) -> Option<f64> {
var.attribute(name).and_then(|a| a.value.as_f64())
}
fn att_text(var: &NcVariable, name: &str) -> Option<String> {
var.attribute(name).and_then(|a| a.value.as_string())
}
fn fmt_err(e: netcdf_reader::Error) -> Error {
Error::Format {
format: "netcdf".to_string(),
detail: e.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn read_bytes(bytes: &[u8]) -> Result<NativeDataset> {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
NetcdfReader::new().read_native(f.path(), &[], &Selection::All)
}
#[test]
fn rejects_bad_magic() {
let err = read_bytes(b"NOPE, not a netcdf file at all").unwrap_err();
assert!(matches!(err, Error::Format { .. }));
}
#[test]
fn truncated_classic_file_is_an_error_not_a_panic() {
let err = read_bytes(b"CDF\x01\x00\x00").unwrap_err();
assert!(matches!(err, Error::Format { .. }));
}
}