use arrow::array::{Array, StringArray};
use cobre_core::EntityId;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::fs::File;
use std::path::Path;
use crate::LoadError;
use crate::parquet_helpers::{
extract_optional_int32, extract_required_float64, extract_required_int32,
};
#[derive(Debug, Clone, PartialEq)]
pub struct EvaporationModelRow {
pub hydro_id: EntityId,
pub stage_id: Option<i32>,
pub intercept_m3s: f64,
pub volume_slope_m3s_per_hm3: f64,
pub reference_volume_hm3: f64,
pub source: String,
}
pub fn parse_evaporation_models(path: &Path) -> Result<Vec<EvaporationModelRow>, LoadError> {
let file = File::open(path).map_err(|e| LoadError::io(path, e))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| LoadError::parse(path, e.to_string()))?;
let reader = builder
.build()
.map_err(|e| LoadError::parse(path, e.to_string()))?;
let mut rows: Vec<EvaporationModelRow> = Vec::new();
for batch_result in reader {
let batch = batch_result.map_err(|e| LoadError::parse(path, e.to_string()))?;
let hydro_id_col = extract_required_int32(&batch, "hydro_id", path)?;
let intercept_col = extract_required_float64(&batch, "intercept_m3s", path)?;
let volume_slope_col = extract_required_float64(&batch, "volume_slope_m3s_per_hm3", path)?;
let reference_volume_col = extract_required_float64(&batch, "reference_volume_hm3", path)?;
let source_col = extract_required_string(&batch, "source", path)?;
let stage_id_col = extract_optional_int32(&batch, "stage_id", path)?;
let n = batch.num_rows();
rows.reserve(n);
for i in 0..n {
let hydro_id = EntityId::from(hydro_id_col.value(i));
let intercept_m3s = intercept_col.value(i);
let volume_slope_m3s_per_hm3 = volume_slope_col.value(i);
let reference_volume_hm3 = reference_volume_col.value(i);
for (value, column) in [
(intercept_m3s, "intercept_m3s"),
(volume_slope_m3s_per_hm3, "volume_slope_m3s_per_hm3"),
(reference_volume_hm3, "reference_volume_hm3"),
] {
if !value.is_finite() {
return Err(LoadError::SchemaError {
path: path.to_path_buf(),
field: format!("evaporation_models[{i}].{column}"),
message: format!("value must be finite, got {value}"),
});
}
}
let source = source_col.value(i).to_string();
let stage_id = stage_id_col
.filter(|col| !col.is_null(i))
.map(|col| col.value(i));
rows.push(EvaporationModelRow {
hydro_id,
stage_id,
intercept_m3s,
volume_slope_m3s_per_hm3,
reference_volume_hm3,
source,
});
}
}
rows.sort_by(|a, b| {
a.hydro_id
.0
.cmp(&b.hydro_id.0)
.then_with(|| a.stage_id.cmp(&b.stage_id))
});
Ok(rows)
}
fn extract_required_string<'a>(
batch: &'a arrow::record_batch::RecordBatch,
name: &str,
path: &Path,
) -> Result<&'a StringArray, LoadError> {
let col = batch
.column_by_name(name)
.ok_or_else(|| LoadError::SchemaError {
path: path.to_path_buf(),
field: name.to_string(),
message: format!("missing required column \"{name}\""),
})?;
col.as_any()
.downcast_ref::<StringArray>()
.ok_or_else(|| LoadError::SchemaError {
path: path.to_path_buf(),
field: name.to_string(),
message: format!(
"column \"{name}\" has type {} but Utf8 is required",
col.data_type()
),
})
}