From 3e892c24899b6126c2de7a7dc5097d5c56c99837 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 20 Mar 2025 13:37:26 +0000 Subject: [PATCH 1/3] Separate functions for allowing/disallowing empty csv files --- src/input.rs | 26 +++++++++++++++++++++++++- src/input/agent/search_space.rs | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/input.rs b/src/input.rs index 400bdb19..052b5a24 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,7 @@ //! Common routines for handling input data. use crate::agent::AssetPool; use crate::model::{Model, ModelFile}; -use anyhow::{ensure, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use float_cmp::approx_eq; use indexmap::IndexMap; use itertools::Itertools; @@ -26,11 +26,31 @@ pub use time_slice::read_time_slice_info; /// Read a series of type `T`s from a CSV file. /// +/// Will raise an error if the file is empty. +/// /// # Arguments /// /// * `file_path` - Path to the CSV file pub fn read_csv<'a, T: DeserializeOwned + 'a>( file_path: &'a Path, +) -> Result + 'a> { + _read_csv_internal(file_path, true) +} + +/// Read a series of type `T`s from a CSV file. +/// +/// # Arguments +/// +/// * `file_path` - Path to the CSV file +pub fn read_csv_optional<'a, T: DeserializeOwned + 'a>( + file_path: &'a Path, +) -> Result + 'a> { + _read_csv_internal(file_path, false) +} + +fn _read_csv_internal<'a, T: DeserializeOwned + 'a>( + file_path: &'a Path, + check_empty: bool, ) -> Result + 'a> { let vec = csv::Reader::from_path(file_path) .with_context(|| input_err_msg(file_path))? @@ -38,6 +58,10 @@ pub fn read_csv<'a, T: DeserializeOwned + 'a>( .process_results(|iter| iter.collect_vec()) .with_context(|| input_err_msg(file_path))?; + if check_empty && vec.is_empty() { + bail!("CSV file {} cannot be empty", file_path.display()); + } + Ok(vec.into_iter()) } diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs index 0f19f805..ad1ac477 100644 --- a/src/input/agent/search_space.rs +++ b/src/input/agent/search_space.rs @@ -81,7 +81,7 @@ pub fn read_agent_search_space( milestone_years: &[u32], ) -> Result, Vec>> { let file_path = model_dir.join(AGENT_SEARCH_SPACE_FILE_NAME); - let iter = read_csv::(&file_path)?; + let iter = read_csv_optional::(&file_path)?; read_agent_search_space_from_iter(iter, agents, process_ids, commodities, milestone_years) .with_context(|| input_err_msg(&file_path)) } From ba94748efda08baf24262a972446159093a0db55 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 20 Mar 2025 13:41:56 +0000 Subject: [PATCH 2/3] Add test --- src/input.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/input.rs b/src/input.rs index 052b5a24..8bbef276 100644 --- a/src/input.rs +++ b/src/input.rs @@ -309,6 +309,14 @@ mod tests { } ] ); + + // File with no data (only column headers) + let file_path = create_csv_file(dir.path(), "id,value\n"); + assert!(read_csv::(&file_path).is_err()); + assert!(read_csv_optional::(&file_path) + .unwrap() + .next() + .is_none()); } #[test] From 7d7372fe408628bddc8c0350d43559e59c55e499 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 21 Mar 2025 08:26:21 +0000 Subject: [PATCH 3/3] Move code for checking `Vec` --- src/input.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/input.rs b/src/input.rs index 8bbef276..0e185b20 100644 --- a/src/input.rs +++ b/src/input.rs @@ -34,7 +34,11 @@ pub use time_slice::read_time_slice_info; pub fn read_csv<'a, T: DeserializeOwned + 'a>( file_path: &'a Path, ) -> Result + 'a> { - _read_csv_internal(file_path, true) + let vec = _read_csv_internal(file_path)?; + if vec.is_empty() { + bail!("CSV file {} cannot be empty", file_path.display()); + } + Ok(vec.into_iter()) } /// Read a series of type `T`s from a CSV file. @@ -45,24 +49,18 @@ pub fn read_csv<'a, T: DeserializeOwned + 'a>( pub fn read_csv_optional<'a, T: DeserializeOwned + 'a>( file_path: &'a Path, ) -> Result + 'a> { - _read_csv_internal(file_path, false) + let vec = _read_csv_internal(file_path)?; + Ok(vec.into_iter()) } -fn _read_csv_internal<'a, T: DeserializeOwned + 'a>( - file_path: &'a Path, - check_empty: bool, -) -> Result + 'a> { +fn _read_csv_internal<'a, T: DeserializeOwned + 'a>(file_path: &'a Path) -> Result> { let vec = csv::Reader::from_path(file_path) .with_context(|| input_err_msg(file_path))? .into_deserialize() .process_results(|iter| iter.collect_vec()) .with_context(|| input_err_msg(file_path))?; - if check_empty && vec.is_empty() { - bail!("CSV file {} cannot be empty", file_path.display()); - } - - Ok(vec.into_iter()) + Ok(vec) } /// Parse a TOML file at the specified path.