use std::collections::BTreeMap;
use ciborium::Value as CborValue;
use netcdf::AttributeValue;
pub(crate) const CF_ATTRIBUTES: &[&str] = &[
"standard_name",
"long_name",
"units",
"calendar",
"cell_methods",
"coordinates",
"axis",
"positive",
"valid_min",
"valid_max",
"valid_range",
"bounds",
"grid_mapping",
"ancillary_variables",
"flag_values",
"flag_meanings",
];
pub(crate) fn attr_value_to_cbor(val: &AttributeValue) -> CborValue {
match val {
AttributeValue::Str(s) => CborValue::Text(s.clone()),
AttributeValue::Strs(v) => {
CborValue::Array(v.iter().map(|s| CborValue::Text(s.clone())).collect())
}
AttributeValue::Double(f) => CborValue::Float(*f),
AttributeValue::Doubles(v) => {
CborValue::Array(v.iter().map(|f| CborValue::Float(*f)).collect())
}
AttributeValue::Float(f) => CborValue::Float(*f as f64),
AttributeValue::Floats(v) => {
CborValue::Array(v.iter().map(|f| CborValue::Float(*f as f64)).collect())
}
AttributeValue::Int(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Ints(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
AttributeValue::Short(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Shorts(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
AttributeValue::Ushort(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Ushorts(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
AttributeValue::Uint(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Uints(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
AttributeValue::Longlong(i) => CborValue::Integer((*i).into()),
AttributeValue::Longlongs(v) => {
CborValue::Array(v.iter().map(|i| CborValue::Integer((*i).into())).collect())
}
AttributeValue::Ulonglong(i) => CborValue::Integer((*i).into()),
AttributeValue::Ulonglongs(v) => {
CborValue::Array(v.iter().map(|i| CborValue::Integer((*i).into())).collect())
}
AttributeValue::Schar(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Schars(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
AttributeValue::Uchar(i) => CborValue::Integer((*i as i64).into()),
AttributeValue::Uchars(v) => CborValue::Array(
v.iter()
.map(|i| CborValue::Integer((*i as i64).into()))
.collect(),
),
}
}
pub(crate) fn attr_value_type_tag(val: &AttributeValue) -> &'static str {
match val {
AttributeValue::Str(_) | AttributeValue::Strs(_) => "string",
AttributeValue::Double(_) | AttributeValue::Doubles(_) => "double",
AttributeValue::Float(_) | AttributeValue::Floats(_) => "float",
AttributeValue::Int(_) | AttributeValue::Ints(_) => "int",
AttributeValue::Uint(_) | AttributeValue::Uints(_) => "uint",
AttributeValue::Short(_) | AttributeValue::Shorts(_) => "short",
AttributeValue::Ushort(_) | AttributeValue::Ushorts(_) => "ushort",
AttributeValue::Longlong(_) | AttributeValue::Longlongs(_) => "int64",
AttributeValue::Ulonglong(_) | AttributeValue::Ulonglongs(_) => "uint64",
AttributeValue::Schar(_) | AttributeValue::Schars(_) => "byte",
AttributeValue::Uchar(_) | AttributeValue::Uchars(_) => "ubyte",
}
}
pub(crate) fn extract_var_attr_types(var: &netcdf::Variable<'_>) -> BTreeMap<String, CborValue> {
let mut map = BTreeMap::new();
for attr in var.attributes() {
if let Ok(val) = attr.value() {
map.insert(
attr.name().to_string(),
CborValue::Text(attr_value_type_tag(&val).to_string()),
);
}
}
map
}
pub(crate) fn extract_var_attrs(var: &netcdf::Variable<'_>) -> BTreeMap<String, CborValue> {
let mut map = BTreeMap::new();
let var_name = var.name();
for attr in var.attributes() {
let name = attr.name();
match attr.value() {
Ok(val) => {
map.insert(name.to_string(), attr_value_to_cbor(&val));
}
Err(e) => {
eprintln!("warning: variable '{var_name}': failed to read attribute '{name}': {e}");
}
}
}
map
}
pub(crate) fn extract_cf_attrs(var: &netcdf::Variable<'_>) -> BTreeMap<String, CborValue> {
let mut map = BTreeMap::new();
let var_name = var.name();
for attr in var.attributes() {
let name = attr.name();
if !CF_ATTRIBUTES.contains(&name) {
continue;
}
match attr.value() {
Ok(val) => {
map.insert(name.to_string(), attr_value_to_cbor(&val));
}
Err(e) => {
eprintln!(
"warning: variable '{var_name}': failed to read CF attribute '{name}': {e}"
);
}
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
fn expect_text(val: &CborValue, want: &str) {
match val {
CborValue::Text(s) => assert_eq!(s, want),
other => panic!("expected Text, got {other:?}"),
}
}
fn expect_float(val: &CborValue, want: f64) {
match val {
CborValue::Float(f) => assert!((*f - want).abs() < 1e-12, "expected {want}, got {f}"),
other => panic!("expected Float, got {other:?}"),
}
}
fn expect_int(val: &CborValue, want: i64) {
match val {
CborValue::Integer(i) => {
let n: i128 = (*i).into();
assert_eq!(n, want as i128);
}
other => panic!("expected Integer, got {other:?}"),
}
}
fn expect_array_len(val: &CborValue, want: usize) -> &Vec<CborValue> {
match val {
CborValue::Array(arr) => {
assert_eq!(arr.len(), want);
arr
}
other => panic!("expected Array, got {other:?}"),
}
}
#[test]
fn attr_value_to_cbor_str() {
expect_text(
&attr_value_to_cbor(&AttributeValue::Str("K".to_string())),
"K",
);
}
#[test]
fn attr_value_to_cbor_strs() {
let v = attr_value_to_cbor(&AttributeValue::Strs(vec![
"a".to_string(),
"bb".to_string(),
]));
let arr = expect_array_len(&v, 2);
expect_text(&arr[0], "a");
expect_text(&arr[1], "bb");
}
#[test]
fn attr_value_to_cbor_double() {
expect_float(&attr_value_to_cbor(&AttributeValue::Double(273.15)), 273.15);
}
#[test]
fn attr_value_to_cbor_doubles() {
let v = attr_value_to_cbor(&AttributeValue::Doubles(vec![1.0, -2.5, 3.75]));
let arr = expect_array_len(&v, 3);
expect_float(&arr[0], 1.0);
expect_float(&arr[1], -2.5);
expect_float(&arr[2], 3.75);
}
#[test]
fn attr_value_to_cbor_float() {
expect_float(&attr_value_to_cbor(&AttributeValue::Float(2.5_f32)), 2.5);
}
#[test]
fn attr_value_to_cbor_floats() {
let v = attr_value_to_cbor(&AttributeValue::Floats(vec![1.0_f32, 2.0_f32]));
let arr = expect_array_len(&v, 2);
expect_float(&arr[0], 1.0);
expect_float(&arr[1], 2.0);
}
#[test]
fn attr_value_to_cbor_int() {
expect_int(&attr_value_to_cbor(&AttributeValue::Int(-42_i32)), -42);
}
#[test]
fn attr_value_to_cbor_ints() {
let v = attr_value_to_cbor(&AttributeValue::Ints(vec![1_i32, -1_i32, 999_i32]));
let arr = expect_array_len(&v, 3);
expect_int(&arr[0], 1);
expect_int(&arr[1], -1);
expect_int(&arr[2], 999);
}
#[test]
fn attr_value_to_cbor_short() {
expect_int(
&attr_value_to_cbor(&AttributeValue::Short(-32768_i16)),
-32768,
);
}
#[test]
fn attr_value_to_cbor_shorts() {
let v = attr_value_to_cbor(&AttributeValue::Shorts(vec![0_i16, -1_i16]));
let arr = expect_array_len(&v, 2);
expect_int(&arr[0], 0);
expect_int(&arr[1], -1);
}
#[test]
fn attr_value_to_cbor_ushort() {
expect_int(
&attr_value_to_cbor(&AttributeValue::Ushort(65535_u16)),
65535,
);
}
#[test]
fn attr_value_to_cbor_ushorts() {
let v = attr_value_to_cbor(&AttributeValue::Ushorts(vec![1_u16, 65535_u16]));
let arr = expect_array_len(&v, 2);
expect_int(&arr[0], 1);
expect_int(&arr[1], 65535);
}
#[test]
fn attr_value_to_cbor_uint() {
expect_int(
&attr_value_to_cbor(&AttributeValue::Uint(4_000_000_000_u32)),
4_000_000_000,
);
}
#[test]
fn attr_value_to_cbor_uints() {
let v = attr_value_to_cbor(&AttributeValue::Uints(vec![0_u32, 1_000_000_u32]));
let arr = expect_array_len(&v, 2);
expect_int(&arr[0], 0);
expect_int(&arr[1], 1_000_000);
}
#[test]
fn attr_value_to_cbor_longlong() {
expect_int(
&attr_value_to_cbor(&AttributeValue::Longlong(i64::MIN)),
i64::MIN,
);
}
#[test]
fn attr_value_to_cbor_longlongs() {
let v = attr_value_to_cbor(&AttributeValue::Longlongs(vec![0_i64, i64::MAX, i64::MIN]));
let arr = expect_array_len(&v, 3);
expect_int(&arr[0], 0);
expect_int(&arr[1], i64::MAX);
expect_int(&arr[2], i64::MIN);
}
#[test]
fn attr_value_to_cbor_ulonglong() {
expect_int(
&attr_value_to_cbor(&AttributeValue::Ulonglong(12345_u64)),
12345,
);
}
#[test]
fn attr_value_to_cbor_ulonglong_above_i64_max() {
let large = (i64::MAX as u64) + 42;
let cbor = attr_value_to_cbor(&AttributeValue::Ulonglong(large));
match cbor {
CborValue::Integer(i) => {
let n: i128 = i.into();
assert_eq!(n, large as i128);
assert!(n > 0, "u64 > i64::MAX must not wrap to negative");
}
other => panic!("expected Integer, got {other:?}"),
}
}
#[test]
fn attr_value_to_cbor_ulonglongs() {
let v = attr_value_to_cbor(&AttributeValue::Ulonglongs(vec![
0_u64,
42_u64,
1_000_000_u64,
u64::MAX,
]));
let arr = expect_array_len(&v, 4);
expect_int(&arr[0], 0);
expect_int(&arr[1], 42);
expect_int(&arr[2], 1_000_000);
match &arr[3] {
CborValue::Integer(i) => {
let n: i128 = (*i).into();
assert_eq!(n, u64::MAX as i128);
}
other => panic!("expected Integer for u64::MAX, got {other:?}"),
}
}
#[test]
fn attr_value_to_cbor_schar() {
expect_int(&attr_value_to_cbor(&AttributeValue::Schar(-128_i8)), -128);
}
#[test]
fn attr_value_to_cbor_schars() {
let v = attr_value_to_cbor(&AttributeValue::Schars(vec![-1_i8, 0_i8, 127_i8]));
let arr = expect_array_len(&v, 3);
expect_int(&arr[0], -1);
expect_int(&arr[1], 0);
expect_int(&arr[2], 127);
}
#[test]
fn attr_value_to_cbor_uchar() {
expect_int(&attr_value_to_cbor(&AttributeValue::Uchar(255_u8)), 255);
}
#[test]
fn attr_value_to_cbor_uchars() {
let v = attr_value_to_cbor(&AttributeValue::Uchars(vec![0_u8, 128_u8, 255_u8]));
let arr = expect_array_len(&v, 3);
expect_int(&arr[0], 0);
expect_int(&arr[1], 128);
expect_int(&arr[2], 255);
}
#[test]
fn attr_value_type_tags_cover_every_variant() {
use AttributeValue::*;
let cases: &[(AttributeValue, &str)] = &[
(Str("x".into()), "string"),
(Strs(vec!["a".into()]), "string"),
(Double(1.0), "double"),
(Doubles(vec![1.0]), "double"),
(Float(1.0), "float"),
(Floats(vec![1.0]), "float"),
(Int(1), "int"),
(Ints(vec![1]), "int"),
(Uint(1), "uint"),
(Uints(vec![1]), "uint"),
(Short(1), "short"),
(Shorts(vec![1]), "short"),
(Ushort(1), "ushort"),
(Ushorts(vec![1]), "ushort"),
(Longlong(1), "int64"),
(Longlongs(vec![1]), "int64"),
(Ulonglong(1), "uint64"),
(Ulonglongs(vec![1]), "uint64"),
(Schar(1), "byte"),
(Schars(vec![1]), "byte"),
(Uchar(1), "ubyte"),
(Uchars(vec![1]), "ubyte"),
];
for (val, want) in cases {
assert_eq!(attr_value_type_tag(val), *want, "tag for {val:?}");
}
}
#[test]
fn cf_attributes_contains_all_16() {
assert_eq!(CF_ATTRIBUTES.len(), 16);
for expected in &[
"standard_name",
"long_name",
"units",
"calendar",
"cell_methods",
"coordinates",
"axis",
"positive",
"valid_min",
"valid_max",
"valid_range",
"bounds",
"grid_mapping",
"ancillary_variables",
"flag_values",
"flag_meanings",
] {
assert!(
CF_ATTRIBUTES.contains(expected),
"CF_ATTRIBUTES should contain '{expected}'"
);
}
}
}