use ndarray::ArrayD;
use serde_json::{Map, Value};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CfEncoding {
pub fill_value: Option<f64>,
pub scale_factor: Option<f64>,
pub add_offset: Option<f64>,
}
pub fn parse_cf_encoding(
attributes: &Map<String, Value>,
zarr_fill_value: Option<&Value>,
) -> CfEncoding {
CfEncoding {
fill_value: resolve_fill_value(zarr_fill_value, attributes),
scale_factor: parse_attr_number(attributes, "scale_factor"),
add_offset: parse_attr_number(attributes, "add_offset"),
}
}
pub fn apply_cf_decode(values: &ArrayD<f64>, encoding: &CfEncoding) -> ArrayD<f64> {
let scale = encoding.scale_factor.unwrap_or(1.0);
let offset = encoding.add_offset.unwrap_or(0.0);
let has_transform = encoding.scale_factor.is_some() || encoding.add_offset.is_some();
values.mapv(|value| {
if !value.is_finite() {
return f64::NAN;
}
if is_fill_value(value, encoding.fill_value) {
return f64::NAN;
}
if has_transform {
scale * value + offset
} else {
value
}
})
}
pub fn resolve_fill_value(
zarr_fill_value: Option<&Value>,
attributes: &Map<String, Value>,
) -> Option<f64> {
if let Some(value) = zarr_fill_value
&& let Some(parsed) = parse_fill_value_from_json(value)
{
return Some(parsed);
}
attributes
.get("_FillValue")
.or_else(|| attributes.get("fill_value"))
.and_then(parse_fill_value_from_json)
}
pub fn parse_fill_value(attributes: &Map<String, Value>) -> Option<f64> {
resolve_fill_value(None, attributes)
}
fn parse_fill_value_from_json(value: &Value) -> Option<f64> {
parse_json_number(value)
}
pub fn is_fill_value(value: f64, fill_value: Option<f64>) -> bool {
fill_value
.map(|fill| (value - fill).abs() <= 1e-9)
.unwrap_or(false)
}
fn parse_attr_number(attributes: &Map<String, Value>, key: &str) -> Option<f64> {
attributes.get(key).and_then(parse_json_number)
}
fn parse_json_number(value: &Value) -> Option<f64> {
match value {
Value::Number(number) => number.as_f64(),
Value::String(text) => text.parse().ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
use serde_json::json;
#[test]
fn prefers_zarr_fill_value_over_attributes() {
let attrs = Map::new();
assert_eq!(
resolve_fill_value(Some(&json!(65535)), &attrs),
Some(65535.0)
);
}
#[test]
fn falls_back_to_attributes_when_zarr_fill_missing() {
let attrs = json!({ "_FillValue": 255 }).as_object().unwrap().clone();
assert_eq!(resolve_fill_value(None, &attrs), Some(255.0));
}
#[test]
fn parses_fill_value_from_either_attribute_key() {
let attrs = json!({ "_FillValue": 255 }).as_object().unwrap().clone();
assert_eq!(parse_fill_value(&attrs), Some(255.0));
let attrs = json!({ "fill_value": -999 }).as_object().unwrap().clone();
assert_eq!(parse_fill_value(&attrs), Some(-999.0));
}
#[test]
fn masks_fill_and_applies_scale_offset() {
let encoding = CfEncoding {
fill_value: Some(65535.0),
scale_factor: Some(0.01),
add_offset: Some(273.15),
};
let values =
ArrayD::from_shape_vec(IxDyn(&[2, 2]), vec![0.0, 100.0, 200.0, 65535.0]).unwrap();
let decoded = apply_cf_decode(&values, &encoding);
assert!((decoded[[0, 0]] - 273.15).abs() < 1e-9);
assert!((decoded[[0, 1]] - 274.15).abs() < 1e-9);
assert!((decoded[[1, 0]] - 275.15).abs() < 1e-9);
assert!(decoded[[1, 1]].is_nan());
}
#[test]
fn masks_fill_without_transform() {
let encoding = CfEncoding {
fill_value: Some(255.0),
scale_factor: None,
add_offset: None,
};
let values = ArrayD::from_shape_vec(IxDyn(&[3]), vec![1.0, 255.0, 4.0]).unwrap();
let decoded = apply_cf_decode(&values, &encoding);
assert_eq!(decoded[[0]], 1.0);
assert!(decoded[[1]].is_nan());
assert_eq!(decoded[[2]], 4.0);
}
#[test]
fn preserves_existing_nan() {
let encoding = CfEncoding {
fill_value: None,
scale_factor: Some(2.0),
add_offset: Some(1.0),
};
let values = ArrayD::from_shape_vec(IxDyn(&[2]), vec![f64::NAN, 3.0]).unwrap();
let decoded = apply_cf_decode(&values, &encoding);
assert!(decoded[[0]].is_nan());
assert!((decoded[[1]] - 7.0).abs() < 1e-9);
}
}