use std::path::Path;
use std::sync::Arc;
use arrow::array::{Date32Builder, Float64Builder, Int32Builder, RecordBatch};
use chrono::NaiveDate;
use super::date32_days;
use crate::output::atomic::write_parquet_atomic;
use crate::output::error::OutputError;
use crate::output::parquet_config::ParquetWriterConfig;
use crate::output::schemas::fixed_delivery_schema;
use crate::output::stochastic::ensure_parent_dir;
#[derive(Debug, Clone)]
pub struct FixedDeliveryRow {
pub thermal_id: i32,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub value_mw: f64,
}
pub fn write_fixed_delivery(
output_dir: &Path,
rows: &[FixedDeliveryRow],
) -> Result<(), OutputError> {
if rows.is_empty() {
return Ok(());
}
let path = output_dir
.join("anticipated")
.join("fixed_deliveries.parquet");
ensure_parent_dir(&path)?;
let config = ParquetWriterConfig::default();
let batch = build_fixed_delivery_batch(rows)?;
write_parquet_atomic(&path, &batch, &config)
}
fn build_fixed_delivery_batch(rows: &[FixedDeliveryRow]) -> Result<RecordBatch, OutputError> {
let n = rows.len();
let mut thermal_id_col = Int32Builder::with_capacity(n);
let mut start_date_col = Date32Builder::with_capacity(n);
let mut end_date_col = Date32Builder::with_capacity(n);
let mut value_mw_col = Float64Builder::with_capacity(n);
for row in rows {
thermal_id_col.append_value(row.thermal_id);
start_date_col.append_value(date32_days(row.start_date));
end_date_col.append_value(date32_days(row.end_date));
value_mw_col.append_value(row.value_mw);
}
RecordBatch::try_new(
Arc::new(fixed_delivery_schema()),
vec![
Arc::new(thermal_id_col.finish()),
Arc::new(start_date_col.finish()),
Arc::new(end_date_col.finish()),
Arc::new(value_mw_col.finish()),
],
)
.map_err(|e| OutputError::serialization("fixed_delivery", e.to_string()))
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::float_cmp, clippy::unwrap_used)]
mod tests {
use super::*;
use arrow::array::{Date32Array, Float64Array, Int32Array};
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use tempfile::tempdir;
fn sample_rows() -> Vec<FixedDeliveryRow> {
vec![
FixedDeliveryRow {
thermal_id: 3,
start_date: NaiveDate::from_ymd_opt(2030, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2030, 6, 30).unwrap(),
value_mw: 120.5,
},
FixedDeliveryRow {
thermal_id: 7,
start_date: NaiveDate::from_ymd_opt(2031, 7, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2031, 12, 31).unwrap(),
value_mw: 88.0,
},
]
}
fn read_batch(path: &Path) -> RecordBatch {
let file = std::fs::File::open(path).unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
let mut reader = builder.build().unwrap();
reader.next().unwrap().unwrap()
}
#[test]
fn fixed_delivery_round_trip_preserves_rows_in_order() {
let rows = sample_rows();
let root = tempdir().expect("tempdir");
write_fixed_delivery(root.path(), &rows).expect("write must succeed");
let path = root
.path()
.join("anticipated")
.join("fixed_deliveries.parquet");
assert!(path.exists(), "file must exist after non-empty write");
let batch = read_batch(&path);
assert_eq!(batch.num_columns(), 4, "must have 4 columns");
assert_eq!(batch.num_rows(), rows.len(), "one row per input record");
let thermal_id = batch
.column_by_name("thermal_id")
.unwrap()
.as_any()
.downcast_ref::<Int32Array>()
.unwrap();
let start_date = batch
.column_by_name("start_date")
.unwrap()
.as_any()
.downcast_ref::<Date32Array>()
.unwrap();
let end_date = batch
.column_by_name("end_date")
.unwrap()
.as_any()
.downcast_ref::<Date32Array>()
.unwrap();
let value_mw = batch
.column_by_name("value_mw")
.unwrap()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap();
for (i, row) in rows.iter().enumerate() {
assert_eq!(thermal_id.value(i), row.thermal_id);
assert_eq!(start_date.value(i), date32_days(row.start_date));
assert_eq!(end_date.value(i), date32_days(row.end_date));
assert!(value_mw.value(i) == row.value_mw);
}
}
#[test]
fn fixed_delivery_empty_slice_writes_no_file_and_no_directory() {
let root = tempdir().expect("tempdir");
write_fixed_delivery(root.path(), &[]).expect("empty write must succeed");
let anticipated_dir = root.path().join("anticipated");
assert!(
!anticipated_dir.exists(),
"empty slice must not create the anticipated/ directory"
);
assert!(
!anticipated_dir.join("fixed_deliveries.parquet").exists(),
"empty slice must not write the parquet file"
);
}
}