use std::collections::HashMap;
use rustyhdf5::AttrValue;
#[derive(Debug, Clone, Default)]
pub struct CfAttributes {
pub units: Option<String>,
pub long_name: Option<String>,
pub standard_name: Option<String>,
pub fill_value: Option<FillValue>,
pub missing_value: Option<FillValue>,
pub scale_factor: Option<f64>,
pub add_offset: Option<f64>,
pub valid_range: Option<(f64, f64)>,
pub calendar: Option<String>,
pub axis: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FillValue {
Float(f64),
Int(i64),
UInt(u64),
String(String),
}
impl FillValue {
pub fn as_f64(&self) -> Option<f64> {
match self {
FillValue::Float(v) => Some(*v),
FillValue::Int(v) => Some(*v as f64),
FillValue::UInt(v) => Some(*v as f64),
FillValue::String(_) => None,
}
}
}
pub fn extract_cf_attributes(attrs: &HashMap<String, AttrValue>) -> CfAttributes {
CfAttributes {
units: get_string(attrs, "units"),
long_name: get_string(attrs, "long_name"),
standard_name: get_string(attrs, "standard_name"),
fill_value: get_fill_value(attrs, "_FillValue"),
missing_value: get_fill_value(attrs, "missing_value"),
scale_factor: get_f64(attrs, "scale_factor"),
add_offset: get_f64(attrs, "add_offset"),
valid_range: get_valid_range(attrs),
calendar: get_string(attrs, "calendar"),
axis: get_string(attrs, "axis"),
}
}
pub fn apply_scale_offset(
data: &[f64],
cf: &CfAttributes,
) -> Vec<f64> {
let scale = cf.scale_factor.unwrap_or(1.0);
let offset = cf.add_offset.unwrap_or(0.0);
let has_transform = cf.scale_factor.is_some() || cf.add_offset.is_some();
if !has_transform {
return data.to_vec();
}
let fill_f64 = cf.fill_value.as_ref().and_then(|fv| fv.as_f64());
let missing_f64 = cf.missing_value.as_ref().and_then(|fv| fv.as_f64());
data.iter()
.map(|&v| {
if is_missing(v, fill_f64, missing_f64) {
f64::NAN
} else {
v * scale + offset
}
})
.collect()
}
fn is_missing(value: f64, fill: Option<f64>, missing: Option<f64>) -> bool {
if let Some(fv) = fill {
if (value - fv).abs() < f64::EPSILON || (value.is_nan() && fv.is_nan()) {
return true;
}
}
if let Some(mv) = missing {
if (value - mv).abs() < f64::EPSILON || (value.is_nan() && mv.is_nan()) {
return true;
}
}
false
}
fn get_string(attrs: &HashMap<String, AttrValue>, key: &str) -> Option<String> {
match attrs.get(key) {
Some(AttrValue::String(s)) => Some(s.clone()),
_ => None,
}
}
fn get_f64(attrs: &HashMap<String, AttrValue>, key: &str) -> Option<f64> {
match attrs.get(key) {
Some(AttrValue::F64(v)) => Some(*v),
Some(AttrValue::I64(v)) => Some(*v as f64),
Some(AttrValue::U64(v)) => Some(*v as f64),
_ => None,
}
}
fn get_fill_value(attrs: &HashMap<String, AttrValue>, key: &str) -> Option<FillValue> {
match attrs.get(key) {
Some(AttrValue::F64(v)) => Some(FillValue::Float(*v)),
Some(AttrValue::I64(v)) => Some(FillValue::Int(*v)),
Some(AttrValue::U64(v)) => Some(FillValue::UInt(*v)),
Some(AttrValue::String(s)) => Some(FillValue::String(s.clone())),
Some(AttrValue::F64Array(arr)) if !arr.is_empty() => Some(FillValue::Float(arr[0])),
Some(AttrValue::I64Array(arr)) if !arr.is_empty() => Some(FillValue::Int(arr[0])),
_ => None,
}
}
fn get_valid_range(attrs: &HashMap<String, AttrValue>) -> Option<(f64, f64)> {
match attrs.get("valid_range") {
Some(AttrValue::F64Array(arr)) if arr.len() >= 2 => {
return Some((arr[0], arr[1]));
}
Some(AttrValue::I64Array(arr)) if arr.len() >= 2 => {
return Some((arr[0] as f64, arr[1] as f64));
}
_ => {}
}
let min = get_f64(attrs, "valid_min");
let max = get_f64(attrs, "valid_max");
match (min, max) {
(Some(lo), Some(hi)) => Some((lo, hi)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_cf_empty() {
let attrs = HashMap::new();
let cf = extract_cf_attributes(&attrs);
assert!(cf.units.is_none());
assert!(cf.scale_factor.is_none());
assert!(cf.add_offset.is_none());
}
#[test]
fn test_extract_cf_full() {
let mut attrs = HashMap::new();
attrs.insert("units".into(), AttrValue::String("K".into()));
attrs.insert("long_name".into(), AttrValue::String("Temperature".into()));
attrs.insert(
"standard_name".into(),
AttrValue::String("air_temperature".into()),
);
attrs.insert("_FillValue".into(), AttrValue::F64(-9999.0));
attrs.insert("scale_factor".into(), AttrValue::F64(0.01));
attrs.insert("add_offset".into(), AttrValue::F64(273.15));
attrs.insert("calendar".into(), AttrValue::String("standard".into()));
attrs.insert("axis".into(), AttrValue::String("T".into()));
let cf = extract_cf_attributes(&attrs);
assert_eq!(cf.units.as_deref(), Some("K"));
assert_eq!(cf.long_name.as_deref(), Some("Temperature"));
assert_eq!(cf.standard_name.as_deref(), Some("air_temperature"));
assert_eq!(cf.fill_value, Some(FillValue::Float(-9999.0)));
assert_eq!(cf.scale_factor, Some(0.01));
assert_eq!(cf.add_offset, Some(273.15));
assert_eq!(cf.calendar.as_deref(), Some("standard"));
assert_eq!(cf.axis.as_deref(), Some("T"));
}
#[test]
fn test_apply_scale_offset_no_transform() {
let data = vec![1.0, 2.0, 3.0];
let cf = CfAttributes::default();
let result = apply_scale_offset(&data, &cf);
assert_eq!(result, data);
}
#[test]
fn test_apply_scale_offset_with_transform() {
let data = vec![100.0, 200.0, 300.0];
let cf = CfAttributes {
scale_factor: Some(0.01),
add_offset: Some(273.15),
..Default::default()
};
let result = apply_scale_offset(&data, &cf);
assert!((result[0] - 274.15).abs() < 1e-10);
assert!((result[1] - 275.15).abs() < 1e-10);
assert!((result[2] - 276.15).abs() < 1e-10);
}
#[test]
fn test_apply_scale_offset_with_fill() {
let data = vec![100.0, -9999.0, 300.0];
let cf = CfAttributes {
scale_factor: Some(0.01),
add_offset: Some(273.15),
fill_value: Some(FillValue::Float(-9999.0)),
..Default::default()
};
let result = apply_scale_offset(&data, &cf);
assert!((result[0] - 274.15).abs() < 1e-10);
assert!(result[1].is_nan());
assert!((result[2] - 276.15).abs() < 1e-10);
}
#[test]
fn test_valid_range_array() {
let mut attrs = HashMap::new();
attrs.insert(
"valid_range".into(),
AttrValue::F64Array(vec![0.0, 100.0]),
);
let cf = extract_cf_attributes(&attrs);
assert_eq!(cf.valid_range, Some((0.0, 100.0)));
}
#[test]
fn test_valid_range_min_max() {
let mut attrs = HashMap::new();
attrs.insert("valid_min".into(), AttrValue::F64(-10.0));
attrs.insert("valid_max".into(), AttrValue::F64(50.0));
let cf = extract_cf_attributes(&attrs);
assert_eq!(cf.valid_range, Some((-10.0, 50.0)));
}
#[test]
fn test_fill_value_int() {
let mut attrs = HashMap::new();
attrs.insert("_FillValue".into(), AttrValue::I64(-999));
let cf = extract_cf_attributes(&attrs);
assert_eq!(cf.fill_value, Some(FillValue::Int(-999)));
assert_eq!(cf.fill_value.unwrap().as_f64(), Some(-999.0));
}
}