From bb48b0226e692c9e364ddedbfc619a631cc3b6e2 Mon Sep 17 00:00:00 2001 From: John McNamara Date: Wed, 3 Jul 2024 00:47:42 +0100 Subject: [PATCH] data validation: add data validation support Feature #97 --- src/conditional_format.rs | 14 +- src/data_validation.rs | 677 +++++++++++++++ src/data_validation/tests.rs | 1584 ++++++++++++++++++++++++++++++++++ src/error.rs | 8 + src/lib.rs | 4 + src/worksheet.rs | 217 ++++- 6 files changed, 2494 insertions(+), 10 deletions(-) create mode 100644 src/data_validation.rs create mode 100644 src/data_validation/tests.rs diff --git a/src/conditional_format.rs b/src/conditional_format.rs index f70b9a04..3e42156b 100644 --- a/src/conditional_format.rs +++ b/src/conditional_format.rs @@ -1,4 +1,4 @@ -// image - A module to represent Excel conditional formats. +// conditional_format - A module to represent Excel conditional formats. // // SPDX-License-Identifier: MIT OR Apache-2.0 // @@ -6784,7 +6784,7 @@ conditional_format_value_from_type!(&NaiveDate & NaiveDateTime & NaiveTime); /// [`ConditionalFormatCell`]. /// /// -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone)] pub enum ConditionalFormatCellRule { /// Show the conditional format for cells that are equal to the target value. EqualTo(T), @@ -6834,7 +6834,7 @@ impl fmt::Display for ConditionalFormatCellRule /// -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone)] pub enum ConditionalFormatDataBarDirection { /// The bars go "Right to left" or "Left to right" depending on the context. /// This is the default. @@ -7225,7 +7225,7 @@ pub enum ConditionalFormatDataBarDirection { /// /// /// -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub enum ConditionalFormatDataBarAxisPosition { /// The axis is set automatically depending on whether the data contains /// negative values. This is the default. diff --git a/src/data_validation.rs b/src/data_validation.rs new file mode 100644 index 00000000..b3f616a5 --- /dev/null +++ b/src/data_validation.rs @@ -0,0 +1,677 @@ +// data_validation - A module to represent Excel data validations. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2024, John McNamara, jmcnamara@cpan.org + +//! # Working with Data Validation +//! +//! TODO + +#![warn(missing_docs)] + +mod tests; + +#[cfg(feature = "chrono")] +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::{ExcelDateTime, Formula, IntoExcelDateTime, XlsxError}; +use std::fmt; + +// ----------------------------------------------------------------------- +// DataValidation +// ----------------------------------------------------------------------- + +/// The `DataValidation` struct represents a Cell conditional format. +/// +/// TODO +/// +#[derive(Clone)] +pub struct DataValidation { + pub(crate) validation_type: DataValidationType, + pub(crate) rule: DataValidationRuleInternal, + pub(crate) ignore_blank: bool, + pub(crate) show_input_message: bool, + pub(crate) show_error_message: bool, + pub(crate) multi_range: String, + pub(crate) input_title: String, + pub(crate) error_title: String, + pub(crate) input_message: String, + pub(crate) error_message: String, + pub(crate) error_style: DataValidationErrorStyle, +} + +impl DataValidation { + /// Create a new Cell conditional format struct. + #[allow(clippy::new_without_default)] + pub fn new() -> DataValidation { + DataValidation { + validation_type: DataValidationType::Any, + rule: DataValidationRuleInternal::EqualTo(String::new()), + ignore_blank: true, + show_input_message: true, + show_error_message: true, + multi_range: String::new(), + input_title: String::new(), + error_title: String::new(), + input_message: String::new(), + error_message: String::new(), + error_style: DataValidationErrorStyle::Stop, + } + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_any_value(mut self) -> DataValidation { + self.rule = DataValidationRuleInternal::EqualTo(String::new()); + self.validation_type = DataValidationType::Any; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_whole_number(mut self, rule: DataValidationRule) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Whole; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_whole_number_formula( + mut self, + rule: DataValidationRule, + ) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Whole; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_decimal_number(mut self, rule: DataValidationRule) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Decimal; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_decimal_number_formula( + mut self, + rule: DataValidationRule, + ) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Decimal; + self + } + + /// Set the TODO + /// + /// TODO + /// + /// # Errors + /// + pub fn allow_list_strings( + mut self, + list: &[impl AsRef], + ) -> Result { + let joined_list = list + .iter() + .map(|s| s.as_ref().to_string().replace('"', "\"\"")) + .collect::>() + .join(","); + + let length = joined_list.chars().count(); + if length > 255 { + return Err(XlsxError::DataValidationError( + format!("Validation list length '{length}' including commas is greater than Excel's limit of 255 characters: {joined_list}") + )); + } + + let joined_list = format!("\"{joined_list}\""); + + self.rule = DataValidationRuleInternal::ListSource(joined_list); + self.validation_type = DataValidationType::List; + Ok(self) + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_list_formula(mut self, rule: Formula) -> DataValidation { + let formula = rule.expand_formula(true).to_string(); + self.rule = DataValidationRuleInternal::ListSource(formula); + self.validation_type = DataValidationType::List; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_date( + mut self, + rule: DataValidationRule, + ) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Date; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_date_formula(mut self, rule: DataValidationRule) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Date; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_time( + mut self, + rule: DataValidationRule, + ) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Time; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_time_formula(mut self, rule: DataValidationRule) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::Time; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_text_length(mut self, rule: DataValidationRule) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::TextLength; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_text_length_formula( + mut self, + rule: DataValidationRule, + ) -> DataValidation { + self.rule = rule.to_internal_rule(); + self.validation_type = DataValidationType::TextLength; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn allow_custom_formula(mut self, rule: Formula) -> DataValidation { + let formula = rule.expand_formula(true).to_string(); + self.rule = DataValidationRuleInternal::CustomFormula(formula); + self.validation_type = DataValidationType::Custom; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn ignore_blank(mut self, enable: bool) -> DataValidation { + self.ignore_blank = enable; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn show_input_message(mut self, enable: bool) -> DataValidation { + self.show_input_message = enable; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn show_error_message(mut self, enable: bool) -> DataValidation { + self.show_error_message = enable; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn set_input_title(mut self, text: impl Into) -> DataValidation { + let text = text.into(); + let length = text.chars().count(); + + if length > 32 { + eprintln!( + "Validation title length '{length}' greater than Excel's limit of 32 characters." + ); + return self; + } + + self.input_title = text; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn set_input_message(mut self, text: impl Into) -> DataValidation { + let text = text.into(); + let length = text.chars().count(); + + if length > 255 { + eprintln!( + "Validation message length '{length}' greater than Excel's limit of 255 characters." + ); + return self; + } + + self.input_message = text; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn set_error_title(mut self, text: impl Into) -> DataValidation { + let text = text.into(); + let length = text.chars().count(); + + if length > 32 { + eprintln!( + "Validation title length '{length}' greater than Excel's limit of 32 characters." + ); + return self; + } + + self.error_title = text; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn set_error_message(mut self, text: impl Into) -> DataValidation { + let text = text.into(); + let length = text.chars().count(); + + if length > 255 { + eprintln!( + "Validation message length '{length}' greater than Excel's limit of 255 characters." + ); + return self; + } + + self.error_message = text; + self + } + + /// Set the TODO + /// + /// TODO + /// + pub fn set_error_style(mut self, error_style: DataValidationErrorStyle) -> DataValidation { + self.error_style = error_style; + self + } + + /// Set an additional multi-cell range for the conditional format. + /// + /// The `set_multi_range()` method is used to extend a conditional + /// format over non-contiguous ranges like `"B3:D6 I3:K6 B9:D12 + /// I9:K12"`. + /// + /// See [Selecting a non-contiguous + /// range](crate::conditional_format#selecting-a-non-contiguous-range) + /// for more information. + /// + /// # Parameters + /// + /// * `range` - A string like type representing an Excel range. + /// + /// Note, you can use an Excel range like `"$B$3:$D$6,$I$3:$K$6"` or + /// omit the `$` anchors and replace the commas with spaces to have a + /// clearer range like `"B3:D6 I3:K6"`. The documentation and examples + /// use the latter format for clarity but it you are copying and + /// pasting from Excel you can use the first format. + /// + /// Note, if the range is invalid then Excel will omit it silently. + /// + pub fn set_multi_range(mut self, range: impl Into) -> DataValidation { + self.multi_range = range.into().replace('$', "").replace(',', " "); + self + } + + // The "Any" validation type should be ignored if it doesn't have any input + // or error titles or messages. This is the same rule as Excel. + pub(crate) fn is_invalid_any(&mut self) -> bool { + self.validation_type == DataValidationType::Any + && self.input_title.is_empty() + && self.input_message.is_empty() + && self.error_title.is_empty() + && self.error_message.is_empty() + } +} + +/// Trait to map rust types into data validation types +/// +/// The `IntoDataValidationValue` trait is used to map Rust types like +/// strings, numbers, dates, times and formulas into a generic type that can be +/// used to replicate Excel data types used in Data Validation. TODO +/// +pub trait IntoDataValidationValue { + /// Function to turn types into a TODO enum value. + fn to_string_value(&self) -> String; +} + +impl IntoDataValidationValue for i32 { + fn to_string_value(&self) -> String { + self.to_string() + } +} + +impl IntoDataValidationValue for u32 { + fn to_string_value(&self) -> String { + self.to_string() + } +} + +impl IntoDataValidationValue for f64 { + fn to_string_value(&self) -> String { + self.to_string() + } +} + +impl IntoDataValidationValue for Formula { + fn to_string_value(&self) -> String { + self.expand_formula(true).to_string() + } +} + +impl IntoDataValidationValue for ExcelDateTime { + fn to_string_value(&self) -> String { + self.to_excel().to_string() + } +} + +impl IntoDataValidationValue for &ExcelDateTime { + fn to_string_value(&self) -> String { + self.to_excel().to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for NaiveDateTime { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_datetime_to_excel(self).to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for &NaiveDateTime { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_datetime_to_excel(self).to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for NaiveDate { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_date_to_excel(self).to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for &NaiveDate { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_date_to_excel(self).to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for NaiveTime { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_time_to_excel(self).to_string() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoDataValidationValue for &NaiveTime { + fn to_string_value(&self) -> String { + ExcelDateTime::chrono_time_to_excel(self).to_string() + } +} + +//#[cfg(feature = "chrono")] +//#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +//data_validation_value_from_type!(&NaiveDate & NaiveDateTime & NaiveTime); + +// ----------------------------------------------------------------------- +// DataValidationType +// ----------------------------------------------------------------------- + +/// The `DataValidationType` enum defines TODO +/// +/// +#[derive(Clone, Eq, PartialEq)] +pub enum DataValidationType { + /// TODO + Whole, + + /// TODO + Decimal, + + /// TODO + Date, + + /// TODO + Time, + + /// TODO + TextLength, + + /// TODO + Custom, + + /// TODO + List, + + /// TODO + Any, +} + +impl fmt::Display for DataValidationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Any => write!(f, "any"), + Self::Date => write!(f, "date"), + Self::List => write!(f, "list"), + Self::Time => write!(f, "time"), + Self::Whole => write!(f, "whole"), + Self::Custom => write!(f, "custom"), + Self::Decimal => write!(f, "decimal"), + Self::TextLength => write!(f, "textLength"), + } + } +} + +// ----------------------------------------------------------------------- +// DataValidationRule +// ----------------------------------------------------------------------- + +/// The `DataValidationRule` enum defines the conditional format rule for +/// [`DataValidation`]. +/// +/// +#[derive(Clone)] +pub enum DataValidationRule { + /// TODO. + EqualTo(T), + + /// TODO. + NotEqualTo(T), + + /// TODO. + GreaterThan(T), + + /// TODO. + GreaterThanOrEqualTo(T), + + /// TODO. + LessThan(T), + + /// TODO. + LessThanOrEqualTo(T), + + /// TODO. + Between(T, T), + + /// TODO. + NotBetween(T, T), +} + +impl DataValidationRule { + fn to_internal_rule(&self) -> DataValidationRuleInternal { + match &self { + DataValidationRule::EqualTo(value) => { + DataValidationRuleInternal::EqualTo(value.to_string_value()) + } + DataValidationRule::NotEqualTo(value) => { + DataValidationRuleInternal::NotEqualTo(value.to_string_value()) + } + DataValidationRule::GreaterThan(value) => { + DataValidationRuleInternal::GreaterThan(value.to_string_value()) + } + + DataValidationRule::GreaterThanOrEqualTo(value) => { + DataValidationRuleInternal::GreaterThanOrEqualTo(value.to_string_value()) + } + DataValidationRule::LessThan(value) => { + DataValidationRuleInternal::LessThan(value.to_string_value()) + } + DataValidationRule::LessThanOrEqualTo(value) => { + DataValidationRuleInternal::LessThanOrEqualTo(value.to_string_value()) + } + DataValidationRule::Between(min, max) => { + DataValidationRuleInternal::Between(min.to_string_value(), max.to_string_value()) + } + DataValidationRule::NotBetween(min, max) => { + DataValidationRuleInternal::NotBetween(min.to_string_value(), max.to_string_value()) + } + } + } +} + +// ----------------------------------------------------------------------- +// DataValidationRuleInternal +// ----------------------------------------------------------------------- + +// TODO +#[derive(Clone)] +pub(crate) enum DataValidationRuleInternal { + EqualTo(String), + + NotEqualTo(String), + + GreaterThan(String), + + GreaterThanOrEqualTo(String), + + LessThan(String), + + LessThanOrEqualTo(String), + + Between(String, String), + + NotBetween(String, String), + + CustomFormula(String), + + ListSource(String), +} + +impl fmt::Display for DataValidationRuleInternal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EqualTo(_) => write!(f, "equal"), + Self::LessThan(_) => write!(f, "lessThan"), + Self::Between(_, _) => write!(f, "between"), + Self::ListSource(_) => write!(f, "list"), + Self::NotEqualTo(_) => write!(f, "notEqual"), + Self::GreaterThan(_) => write!(f, "greaterThan"), + Self::CustomFormula(_) => write!(f, ""), + Self::NotBetween(_, _) => write!(f, "notBetween"), + Self::LessThanOrEqualTo(_) => write!(f, "lessThanOrEqual"), + Self::GreaterThanOrEqualTo(_) => write!(f, "greaterThanOrEqual"), + } + } +} + +// ----------------------------------------------------------------------- +// DataValidationErrorStyle +// ----------------------------------------------------------------------- + +/// The `DataValidationErrorStyle` enum defines TODO +/// +/// +#[derive(Clone)] +pub enum DataValidationErrorStyle { + /// TODO + Stop, + + /// TODO + Warning, + + /// TODO + Information, +} + +impl fmt::Display for DataValidationErrorStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stop => write!(f, "stop"), + Self::Warning => write!(f, "warning"), + Self::Information => write!(f, "information"), + } + } +} diff --git a/src/data_validation/tests.rs b/src/data_validation/tests.rs new file mode 100644 index 00000000..cf3d03ff --- /dev/null +++ b/src/data_validation/tests.rs @@ -0,0 +1,1584 @@ +// data_validation unit tests. +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright 2022-2024, John McNamara, jmcnamara@cpan.org + +#[cfg(test)] +mod data_validation_tests { + + use crate::test_functions::xml_to_vec; + use crate::DataValidation; + use crate::DataValidationErrorStyle; + use crate::DataValidationRule; + use crate::ExcelDateTime; + use crate::Formula; + use crate::Worksheet; + use crate::XlsxError; + + #[cfg(feature = "chrono")] + use chrono::{NaiveDate, NaiveTime}; + + use pretty_assertions::assert_eq; + + #[test] + fn data_validation_01() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = + DataValidation::new().allow_whole_number(DataValidationRule::GreaterThan(-10)); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + -10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_02() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = + DataValidation::new().allow_decimal_number(DataValidationRule::Between(1.0, 2.0)); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 1 + 2 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_03() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::LessThan(10)) + .ignore_blank(false); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_04() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::LessThan(10)) + .show_input_message(false); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_05() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::LessThan(10)) + .show_error_message(false); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_06() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::LessThan(10)) + .show_error_message(false); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_07() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::NotEqualTo(10)) + .set_input_title("Title 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_08() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::NotEqualTo(10)) + .set_input_title("Title 1") + .set_input_message("Message 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_09() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::NotEqualTo(10)) + .set_error_title("Title 2") + .set_error_message("Message 2") + .set_input_title("Title 1") + .set_input_message("Message 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_10() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::NotEqualTo(10)) + .set_error_style(DataValidationErrorStyle::Warning); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_11() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::NotEqualTo(10)) + .set_error_style(DataValidationErrorStyle::Information); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 10 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_12_1() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + ExcelDateTime::parse_from_str("2025-01-01")?, + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_12_2() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + &ExcelDateTime::parse_from_str("2025-01-01")?, + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_12_3() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_12_4() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + &NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_12_5() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + NaiveDate::from_ymd_opt(2025, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_12_6() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_date(DataValidationRule::GreaterThan( + &NaiveDate::from_ymd_opt(2025, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 45658 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_13_1() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_time(DataValidationRule::GreaterThan( + ExcelDateTime::parse_from_str("12:00")?, + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 0.5 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_13_2() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_time(DataValidationRule::GreaterThan( + &ExcelDateTime::parse_from_str("12:00")?, + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 0.5 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_13_3() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_time(DataValidationRule::GreaterThan( + NaiveTime::from_hms_milli_opt(12, 0, 0, 0).unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 0.5 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[cfg(feature = "chrono")] + #[test] + fn data_validation_13_4() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_time(DataValidationRule::GreaterThan( + &NaiveTime::from_hms_milli_opt(12, 0, 0, 0).unwrap(), + )); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 0.5 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_14() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = + DataValidation::new().allow_text_length(DataValidationRule::GreaterThan(6)); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 6 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_15_1() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_custom_formula(Formula::new("=6")); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 6 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_15_2() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_custom_formula("6".into()); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 6 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_16_1() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_list_strings(&["Foo", "Bar", "Baz"])?; + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + "Foo,Bar,Baz" + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_16_2() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().allow_list_strings(&[ + String::from("Foo"), + String::from("Bar"), + String::from("Baz"), + ])?; + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + "Foo,Bar,Baz" + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_17_1() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_any_value() + .set_input_title("Title 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_17_2() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new().set_input_title("Title 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_18() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_any_value() + .set_input_message("Message 1"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_19() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_any_value() + .set_error_title("Title 2") + .set_error_message("Message 2"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_20() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + // Check for empty "Any" validation. + let data_validation = DataValidation::new().allow_any_value(); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_21() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let invalid_title = "This exceeds Excel's title limits"; + let padding = ["a"; 221]; + let invalid_message = "This exceeds Excel's message limits".to_string() + &padding.concat(); + let list_values = [ + "Foobar", "Foobas", "Foobat", "Foobau", "Foobav", "Foobaw", "Foobax", "Foobay", + "Foobaz", "Foobba", "Foobbb", "Foobbc", "Foobbd", "Foobbe", "Foobbf", "Foobbg", + "Foobbh", "Foobbi", "Foobbj", "Foobbk", "Foobbl", "Foobbm", "Foobbn", "Foobbo", + "Foobbp", "Foobbq", "Foobbr", "Foobbs", "Foobbt", "Foobbu", "Foobbv", "Foobbw", + "Foobbx", "Foobby", "Foobbz", "Foobca", "End1", + ]; + + // Check for invalid string lengths. + let data_validation = DataValidation::new() + .allow_any_value() + .set_input_title(invalid_title) + .set_input_message(&invalid_message) + .set_error_title(invalid_title) + .set_error_message(&invalid_message); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + // Check for invalid string list. + let result = DataValidation::new().allow_list_strings(&list_values); + assert!(matches!(result, Err(XlsxError::DataValidationError(_)))); + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_22() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number_formula(DataValidationRule::EqualTo(Formula::new("=J13"))); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + J13 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_23() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number_formula(DataValidationRule::EqualTo(Formula::new("=$J13"))); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + $J13 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_24() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number_formula(DataValidationRule::GreaterThan("B1".into())); + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + let data_validation = DataValidation::new() + .allow_decimal_number_formula(DataValidationRule::GreaterThan("B2".into())); + worksheet.add_data_validation(1, 0, 1, 0, &data_validation)?; + + let data_validation = DataValidation::new().allow_list_formula("$B$3:$E$3".into()); + worksheet.add_data_validation(2, 0, 2, 0, &data_validation)?; + + let data_validation = + DataValidation::new().allow_date_formula(DataValidationRule::GreaterThan("B4".into())); + worksheet.add_data_validation(3, 0, 3, 0, &data_validation)?; + + let data_validation = + DataValidation::new().allow_time_formula(DataValidationRule::GreaterThan("B5".into())); + worksheet.add_data_validation(4, 0, 4, 0, &data_validation)?; + + let data_validation = DataValidation::new() + .allow_text_length_formula(DataValidationRule::GreaterThan("B6".into())); + worksheet.add_data_validation(5, 0, 5, 0, &data_validation)?; + + let data_validation = DataValidation::new().allow_custom_formula("B7".into()); + worksheet.add_data_validation(6, 0, 6, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + B1 + + + B2 + + + $B$3:$E$3 + + + B4 + + + B5 + + + B6 + + + B7 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_25() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = + DataValidation::new().allow_whole_number(DataValidationRule::GreaterThan(7)); + + worksheet.add_data_validation(0, 0, 5, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 7 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_validation_26() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let data_validation = DataValidation::new() + .allow_whole_number(DataValidationRule::GreaterThan(8)) + .set_multi_range("A1:A6 B8 C10"); + + worksheet.add_data_validation(0, 0, 0, 0, &data_validation)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + 8 + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index c334511c..c1738e89 100644 --- a/src/error.rs +++ b/src/error.rs @@ -143,6 +143,10 @@ pub enum XlsxError { /// incorrect or missing. ConditionalFormatError(String), + /// A general error that is raised when a data validation parameter is + /// incorrect or missing. + DataValidationError(String), + /// A customizable error that can be used by third parties to raise errors /// or as a conversion target for other Error types. CustomError(String), @@ -294,6 +298,10 @@ impl fmt::Display for XlsxError { write!(f, "Conditional format error: '{error}'.") } + XlsxError::DataValidationError(error) => { + write!(f, "Data validation error: '{error}'.") + } + XlsxError::CustomError(error) => { write!(f, "{error}") } diff --git a/src/lib.rs b/src/lib.rs index 540092a1..2e367f1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,7 @@ pub mod changelog; pub mod chart; pub mod conditional_format; pub mod cookbook; +pub mod data_validation; pub mod sparkline; pub mod tutorial; pub mod utility; @@ -246,6 +247,9 @@ pub use chart::*; #[doc(hidden)] pub use conditional_format::*; +#[doc(hidden)] +pub use data_validation::*; + #[doc(hidden)] pub use sparkline::*; diff --git a/src/worksheet.rs b/src/worksheet.rs index 603ff875..5dcc5791 100644 --- a/src/worksheet.rs +++ b/src/worksheet.rs @@ -1076,9 +1076,10 @@ use crate::vml::VmlInfo; use crate::xmlwriter::{XMLWriter, XML_WRITE_ERROR}; use crate::{ static_regex, utility, Chart, ChartEmptyCells, ChartRangeCacheData, ChartRangeCacheDataType, - Color, ConditionalFormat, ExcelDateTime, FilterCondition, FilterCriteria, FilterData, - FilterDataType, HeaderImagePosition, HyperlinkType, Image, IntoColor, IntoExcelDateTime, - ObjectMovement, ProtectionOptions, Sparkline, SparklineType, Table, TableFunction, Url, + Color, ConditionalFormat, DataValidation, DataValidationErrorStyle, DataValidationRuleInternal, + DataValidationType, ExcelDateTime, FilterCondition, FilterCriteria, FilterData, FilterDataType, + HeaderImagePosition, HyperlinkType, Image, IntoColor, IntoExcelDateTime, ObjectMovement, + ProtectionOptions, Sparkline, SparklineType, Table, TableFunction, Url, }; /// Integer type to represent a zero indexed row number. Excel's limit for rows @@ -1263,6 +1264,7 @@ pub struct Worksheet { has_drawing_object_linkage: bool, cells_with_autofilter: HashSet<(RowNum, ColNum)>, conditional_formats: BTreeMap>>, + data_validations: BTreeMap, has_conditional_formats: bool, use_x14_extensions: bool, has_x14_conditional_formats: bool, @@ -1455,6 +1457,7 @@ impl Worksheet { has_drawing_object_linkage: false, cells_with_autofilter: HashSet::new(), conditional_formats: BTreeMap::new(), + data_validations: BTreeMap::new(), has_conditional_formats: false, use_x14_extensions: false, has_x14_conditional_formats: false, @@ -6419,6 +6422,76 @@ impl Worksheet { Ok(self) } + /// Add a TODO. + /// + /// Conditional formatting is a feature of Excel which allows you to apply a + /// format to a cell or a range of cells based on certain criteria. This is + /// generally used to highlight particular values in a range of data. + /// + /// + /// + /// The [`ConditionalFormat`](crate::data_validation) variants are used to represent the types of + /// conditional format that can be applied in Excel. + /// + /// # Errors + /// + /// * [`XlsxError::RowColumnLimitError`] - Row or column exceeds Excel's + /// worksheet limits. + /// * [`XlsxError::RowColumnOrderError`] - First row larger than the last + /// row. + /// * [`XlsxError::ConditionalFormatError`] - A general error that is raised + /// when a conditional formatting parameter is incorrect or missing. + /// + /// # Parameters + /// + /// * `first_row` - The first row of the range. (All zero indexed.) + /// * `first_col` - The first row of the range. + /// * `last_row` - The last row of the range. + /// * `last_col` - The last row of the range. + /// * `data_validation` - A conditional format instance that implements + /// the [`ConditionalFormat`] trait. TODO + /// + pub fn add_data_validation( + &mut self, + first_row: RowNum, + first_col: ColNum, + last_row: RowNum, + last_col: ColNum, + data_validation: &DataValidation, + ) -> 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 mut data_validation = data_validation.clone(); + + // The "Any" validation type should be ignored if it doesn't have any + // input or error titles or messages. This is the same rule as Excel. + if data_validation.is_invalid_any() { + return Ok(self); + } + + // Store the data validation based on its range. + let mut cell_range = utility::cell_range(first_row, first_col, last_row, last_col); + if !data_validation.multi_range.is_empty() { + cell_range.clone_from(&data_validation.multi_range); + } + + + self.data_validations.insert(cell_range, data_validation); + + Ok(self) + } + /// Add a sparkline to a worksheet cell. /// /// Sparklines are a feature of Excel 2010+ which allows you to add small @@ -12807,6 +12880,11 @@ impl Worksheet { self.write_conditional_formats(); } + // Write the element. + fn write_data_validations(&mut self) { + let attributes = [("count", self.data_validations.len().to_string())]; + + self.writer.xml_start_tag("dataValidations", &attributes); + + for (range, data_validation) in &self.data_validations.clone() { + // Write the dataValidation element. + self.write_data_validation(range, data_validation); + } + + self.writer.xml_end_tag("dataValidations"); + } + + // Write the element. + fn write_data_validation(&mut self, range: &String, data_validation: &DataValidation) { + // The Any type doesn't have a rule or values so handle that separately. + if data_validation.validation_type == DataValidationType::Any { + self.write_data_validation_any(range, data_validation); + return; + } + + // Start the attributes. + let mut attributes = vec![("type", data_validation.validation_type.to_string())]; + + match data_validation.error_style { + DataValidationErrorStyle::Warning | DataValidationErrorStyle::Information => { + attributes.push(("errorStyle", data_validation.error_style.to_string())); + } + DataValidationErrorStyle::Stop => {} + } + + match &data_validation.rule { + &DataValidationRuleInternal::Between(_, _) + | DataValidationRuleInternal::CustomFormula(_) + | DataValidationRuleInternal::ListSource(_) => { + // Excel doesn't use an operator for these types. + } + _ => { + attributes.push(("operator", data_validation.rule.to_string())); + } + }; + + if data_validation.ignore_blank { + attributes.push(("allowBlank", "1".to_string())); + } + + if data_validation.show_input_message { + attributes.push(("showInputMessage", "1".to_string())); + } + + if data_validation.show_error_message { + attributes.push(("showErrorMessage", "1".to_string())); + } + + if !data_validation.error_title.is_empty() { + attributes.push(("errorTitle", data_validation.error_title.clone())); + } + + if !data_validation.error_message.is_empty() { + attributes.push(("error", data_validation.error_message.clone())); + } + + if !data_validation.input_title.is_empty() { + attributes.push(("promptTitle", data_validation.input_title.clone())); + } + + if !data_validation.input_message.is_empty() { + attributes.push(("prompt", data_validation.input_message.clone())); + } + + attributes.push(("sqref", range.to_string())); + + self.writer.xml_start_tag("dataValidation", &attributes); + + // Write the / elements. + match &data_validation.rule { + DataValidationRuleInternal::EqualTo(value) + | DataValidationRuleInternal::NotEqualTo(value) + | DataValidationRuleInternal::LessThan(value) + | DataValidationRuleInternal::LessThanOrEqualTo(value) + | DataValidationRuleInternal::GreaterThan(value) + | DataValidationRuleInternal::GreaterThanOrEqualTo(value) + | DataValidationRuleInternal::ListSource(value) + | DataValidationRuleInternal::CustomFormula(value) => { + self.writer.xml_data_element_only("formula1", value); + } + DataValidationRuleInternal::Between(min, max) + | DataValidationRuleInternal::NotBetween(min, max) => { + self.writer.xml_data_element_only("formula1", min); + self.writer.xml_data_element_only("formula2", max); + } + } + self.writer.xml_end_tag("dataValidation"); + } + + // Write the element. + fn write_data_validation_any(&mut self, range: &String, data_validation: &DataValidation) { + let mut attributes = vec![]; + + if data_validation.ignore_blank { + attributes.push(("allowBlank", "1".to_string())); + } + + if data_validation.show_input_message { + attributes.push(("showInputMessage", "1".to_string())); + } + + if data_validation.show_error_message { + attributes.push(("showErrorMessage", "1".to_string())); + } + + if !data_validation.error_title.is_empty() { + attributes.push(("errorTitle", data_validation.error_title.clone())); + } + + if !data_validation.error_message.is_empty() { + attributes.push(("error", data_validation.error_message.clone())); + } + + if !data_validation.input_title.is_empty() { + attributes.push(("promptTitle", data_validation.input_title.clone())); + } + + if !data_validation.input_message.is_empty() { + attributes.push(("prompt", data_validation.input_message.clone())); + } + + attributes.push(("sqref", range.to_string())); + + self.writer.xml_empty_tag("dataValidation", &attributes); + } + // Write the element. fn write_hyperlink(&mut self, row: RowNum, col: ColNum, hyperlink: &Url) { let mut attributes = vec![("ref", utility::row_col_to_cell(row, col))];