diff --git a/src/worksheet.rs b/src/worksheet.rs index 9e945fa2..9c7132b1 100644 --- a/src/worksheet.rs +++ b/src/worksheet.rs @@ -1242,13 +1242,13 @@ mod tests; use std::borrow::Cow; -use std::cmp; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; use std::io::Cursor; use std::io::Write; use std::mem; use std::sync::{Arc, Mutex, RwLock}; +use std::{cmp, fmt}; #[cfg(feature = "constant_memory")] use tempfile::tempfile; @@ -1516,6 +1516,7 @@ pub struct Worksheet { nan: String, infinity: String, neg_infinity: String, + ignored_errors: HashMap, #[cfg(feature = "constant_memory")] pub(crate) file_writer: BufWriter, @@ -1730,6 +1731,7 @@ impl Worksheet { nan: "NAN".to_string(), infinity: "INF".to_string(), neg_infinity: "-INF".to_string(), + ignored_errors: HashMap::new(), // These collections need to be reset on resave. comment_relationships: vec![], @@ -13321,6 +13323,62 @@ impl Worksheet { self } + /// TODO + /// + /// # Errors + /// + /// - [`XlsxError::RowColumnLimitError`] - Row or column exceeds Excel's + /// worksheet limits. + /// + pub fn ignore_error( + &mut self, + row: RowNum, + col: ColNum, + error_type: IgnoreError, + ) -> Result<&mut Worksheet, XlsxError> { + self.ignore_error_range(row, col, row, col, error_type) + } + + /// TODO + /// + /// # Errors + /// + /// - [`XlsxError::RowColumnLimitError`] - Row or column exceeds Excel's + /// worksheet limits. + /// - [`XlsxError::RowColumnOrderError`] - First row or column is larger + /// than the last row or column. + /// + /// + pub fn ignore_error_range( + &mut self, + first_row: RowNum, + first_col: ColNum, + last_row: RowNum, + last_col: ColNum, + error_type: IgnoreError, + ) -> Result<&mut Worksheet, XlsxError> { + // Check rows and cols are in the allowed range. + if !self.check_dimensions_only(first_row, first_col) + || !self.check_dimensions_only(last_row, last_col) + { + return Err(XlsxError::RowColumnLimitError); + } + + // Check order of first/last values. + if first_row > last_row || first_col > last_col { + return Err(XlsxError::RowColumnOrderError); + } + + let range = utility::cell_range(first_row, first_col, last_row, last_col); + + self.ignored_errors + .entry(error_type) + .and_modify(|sqref| *sqref = format!("{sqref} {range}")) + .or_insert(range); + + Ok(self) + } + // ----------------------------------------------------------------------- // Crate level helper methods. // ----------------------------------------------------------------------- @@ -15824,6 +15882,11 @@ impl Worksheet { self.write_col_breaks(); } + // Write the ignoredErrors element. + if !self.ignored_errors.is_empty() { + self.write_ignored_errors(); + } + // Write the drawing element. if !self.drawing.drawings.is_empty() { self.write_drawing(); @@ -17441,9 +17504,21 @@ impl Worksheet { String::new() }; + // Get the result type attribute. let result_type = if result.parse::().is_err() { - r#" t="str""# + match result { + // Handle error results. + "#DIV/0!" | "#N/A" | "#NAME?" | "#NULL!" | "#NUM!" | "#REF!" | "#VALUE!" + | "#GETTING_DATA" => r#" t="e""#, + + // Handle boolean results. + "TRUE" | "FALSE" => r#" t="b""#, + + // Handle string results. + _ => r#" t="str""#, + } } else { + // Handle/ignore for numeric results. "" }; @@ -17477,9 +17552,21 @@ impl Worksheet { let cm = if is_dynamic { r#" cm="1""# } else { "" }; + // Get the result type attribute. let result_type = if result.parse::().is_err() { - r#" t="str""# + match result { + // Handle error results. + "#DIV/0!" | "#N/A" | "#NAME?" | "#NULL!" | "#NUM!" | "#REF!" | "#VALUE!" + | "#GETTING_DATA" => r#" t="e""#, + + // Handle boolean results. + "TRUE" | "FALSE" => r#" t="b""#, + + // Handle string results. + _ => r#" t="str""#, + } } else { + // Handle/ignore for numeric results. "" }; @@ -18112,6 +18199,25 @@ impl Worksheet { } xml_end_tag(&mut self.writer, "x14:sparklines"); } + + // Write the element. + fn write_ignored_errors(&mut self) { + xml_start_tag_only(&mut self.writer, "ignoredErrors"); + + for error_type in IgnoreError::iterator() { + let error_name = error_type.to_string(); + + if let Some(error_range) = self.ignored_errors.get(&error_type) { + let attributes = [ + ("sqref", error_range.clone()), + (&error_name, "1".to_string()), + ]; + xml_empty_tag(&mut self.writer, "ignoredError", &attributes); + } + } + + xml_end_tag(&mut self.writer, "ignoredErrors"); + } } // ----------------------------------------------------------------------- @@ -18957,6 +19063,84 @@ impl DefinedName { } } +/// The `IgnoreError` enum defines the Excel cell error types that can be +/// ignored. +/// +/// Used with the [`Worksheet::ignore_error()`](crate::Worksheet::ignore_error) +/// and +/// [`Worksheet::ignore_error_range()`](crate::Worksheet::ignore_error_range) +/// methods. +/// +#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] +pub enum IgnoreError { + /// Ignore errors/warnings for numbers stored as text. + NumberStoredAsText, + + /// Ignore errors/warnings for formula evaluation errors (such as divide by + /// zero). + EvalError, + + /// Ignore errors/warnings for formulas that differ from surrounding + /// formulas. + FormulaDiffers, + + /// Ignore errors/warnings for formulas that refer to empty cells. + EmptyCellReference, + + /// Ignore errors/warnings for formulas that omit cells in a range. + FormulaRange, + + /// Ignore errors/warnings for cells in a table that do not comply with + /// applicable data validation rules. + ListDataValidation, + + /// Ignore errors/warnings for formulas that contain a two digit text + /// representation of a year. + TwoDigitTextYear, + + /// Ignore errors/warnings for unlocked cells that contain formulas. + UnlockedFormula, + + /// Ignore errors/warnings for cell formulas that differ from the column + /// formula. + CalculatedColumn, +} + +impl IgnoreError { + /// Simple iterator for `IgnoreError`. + pub fn iterator() -> impl Iterator { + [ + Self::NumberStoredAsText, + Self::EvalError, + Self::FormulaDiffers, + Self::FormulaRange, + Self::UnlockedFormula, + Self::CalculatedColumn, + Self::EmptyCellReference, + Self::ListDataValidation, + Self::TwoDigitTextYear, + ] + .iter() + .copied() + } +} + +impl fmt::Display for IgnoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EvalError => write!(f, "evalError"), + Self::FormulaRange => write!(f, "formulaRange"), + Self::FormulaDiffers => write!(f, "formula"), + Self::UnlockedFormula => write!(f, "unlockedFormula"), + Self::CalculatedColumn => write!(f, "calculatedColumn"), + Self::TwoDigitTextYear => write!(f, "TwoDigitTextYear"), + Self::EmptyCellReference => write!(f, "emptyCellReference"), + Self::ListDataValidation => write!(f, "listDataValidation"), + Self::NumberStoredAsText => write!(f, "numberStoredAsText"), + } + } +} + #[derive(Clone, Debug)] pub(crate) enum DefinedNameType { Autofilter, diff --git a/tests/input/ignore_error01.xlsx b/tests/input/ignore_error01.xlsx new file mode 100644 index 00000000..8e0f31da Binary files /dev/null and b/tests/input/ignore_error01.xlsx differ diff --git a/tests/input/ignore_error02.xlsx b/tests/input/ignore_error02.xlsx new file mode 100644 index 00000000..8ae5514d Binary files /dev/null and b/tests/input/ignore_error02.xlsx differ diff --git a/tests/input/ignore_error03.xlsx b/tests/input/ignore_error03.xlsx new file mode 100644 index 00000000..71f29ea9 Binary files /dev/null and b/tests/input/ignore_error03.xlsx differ diff --git a/tests/input/ignore_error04.xlsx b/tests/input/ignore_error04.xlsx new file mode 100644 index 00000000..532ac261 Binary files /dev/null and b/tests/input/ignore_error04.xlsx differ diff --git a/tests/input/ignore_error05.xlsx b/tests/input/ignore_error05.xlsx new file mode 100644 index 00000000..c7b54c7d Binary files /dev/null and b/tests/input/ignore_error05.xlsx differ diff --git a/tests/input/ignore_error06.xlsx b/tests/input/ignore_error06.xlsx new file mode 100644 index 00000000..53848341 Binary files /dev/null and b/tests/input/ignore_error06.xlsx differ diff --git a/tests/integration/ignore_error01.rs b/tests/integration/ignore_error01.rs new file mode 100644 index 00000000..51173c58 --- /dev/null +++ b/tests/integration/ignore_error01.rs @@ -0,0 +1,32 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + worksheet.write_string(0, 0, "123")?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error01() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error01") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/ignore_error02.rs b/tests/integration/ignore_error02.rs new file mode 100644 index 00000000..df9b40e9 --- /dev/null +++ b/tests/integration/ignore_error02.rs @@ -0,0 +1,34 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{IgnoreError, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + worksheet.write_string(0, 0, "123")?; + + worksheet.ignore_error(0, 0, IgnoreError::NumberStoredAsText)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error02() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error02") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/ignore_error03.rs b/tests/integration/ignore_error03.rs new file mode 100644 index 00000000..5d36a38f --- /dev/null +++ b/tests/integration/ignore_error03.rs @@ -0,0 +1,36 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{IgnoreError, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + for row in 0..=9 { + worksheet.write_string(row, 0, "123")?; + } + + worksheet.ignore_error_range(0, 0, 9, 0, IgnoreError::NumberStoredAsText)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error03() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error03") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/ignore_error04.rs b/tests/integration/ignore_error04.rs new file mode 100644 index 00000000..efcfbacf --- /dev/null +++ b/tests/integration/ignore_error04.rs @@ -0,0 +1,38 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{IgnoreError, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + worksheet.write_string(0, 0, "123")?; + worksheet.write_string(2, 2, "123")?; + worksheet.write_string(4, 4, "123")?; + + worksheet.ignore_error(0, 0, IgnoreError::NumberStoredAsText)?; + worksheet.ignore_error(2, 2, IgnoreError::NumberStoredAsText)?; + worksheet.ignore_error(4, 4, IgnoreError::NumberStoredAsText)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error04() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error04") + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/ignore_error05.rs b/tests/integration/ignore_error05.rs new file mode 100644 index 00000000..068fd8e6 --- /dev/null +++ b/tests/integration/ignore_error05.rs @@ -0,0 +1,39 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{IgnoreError, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + worksheet.write_string(0, 0, "123")?; + worksheet + .write_formula(1, 0, "=1/0")? + .set_formula_result(1, 0, "#DIV/0!"); + + worksheet.ignore_error(0, 0, IgnoreError::NumberStoredAsText)?; + worksheet.ignore_error(1, 0, IgnoreError::EvalError)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error05() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error05") + .ignore_calc_chain() + .set_function(create_new_xlsx_file) + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/ignore_error06.rs b/tests/integration/ignore_error06.rs new file mode 100644 index 00000000..864387bd --- /dev/null +++ b/tests/integration/ignore_error06.rs @@ -0,0 +1,37 @@ +// Test case that compares a file generated by rust_xlsxwriter with a file +// created by Excel. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2025, John McNamara, jmcnamara@cpan.org + +use crate::common; +use rust_xlsxwriter::{IgnoreError, Workbook, XlsxError}; + +// Create rust_xlsxwriter file to compare against Excel file. +fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + worksheet.write_formula(0, 0, "=B1")?; + worksheet.write_formula(1, 0, "=B1")?; + worksheet.write_formula(2, 0, "=B3")?; + + worksheet.ignore_error(1, 0, IgnoreError::FormulaDiffers)?; + + workbook.save(filename)?; + + Ok(()) +} + +#[test] +fn test_ignore_error06() { + let test_runner = common::TestRunner::new() + .set_name("ignore_error06") + .set_function(create_new_xlsx_file) + .ignore_calc_chain() + .initialize(); + + test_runner.assert_eq(); + test_runner.cleanup(); +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 72969ec4..5af6d2f1 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -670,6 +670,12 @@ mod hyperlink48; mod hyperlink49; mod hyperlink50; mod hyperlink51; +mod ignore_error01; +mod ignore_error02; +mod ignore_error03; +mod ignore_error04; +mod ignore_error05; +mod ignore_error06; mod image01; mod image02; mod image03;