From dc2e3a49f7cd6c291b938310cd9f5ff2414c6915 Mon Sep 17 00:00:00 2001 From: John McNamara Date: Tue, 14 Nov 2023 23:47:51 +0000 Subject: [PATCH] cond format: add initial data bar style conditional format --- ...c_conditional_format_2color_set_minimum.rs | 8 +- ...c_conditional_format_3color_set_minimum.rs | 8 +- src/conditional_format.rs | 903 ++++++++++++++++-- src/conditional_format/tests.rs | 769 ++++++++++++++- src/workbook.rs | 3 + src/worksheet.rs | 127 ++- 6 files changed, 1677 insertions(+), 141 deletions(-) diff --git a/examples/doc_conditional_format_2color_set_minimum.rs b/examples/doc_conditional_format_2color_set_minimum.rs index 1e8f537c..0b70b6e5 100644 --- a/examples/doc_conditional_format_2color_set_minimum.rs +++ b/examples/doc_conditional_format_2color_set_minimum.rs @@ -5,9 +5,7 @@ //! Example of adding 2 color scale type conditional formatting to a worksheet //! with user defined minimum and maximum values. -use rust_xlsxwriter::{ - ConditionalFormat2ColorScale, ConditionalFormatScaleType, Workbook, XlsxError, -}; +use rust_xlsxwriter::{ConditionalFormat2ColorScale, ConditionalFormatType, Workbook, XlsxError}; fn main() -> Result<(), XlsxError> { // Create a new Excel file object. @@ -29,8 +27,8 @@ fn main() -> Result<(), XlsxError> { // defined range. Values <= 3 will be shown with the minimum color while // values >= 7 will be shown with the maximum color. let conditional_format = ConditionalFormat2ColorScale::new() - .set_minimum(ConditionalFormatScaleType::Number, 3) - .set_maximum(ConditionalFormatScaleType::Number, 7); + .set_minimum(ConditionalFormatType::Number, 3) + .set_maximum(ConditionalFormatType::Number, 7); worksheet.add_conditional_format(2, 3, 11, 3, &conditional_format)?; diff --git a/examples/doc_conditional_format_3color_set_minimum.rs b/examples/doc_conditional_format_3color_set_minimum.rs index 71733d24..607d031d 100644 --- a/examples/doc_conditional_format_3color_set_minimum.rs +++ b/examples/doc_conditional_format_3color_set_minimum.rs @@ -5,9 +5,7 @@ //! Example of adding 3 color scale type conditional formatting to a worksheet //! with user defined minimum and maximum values. -use rust_xlsxwriter::{ - ConditionalFormat3ColorScale, ConditionalFormatScaleType, Workbook, XlsxError, -}; +use rust_xlsxwriter::{ConditionalFormat3ColorScale, ConditionalFormatType, Workbook, XlsxError}; fn main() -> Result<(), XlsxError> { // Create a new Excel file object. @@ -29,8 +27,8 @@ fn main() -> Result<(), XlsxError> { // defined range. Values <= 3 will be shown with the minimum color while // values >= 7 will be shown with the maximum color. let conditional_format = ConditionalFormat3ColorScale::new() - .set_minimum(ConditionalFormatScaleType::Number, 3) - .set_maximum(ConditionalFormatScaleType::Number, 7); + .set_minimum(ConditionalFormatType::Number, 3) + .set_maximum(ConditionalFormatType::Number, 7); worksheet.add_conditional_format(2, 3, 11, 3, &conditional_format)?; diff --git a/src/conditional_format.rs b/src/conditional_format.rs index 53d7222f..48220c52 100644 --- a/src/conditional_format.rs +++ b/src/conditional_format.rs @@ -129,6 +129,8 @@ //! format. //! - [`ConditionalFormat3ColorScale`]: The 3 color scale style conditional //! format. +//! - [`ConditionalFormatDataBar`]: The Data Bar scale style conditional +//! format. //! //! # Excel's limitations on conditional format properties //! @@ -275,7 +277,10 @@ pub trait ConditionalFormat { fn validate(&self) -> Result<(), XlsxError>; /// Return the conditional format rule as an XML string. - fn get_rule_string(&self, dxf_index: Option, priority: u32, anchor: &str) -> String; + fn rule(&self, dxf_index: Option, priority: u32, range: &str, guid: &str) -> String; + + /// Return the extended x14 conditional format rule as an XML string. + fn x14_rule(&self, guid: &str) -> String; /// Get a mutable reference to the format object in the conditional format. fn format_as_mut(&mut self) -> Option<&mut Format>; @@ -286,6 +291,9 @@ pub trait ConditionalFormat { /// Get the multi-cell range for the conditional format, if present. fn multi_range(&self) -> String; + /// Check if the conditional format uses Excel 2010+ extensions. + fn has_extensions(&self) -> bool; + /// Clone a reference into a concrete Box type. fn box_clone(&self) -> Box; } @@ -297,10 +305,15 @@ macro_rules! generate_conditional_format_impls { self.validate() } - fn get_rule_string(&self, dxf_index: Option, priority: u32, anchor: &str) -> String { - self.get_rule_string(dxf_index, priority, anchor) + fn rule(&self, dxf_index: Option, priority: u32, range: &str, guid: &str) -> String { + self.rule(dxf_index, priority, range, guid) } + fn x14_rule(&self, guid: &str) -> String { + self.x14_rule(guid) + } + + fn format_as_mut(&mut self) -> Option<&mut Format> { self.format_as_mut() } @@ -313,6 +326,10 @@ macro_rules! generate_conditional_format_impls { self.multi_range() } + fn has_extensions(&self) -> bool { + self.has_extensions() + } + fn box_clone(&self) -> Box { Box::new(self.clone()) } @@ -330,6 +347,7 @@ generate_conditional_format_impls!( ConditionalFormatTop ConditionalFormat2ColorScale ConditionalFormat3ColorScale + ConditionalFormatDataBar ); // ----------------------------------------------------------------------- @@ -514,6 +532,7 @@ pub struct ConditionalFormatCell { criteria: ConditionalFormatCellCriteria, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -528,6 +547,7 @@ impl ConditionalFormatCell { criteria: ConditionalFormatCellCriteria::None, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -718,11 +738,12 @@ impl ConditionalFormatCell { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "cellIs".to_string())]; @@ -756,6 +777,12 @@ impl ConditionalFormatCell { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -844,6 +871,7 @@ pub struct ConditionalFormatBlank { is_inverted: bool, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -856,6 +884,7 @@ impl ConditionalFormatBlank { is_inverted: false, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -878,14 +907,16 @@ impl ConditionalFormatBlank { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - anchor: &str, + range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![]; + let anchor = &range_to_anchor(range); // Write the type. if self.is_inverted { @@ -921,6 +952,12 @@ impl ConditionalFormatBlank { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -1017,6 +1054,7 @@ pub struct ConditionalFormatError { is_inverted: bool, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -1029,6 +1067,7 @@ impl ConditionalFormatError { is_inverted: false, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -1051,14 +1090,16 @@ impl ConditionalFormatError { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - anchor: &str, + range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![]; + let anchor = &range_to_anchor(range); // Write the type. if self.is_inverted { @@ -1094,6 +1135,12 @@ impl ConditionalFormatError { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -1191,6 +1238,7 @@ pub struct ConditionalFormatDuplicate { is_inverted: bool, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -1203,6 +1251,7 @@ impl ConditionalFormatDuplicate { is_inverted: false, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -1225,11 +1274,12 @@ impl ConditionalFormatDuplicate { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![]; @@ -1258,6 +1308,12 @@ impl ConditionalFormatDuplicate { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -1356,6 +1412,7 @@ pub struct ConditionalFormatAverage { criteria: ConditionalFormatAverageCriteria, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -1368,6 +1425,7 @@ impl ConditionalFormatAverage { criteria: ConditionalFormatAverageCriteria::AboveAverage, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -1395,11 +1453,12 @@ impl ConditionalFormatAverage { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "aboveAverage".to_string())]; @@ -1471,6 +1530,12 @@ impl ConditionalFormatAverage { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -1573,6 +1638,7 @@ pub struct ConditionalFormatTop { is_percent: bool, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -1587,6 +1653,7 @@ impl ConditionalFormatTop { is_percent: false, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -1631,11 +1698,12 @@ impl ConditionalFormatTop { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "top10".to_string())]; @@ -1668,6 +1736,12 @@ impl ConditionalFormatTop { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -1787,6 +1861,7 @@ pub struct ConditionalFormatText { criteria: ConditionalFormatTextCriteria, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -1800,6 +1875,7 @@ impl ConditionalFormatText { criteria: ConditionalFormatTextCriteria::Contains, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -1845,15 +1921,17 @@ impl ConditionalFormatText { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - anchor: &str, + range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![]; let text = self.value.clone(); + let anchor = &range_to_anchor(range); // Set the rule attributes based on the criteria. let formula = match self.criteria { @@ -1898,6 +1976,12 @@ impl ConditionalFormatText { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -2025,6 +2109,7 @@ pub struct ConditionalFormatDate { criteria: ConditionalFormatDateCriteria, multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -2037,6 +2122,7 @@ impl ConditionalFormatDate { criteria: ConditionalFormatDateCriteria::Yesterday, multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -2064,14 +2150,16 @@ impl ConditionalFormatDate { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - anchor: &str, + range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "timePeriod".to_string())]; + let anchor = &range_to_anchor(range); // Set the rule attributes based on the criteria. let formula = match self.criteria { @@ -2135,6 +2223,12 @@ impl ConditionalFormatDate { writer.read_to_string() } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -2241,8 +2335,8 @@ impl ConditionalFormatDate { /// #[derive(Clone)] pub struct ConditionalFormat2ColorScale { - min_type: ConditionalFormatScaleType, - max_type: ConditionalFormatScaleType, + min_type: ConditionalFormatType, + max_type: ConditionalFormatType, min_value: ConditionalFormatValue, max_value: ConditionalFormatValue, min_color: Color, @@ -2250,6 +2344,7 @@ pub struct ConditionalFormat2ColorScale { multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -2260,14 +2355,15 @@ impl ConditionalFormat2ColorScale { #[allow(clippy::unreadable_literal)] // For RGB colors. pub fn new() -> ConditionalFormat2ColorScale { ConditionalFormat2ColorScale { - min_type: ConditionalFormatScaleType::Lowest, - max_type: ConditionalFormatScaleType::Highest, + min_type: ConditionalFormatType::Lowest, + max_type: ConditionalFormatType::Highest, min_value: 0.into(), max_value: 0.into(), min_color: Color::RGB(0xFFEF9C), max_color: Color::RGB(0x63BE7B), multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -2280,7 +2376,7 @@ impl ConditionalFormat2ColorScale { /// /// # Parameters /// - /// * `rule_type`: a [`ConditionalFormatScaleType`] enum value. + /// * `rule_type`: a [`ConditionalFormatType`] enum value. /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] /// such as numbers, dates, times and formula ranges. String values are /// ignored in this type of conditional format. @@ -2294,7 +2390,7 @@ impl ConditionalFormat2ColorScale { /// # // This code is available in examples/doc_conditional_format_2color_set_minimum.rs /// # /// # use rust_xlsxwriter::{ - /// # ConditionalFormat2ColorScale, ConditionalFormatScaleType, Workbook, XlsxError, + /// # ConditionalFormat2ColorScale, ConditionalFormatType, Workbook, XlsxError, /// # }; /// # /// # fn main() -> Result<(), XlsxError> { @@ -2317,8 +2413,8 @@ impl ConditionalFormat2ColorScale { /// // defined range. Values <= 3 will be shown with the minimum color while /// // values >= 7 will be shown with the maximum color. /// let conditional_format = ConditionalFormat2ColorScale::new() - /// .set_minimum(ConditionalFormatScaleType::Number, 3) - /// .set_maximum(ConditionalFormatScaleType::Number, 7); + /// .set_minimum(ConditionalFormatType::Number, 3) + /// .set_maximum(ConditionalFormatType::Number, 7); /// /// worksheet.add_conditional_format(2, 3, 11, 3, &conditional_format)?; /// # @@ -2335,7 +2431,7 @@ impl ConditionalFormat2ColorScale { /// pub fn set_minimum( mut self, - rule_type: ConditionalFormatScaleType, + rule_type: ConditionalFormatType, value: impl Into, ) -> ConditionalFormat2ColorScale { let value = value.into(); @@ -2346,8 +2442,8 @@ impl ConditionalFormat2ColorScale { } // Check that percent and percentile are in range 0..100. - if rule_type == ConditionalFormatScaleType::Percent - || rule_type == ConditionalFormatScaleType::Percentile + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile { if let Ok(num) = value.value.parse::() { if !(0.0..=100.0).contains(&num) { @@ -2358,8 +2454,9 @@ impl ConditionalFormat2ColorScale { } // The highest and lowest options cannot be set by the user. - if rule_type != ConditionalFormatScaleType::Lowest - && rule_type != ConditionalFormatScaleType::Highest + if rule_type != ConditionalFormatType::Lowest + && rule_type != ConditionalFormatType::Highest + && rule_type != ConditionalFormatType::Automatic { self.min_type = rule_type; self.min_value = value; @@ -2376,14 +2473,14 @@ impl ConditionalFormat2ColorScale { /// /// # Parameters /// - /// * `rule_type`: a [`ConditionalFormatScaleType`] enum value. + /// * `rule_type`: a [`ConditionalFormatType`] enum value. /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] /// such as numbers, dates, times and formula ranges. String values are /// ignored in this type of conditional format. /// pub fn set_maximum( mut self, - rule_type: ConditionalFormatScaleType, + rule_type: ConditionalFormatType, value: impl Into, ) -> ConditionalFormat2ColorScale { let value = value.into(); @@ -2394,8 +2491,8 @@ impl ConditionalFormat2ColorScale { } // Check that percent and percentile are in range 0..100. - if rule_type == ConditionalFormatScaleType::Percent - || rule_type == ConditionalFormatScaleType::Percentile + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile { if let Ok(num) = value.value.parse::() { if !(0.0..=100.0).contains(&num) { @@ -2406,8 +2503,9 @@ impl ConditionalFormat2ColorScale { } // The highest and lowest options cannot be set by the user. - if rule_type != ConditionalFormatScaleType::Lowest - && rule_type != ConditionalFormatScaleType::Highest + if rule_type != ConditionalFormatType::Lowest + && rule_type != ConditionalFormatType::Highest + && rule_type != ConditionalFormatType::Automatic { self.max_type = rule_type; self.max_value = value; @@ -2514,11 +2612,12 @@ impl ConditionalFormat2ColorScale { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "colorScale".to_string())]; @@ -2554,28 +2653,29 @@ impl ConditionalFormat2ColorScale { } // Write the element. - fn write_type(writer: &mut XMLWriter, rule_type: ConditionalFormatScaleType, value: &str) { + fn write_type(writer: &mut XMLWriter, rule_type: ConditionalFormatType, value: &str) { let mut attributes = vec![]; match rule_type { - ConditionalFormatScaleType::Lowest => { + ConditionalFormatType::Lowest => { attributes.push(("type", "min".to_string())); } - ConditionalFormatScaleType::Number => { + ConditionalFormatType::Number => { attributes.push(("type", "num".to_string())); } - ConditionalFormatScaleType::Percent => { + ConditionalFormatType::Percent => { attributes.push(("type", "percent".to_string())); } - ConditionalFormatScaleType::Formula => { + ConditionalFormatType::Formula => { attributes.push(("type", "formula".to_string())); } - ConditionalFormatScaleType::Percentile => { + ConditionalFormatType::Percentile => { attributes.push(("type", "percentile".to_string())); } - ConditionalFormatScaleType::Highest => { + ConditionalFormatType::Highest => { attributes.push(("type", "max".to_string())); } + ConditionalFormatType::Automatic => {} } attributes.push(("val", value.to_string())); @@ -2589,6 +2689,12 @@ impl ConditionalFormat2ColorScale { writer.xml_empty_tag("color", &attributes); } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } } // ----------------------------------------------------------------------- @@ -2701,9 +2807,9 @@ impl ConditionalFormat2ColorScale { /// #[derive(Clone)] pub struct ConditionalFormat3ColorScale { - min_type: ConditionalFormatScaleType, - mid_type: ConditionalFormatScaleType, - max_type: ConditionalFormatScaleType, + min_type: ConditionalFormatType, + mid_type: ConditionalFormatType, + max_type: ConditionalFormatType, min_value: ConditionalFormatValue, mid_value: ConditionalFormatValue, max_value: ConditionalFormatValue, @@ -2713,6 +2819,7 @@ pub struct ConditionalFormat3ColorScale { multi_range: String, stop_if_true: bool, + has_extensions: bool, pub(crate) format: Option, } @@ -2723,9 +2830,9 @@ impl ConditionalFormat3ColorScale { #[allow(clippy::unreadable_literal)] // For RGB colors. pub fn new() -> ConditionalFormat3ColorScale { ConditionalFormat3ColorScale { - min_type: ConditionalFormatScaleType::Lowest, - mid_type: ConditionalFormatScaleType::Percentile, - max_type: ConditionalFormatScaleType::Highest, + min_type: ConditionalFormatType::Lowest, + mid_type: ConditionalFormatType::Percentile, + max_type: ConditionalFormatType::Highest, min_value: 0.into(), mid_value: 50.into(), max_value: 0.into(), @@ -2734,6 +2841,7 @@ impl ConditionalFormat3ColorScale { max_color: Color::RGB(0x63BE7B), multi_range: String::new(), stop_if_true: false, + has_extensions: false, format: None, } } @@ -2746,7 +2854,7 @@ impl ConditionalFormat3ColorScale { /// /// # Parameters /// - /// * `rule_type`: a [`ConditionalFormatScaleType`] enum value. + /// * `rule_type`: a [`ConditionalFormatType`] enum value. /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] /// such as numbers, dates, times and formula ranges. String values are /// ignored in this type of conditional format. @@ -2760,7 +2868,7 @@ impl ConditionalFormat3ColorScale { /// # // This code is available in examples/doc_conditional_format_3color_set_minimum.rs /// # /// # use rust_xlsxwriter::{ - /// # ConditionalFormat3ColorScale, ConditionalFormatScaleType, Workbook, XlsxError, + /// # ConditionalFormat3ColorScale, ConditionalFormatType, Workbook, XlsxError, /// # }; /// # /// # fn main() -> Result<(), XlsxError> { @@ -2783,8 +2891,8 @@ impl ConditionalFormat3ColorScale { /// // defined range. Values <= 3 will be shown with the minimum color while /// // values >= 7 will be shown with the maximum color. /// let conditional_format = ConditionalFormat3ColorScale::new() - /// .set_minimum(ConditionalFormatScaleType::Number, 3) - /// .set_maximum(ConditionalFormatScaleType::Number, 7); + /// .set_minimum(ConditionalFormatType::Number, 3) + /// .set_maximum(ConditionalFormatType::Number, 7); /// /// worksheet.add_conditional_format(2, 3, 11, 3, &conditional_format)?; /// # @@ -2801,7 +2909,7 @@ impl ConditionalFormat3ColorScale { /// pub fn set_minimum( mut self, - rule_type: ConditionalFormatScaleType, + rule_type: ConditionalFormatType, value: impl Into, ) -> ConditionalFormat3ColorScale { let value = value.into(); @@ -2812,8 +2920,8 @@ impl ConditionalFormat3ColorScale { } // Check that percent and percentile are in range 0..100. - if rule_type == ConditionalFormatScaleType::Percent - || rule_type == ConditionalFormatScaleType::Percentile + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile { if let Ok(num) = value.value.parse::() { if !(0.0..=100.0).contains(&num) { @@ -2824,8 +2932,9 @@ impl ConditionalFormat3ColorScale { } // The highest and lowest options cannot be set by the user. - if rule_type != ConditionalFormatScaleType::Lowest - && rule_type != ConditionalFormatScaleType::Highest + if rule_type != ConditionalFormatType::Lowest + && rule_type != ConditionalFormatType::Highest + && rule_type != ConditionalFormatType::Automatic { self.min_type = rule_type; self.min_value = value; @@ -2842,14 +2951,14 @@ impl ConditionalFormat3ColorScale { /// /// # Parameters /// - /// * `rule_type`: a [`ConditionalFormatScaleType`] enum value. + /// * `rule_type`: a [`ConditionalFormatType`] enum value. /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] /// such as numbers, dates, times and formula ranges. String values are /// ignored in this type of conditional format. /// pub fn set_midpoint( mut self, - rule_type: ConditionalFormatScaleType, + rule_type: ConditionalFormatType, value: impl Into, ) -> ConditionalFormat3ColorScale { let value = value.into(); @@ -2860,8 +2969,8 @@ impl ConditionalFormat3ColorScale { } // Check that percent and percentile are in range 0..100. - if rule_type == ConditionalFormatScaleType::Percent - || rule_type == ConditionalFormatScaleType::Percentile + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile { if let Ok(num) = value.value.parse::() { if !(0.0..=100.0).contains(&num) { @@ -2872,8 +2981,9 @@ impl ConditionalFormat3ColorScale { } // The highest and lowest options cannot be set by the user. - if rule_type != ConditionalFormatScaleType::Lowest - && rule_type != ConditionalFormatScaleType::Highest + if rule_type != ConditionalFormatType::Lowest + && rule_type != ConditionalFormatType::Highest + && rule_type != ConditionalFormatType::Automatic { self.mid_type = rule_type; self.mid_value = value; @@ -2890,14 +3000,14 @@ impl ConditionalFormat3ColorScale { /// /// # Parameters /// - /// * `rule_type`: a [`ConditionalFormatScaleType`] enum value. + /// * `rule_type`: a [`ConditionalFormatType`] enum value. /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] /// such as numbers, dates, times and formula ranges. String values are /// ignored in this type of conditional format. /// pub fn set_maximum( mut self, - rule_type: ConditionalFormatScaleType, + rule_type: ConditionalFormatType, value: impl Into, ) -> ConditionalFormat3ColorScale { let value = value.into(); @@ -2908,8 +3018,8 @@ impl ConditionalFormat3ColorScale { } // Check that percent and percentile are in range 0..100. - if rule_type == ConditionalFormatScaleType::Percent - || rule_type == ConditionalFormatScaleType::Percentile + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile { if let Ok(num) = value.value.parse::() { if !(0.0..=100.0).contains(&num) { @@ -2920,8 +3030,9 @@ impl ConditionalFormat3ColorScale { } // The highest and lowest options cannot be set by the user. - if rule_type != ConditionalFormatScaleType::Lowest - && rule_type != ConditionalFormatScaleType::Highest + if rule_type != ConditionalFormatType::Lowest + && rule_type != ConditionalFormatType::Highest + && rule_type != ConditionalFormatType::Automatic { self.max_type = rule_type; self.max_value = value; @@ -3002,7 +3113,7 @@ impl ConditionalFormat3ColorScale { /// Set the color of the midpoint in the 3 color scale. /// /// Set the midpoint color value for a 3 color scale type of conditional - /// format. By default the midpoint color is `#TODO` (todo). + /// format. By default the midpoint color is `#FFEB84` (Yellow). /// /// # Parameters /// @@ -3051,11 +3162,12 @@ impl ConditionalFormat3ColorScale { } // Return the conditional format rule as an XML string. - pub(crate) fn get_rule_string( + pub(crate) fn rule( &self, dxf_index: Option, priority: u32, - _anchor: &str, + _range: &str, + _guid: &str, ) -> String { let mut writer = XMLWriter::new(); let mut attributes = vec![("type", "colorScale".to_string())]; @@ -3093,28 +3205,29 @@ impl ConditionalFormat3ColorScale { } // Write the element. - fn write_type(writer: &mut XMLWriter, rule_type: ConditionalFormatScaleType, value: &str) { + fn write_type(writer: &mut XMLWriter, rule_type: ConditionalFormatType, value: &str) { let mut attributes = vec![]; match rule_type { - ConditionalFormatScaleType::Lowest => { + ConditionalFormatType::Lowest => { attributes.push(("type", "min".to_string())); } - ConditionalFormatScaleType::Number => { + ConditionalFormatType::Number => { attributes.push(("type", "num".to_string())); } - ConditionalFormatScaleType::Percent => { + ConditionalFormatType::Percent => { attributes.push(("type", "percent".to_string())); } - ConditionalFormatScaleType::Formula => { + ConditionalFormatType::Formula => { attributes.push(("type", "formula".to_string())); } - ConditionalFormatScaleType::Percentile => { + ConditionalFormatType::Percentile => { attributes.push(("type", "percentile".to_string())); } - ConditionalFormatScaleType::Highest => { + ConditionalFormatType::Highest => { attributes.push(("type", "max".to_string())); } + ConditionalFormatType::Automatic => {} } attributes.push(("val", value.to_string())); @@ -3128,6 +3241,546 @@ impl ConditionalFormat3ColorScale { writer.xml_empty_tag("color", &attributes); } + + // Return an extended x14 rule for conditional formats that support it. + #[allow(clippy::unused_self)] + pub(crate) fn x14_rule(&self, _guid: &str) -> String { + String::new() + } +} + +// ----------------------------------------------------------------------- +// ConditionalFormatDataBar +// ----------------------------------------------------------------------- + +/// The `ConditionalFormatDataBar` struct represents a Data Bar +/// conditional format. +/// +/// `ConditionalFormatDataBar` is used to represent a Cell style conditional +/// format in Excel. A Data Bar Cell conditional format shows a per cell +/// color gradient from the minimum value to the maximum value. +/// +/// +/// +/// For more information see [Working with Conditional +/// Formats](crate::conditional_format). +/// +/// # Examples +/// +/// +/// This creates conditional format rules like this: +/// +/// +/// +/// And the following output file: +/// +/// +/// +/// +#[derive(Clone)] +pub struct ConditionalFormatDataBar { + min_type: ConditionalFormatType, + max_type: ConditionalFormatType, + min_value: ConditionalFormatValue, + max_value: ConditionalFormatValue, + bar_color: Color, + border_color: Color, + negative_fill_color: Color, + negative_border_color: Color, + axis_color: Color, + no_border: bool, + solid_bar: bool, + direction: ConditionalFormatDataBarDirection, + + multi_range: String, + stop_if_true: bool, + has_extensions: bool, + pub(crate) format: Option, +} + +/// **Section 1**: The following methods are specific to `ConditionalFormatDataBar`. +impl ConditionalFormatDataBar { + /// Create a new Cell conditional format struct. + #[allow(clippy::new_without_default)] + #[allow(clippy::unreadable_literal)] // For RGB colors. + pub fn new() -> ConditionalFormatDataBar { + ConditionalFormatDataBar { + min_type: ConditionalFormatType::Automatic, + max_type: ConditionalFormatType::Automatic, + min_value: 0.into(), + max_value: 0.into(), + bar_color: Color::RGB(0x638EC6), + border_color: Color::RGB(0x638EC6), + negative_fill_color: Color::RGB(0xFF0000), + negative_border_color: Color::RGB(0xFF0000), + axis_color: Color::RGB(0x000000), + no_border: false, + solid_bar: false, + direction: ConditionalFormatDataBarDirection::Context, + + multi_range: String::new(), + stop_if_true: false, + has_extensions: true, + format: None, + } + } + + /// Set the type and value of the minimum in the data bar. + /// + /// Set the minimum type (number, percent, formula or percentile) and value + /// for a data bar type of conditional format. By default the minimum + /// is the lowest value in the conditional formatting range. + /// + /// # Parameters + /// + /// * `rule_type`: a [`ConditionalFormatType`] enum value. + /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] + /// such as numbers, dates, times and formula ranges. String values are + /// ignored in this type of conditional format. + /// + /// # Examples + /// + /// + /// Output file: + /// + /// + /// + pub fn set_minimum( + mut self, + rule_type: ConditionalFormatType, + value: impl Into, + ) -> ConditionalFormatDataBar { + let value = value.into(); + + // This type of rule doesn't support strings. + if value.is_string { + return self; + } + + // Check that percent and percentile are in range 0..100. + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile + { + if let Ok(num) = value.value.parse::() { + if !(0.0..=100.0).contains(&num) { + eprintln!("Percent/percentile '{num}' must be in Excel range: 0..100."); + return self; + } + } + } + // The highest and lowest options cannot be set by the user. TODO + if rule_type != ConditionalFormatType::Lowest && rule_type != ConditionalFormatType::Highest + { + self.min_type = rule_type; + self.min_value = value; + } + + self + } + + /// Set the type and value of the maximum in the data bar. + /// + /// Set the maximum type (number, percent, formula or percentile) and value + /// for a data bar type of conditional format. By default the maximum + /// is the highest value in the conditional formatting range. + /// + /// # Parameters + /// + /// * `rule_type`: a [`ConditionalFormatType`] enum value. + /// * `value` - Any type that can convert into a [`ConditionalFormatValue`] + /// such as numbers, dates, times and formula ranges. String values are + /// ignored in this type of conditional format. + /// + pub fn set_maximum( + mut self, + rule_type: ConditionalFormatType, + value: impl Into, + ) -> ConditionalFormatDataBar { + let value = value.into(); + + // This type of rule doesn't support strings. + if value.is_string { + return self; + } + + // Check that percent and percentile are in range 0..100. + if rule_type == ConditionalFormatType::Percent + || rule_type == ConditionalFormatType::Percentile + { + if let Ok(num) = value.value.parse::() { + if !(0.0..=100.0).contains(&num) { + eprintln!("Percent/percentile '{num}' must be in Excel range: 0..100."); + return self; + } + } + } + + // The highest and lowest options cannot be set by the user. + if rule_type != ConditionalFormatType::Lowest && rule_type != ConditionalFormatType::Highest + { + self.max_type = rule_type; + self.max_value = value; + } + + self + } + + /// Set the color of the Todo in the data bar. + /// + /// Set the minimum color value for a data bar type of conditional + /// format. By default the minimum color is `#FFEF9C` (yellow). + /// + /// # Parameters + /// + /// * `color` - The color property defined by a [`Color`] enum value or a + /// type that implements the [`IntoColor`] trait. + /// + /// # Examples + /// + /// + /// Output file: + /// + /// + /// + pub fn set_bar_color(mut self, color: T) -> ConditionalFormatDataBar + where + T: IntoColor, + { + let color = color.new_color(); + if color.is_valid() { + self.bar_color = color; + self.border_color = color; + } + + self + } + + /// + pub fn set_bar_negative_color(mut self, color: T) -> ConditionalFormatDataBar + where + T: IntoColor, + { + let color = color.new_color(); + if color.is_valid() { + self.negative_fill_color = color; + } + + self + } + + /// todo + pub fn set_border_color(mut self, color: T) -> ConditionalFormatDataBar + where + T: IntoColor, + { + let color = color.new_color(); + if color.is_valid() { + self.border_color = color; + } + + self + } + + /// todo + pub fn set_no_border(mut self, enable: bool) -> ConditionalFormatDataBar { + self.no_border = enable; + + self + } + + /// todo + pub fn set_solid_bar(mut self, enable: bool) -> ConditionalFormatDataBar { + self.solid_bar = enable; + + self + } + + /// todo + pub fn set_direction( + mut self, + direction: ConditionalFormatDataBarDirection, + ) -> ConditionalFormatDataBar { + self.direction = direction; + + self + } + + /// todo - set hidden + pub fn set_classic_style(mut self) -> ConditionalFormatDataBar { + self.has_extensions = false; + + if self.min_type == ConditionalFormatType::Automatic { + self.min_type = ConditionalFormatType::Lowest; + } + if self.max_type == ConditionalFormatType::Automatic { + self.max_type = ConditionalFormatType::Highest; + } + + self + } + + // Validate the conditional format. + #[allow(clippy::unnecessary_wraps)] + #[allow(clippy::unused_self)] + pub(crate) fn validate(&self) -> Result<(), XlsxError> { + Ok(()) + } + + // Return the conditional format rule as an XML string. + pub(crate) fn rule( + &self, + dxf_index: Option, + priority: u32, + _range: &str, + guid: &str, + ) -> String { + let mut writer = XMLWriter::new(); + let mut attributes = vec![("type", "dataBar".to_string())]; + + // Set the format index if present. + if let Some(dxf_index) = dxf_index { + attributes.push(("dxfId", dxf_index.to_string())); + } + + // Set the rule priority order. + attributes.push(("priority", priority.to_string())); + + // Set the "Stop if True" property. + if self.stop_if_true { + attributes.push(("stopIfTrue", "1".to_string())); + } + + // Write the rule. + writer.xml_start_tag("cfRule", &attributes); + writer.xml_start_tag_only("dataBar"); + + Self::write_type( + &mut writer, + self.min_type, + &self.min_value.value, + self.has_extensions, + false, + ); + + Self::write_type( + &mut writer, + self.max_type, + &self.max_value.value, + self.has_extensions, + true, + ); + + Self::write_color(&mut writer, "color", self.bar_color); + + writer.xml_end_tag("dataBar"); + + if self.has_extensions { + // Write the extLst element. + Self::write_extension_list(&mut writer, guid); + } + + writer.xml_end_tag("cfRule"); + + writer.read_to_string() + } + + // Write the element. + fn write_type( + writer: &mut XMLWriter, + rule_type: ConditionalFormatType, + value: &str, + has_extensions: bool, + is_max: bool, + ) { + let mut attributes = vec![]; + + match rule_type { + ConditionalFormatType::Automatic => { + if is_max { + attributes.push(("type", "max".to_string())); + } else { + attributes.push(("type", "min".to_string())); + } + } + ConditionalFormatType::Lowest => { + attributes.push(("type", "min".to_string())); + if !has_extensions { + attributes.push(("val", value.to_string())); + } + } + ConditionalFormatType::Number => { + attributes.push(("type", "num".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Percent => { + attributes.push(("type", "percent".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Formula => { + attributes.push(("type", "formula".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Percentile => { + attributes.push(("type", "percentile".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Highest => { + attributes.push(("type", "max".to_string())); + if !has_extensions { + attributes.push(("val", value.to_string())); + } + } + } + + writer.xml_empty_tag("cfvo", &attributes); + } + + // Write the element. + fn write_color(writer: &mut XMLWriter, color_tag: &str, color: Color) { + let attributes = [("rgb", color.argb_hex_value())]; + + writer.xml_empty_tag(color_tag, &attributes); + } + + // Write the element. + fn write_extension_list(writer: &mut XMLWriter, guid: &str) { + writer.xml_start_tag_only("extLst"); + + let attributes = [ + ( + "xmlns:x14", + "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main", + ), + ("uri", "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"), + ]; + + writer.xml_start_tag("ext", &attributes); + writer.xml_data_element_only("x14:id", guid); + writer.xml_end_tag("ext"); + writer.xml_end_tag("extLst"); + } + + // Return an extended x14 rule for conditional formats that support it. + pub(crate) fn x14_rule(&self, guid: &str) -> String { + let mut writer = XMLWriter::new(); + let attributes = [("type", "dataBar".to_string()), ("id", guid.to_string())]; + + // Write the rule. + writer.xml_start_tag("x14:cfRule", &attributes); + Self::write_data_bar(&mut writer, self.solid_bar, self.no_border, self.direction); + + Self::write_x14_type(&mut writer, self.min_type, &self.min_value.value, false); + Self::write_x14_type(&mut writer, self.max_type, &self.max_value.value, true); + + // Write the color elements. + if !self.no_border { + Self::write_color(&mut writer, "x14:borderColor", self.border_color); + } + + Self::write_color( + &mut writer, + "x14:negativeFillColor", + self.negative_fill_color, + ); + + if !self.no_border { + Self::write_color( + &mut writer, + "x14:negativeBorderColor", + self.negative_border_color, + ); + } + Self::write_color(&mut writer, "x14:axisColor", self.axis_color); + + writer.xml_end_tag("x14:dataBar"); + writer.xml_end_tag("x14:cfRule"); + + writer.read_to_string() + } + + // Write the element. + fn write_data_bar( + writer: &mut XMLWriter, + solid_bar: bool, + no_border: bool, + direction: ConditionalFormatDataBarDirection, + ) { + let mut attributes = vec![ + ("minLength", "0".to_string()), + ("maxLength", "100".to_string()), + ]; + + if !no_border { + attributes.push(("border", "1".to_string())); + } + + if solid_bar { + attributes.push(("gradient", "0".to_string())); + } + + match direction { + ConditionalFormatDataBarDirection::LeftToRight => { + attributes.push(("direction", "leftToRight".to_string())); + } + ConditionalFormatDataBarDirection::RightToLeft => { + attributes.push(("direction", "rightToLeft".to_string())); + } + ConditionalFormatDataBarDirection::Context => {} + } + + if !no_border { + attributes.push(("negativeBarBorderColorSameAsPositive", "0".to_string())); + } + + writer.xml_start_tag("x14:dataBar", &attributes); + } + + // Write the element. + fn write_x14_type( + writer: &mut XMLWriter, + rule_type: ConditionalFormatType, + value: &str, + is_max: bool, + ) { + let mut attributes = vec![]; + + match rule_type { + ConditionalFormatType::Automatic => { + if is_max { + attributes.push(("type", "autoMax".to_string())); + } else { + attributes.push(("type", "autoMin".to_string())); + } + } + ConditionalFormatType::Lowest => { + attributes.push(("type", "min".to_string())); + } + ConditionalFormatType::Number => { + attributes.push(("type", "num".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Percent => { + attributes.push(("type", "percent".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Formula => { + attributes.push(("type", "formula".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Percentile => { + attributes.push(("type", "percentile".to_string())); + attributes.push(("val", value.to_string())); + } + ConditionalFormatType::Highest => { + attributes.push(("type", "max".to_string())); + } + } + + writer.xml_empty_tag("x14:cfvo", &attributes); + } } // ----------------------------------------------------------------------- @@ -3448,11 +4101,12 @@ impl fmt::Display for ConditionalFormatDateCriteria { } // ----------------------------------------------------------------------- -// ConditionalFormatScaleType +// ConditionalFormatType // ----------------------------------------------------------------------- -/// The `ConditionalFormatScaleType` enum defines the conditional format type -/// for [`ConditionalFormat2ColorScale`] and [`ConditionalFormat3ColorScale`]. +/// The `ConditionalFormatType` enum defines the conditional format type +/// for [`ConditionalFormat2ColorScale`], [`ConditionalFormat3ColorScale`] and +/// [`ConditionalFormatDataBar`]. /// /// # Examples /// @@ -3463,7 +4117,7 @@ impl fmt::Display for ConditionalFormatDateCriteria { /// # // This code is available in examples/doc_conditional_format_2color_set_minimum.rs /// # /// # use rust_xlsxwriter::{ -/// # ConditionalFormat2ColorScale, ConditionalFormatScaleType, Workbook, XlsxError, +/// # ConditionalFormat2ColorScale, ConditionalFormatType, Workbook, XlsxError, /// # }; /// # /// # fn main() -> Result<(), XlsxError> { @@ -3486,8 +4140,8 @@ impl fmt::Display for ConditionalFormatDateCriteria { /// // defined range. Values <= 3 will be shown with the minimum color while /// // values >= 7 will be shown with the maximum color. /// let conditional_format = ConditionalFormat2ColorScale::new() -/// .set_minimum(ConditionalFormatScaleType::Number, 3) -/// .set_maximum(ConditionalFormatScaleType::Number, 7); +/// .set_minimum(ConditionalFormatType::Number, 3) +/// .set_maximum(ConditionalFormatType::Number, 7); /// /// worksheet.add_conditional_format(2, 3, 11, 3, &conditional_format)?; /// # @@ -3505,31 +4159,55 @@ impl fmt::Display for ConditionalFormatDateCriteria { /// /// #[derive(Clone, Copy, PartialEq, Eq)] -pub enum ConditionalFormatScaleType { - /// Set the color scale to use the minimum value in the range. This is the - /// default for the minimum value in the scale. +pub enum ConditionalFormatType { + /// Set the color scale/data bar to use the minimum or maximum value in the range. This is the + /// default for data bars. + Automatic, + + /// Set the color scale/data bar to use the minimum value in the range. This is the + /// default for the minimum value in color scales. Lowest, - /// Set the color scale to use a number value other than the + /// Set the color scale/data bar to use a number value other than the /// maximum/minimum. Number, - /// Set the color scale to use a percentage. This must be in the range + /// Set the color scale/data bar to use a percentage. This must be in the range /// 0-100. Percent, - /// Set the color scale to use a formula value. + /// Set the color scale/data bar to use a formula value. Formula, - /// Set the color scale to use a percentile. This must be in the range + /// Set the color scale/data bar to use a percentile. This must be in the range /// 0-100. Percentile, - /// Set the color scale to use the maximum value in the range. This is the - /// default for the maximum value in the scale. + /// Set the color scale/data bar to use the maximum value in the range. This is the + /// default for the maximum value in value in color scales. Highest, } +// ----------------------------------------------------------------------- +// ConditionalFormatDataBarDirection +// ----------------------------------------------------------------------- + +/// The `ConditionalFormatDataBarDirection` enum defines the conditional format +/// directions for [`ConditionalFormatDataBar`] . +/// +/// +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ConditionalFormatDataBarDirection { + /// TODO + Context, + + /// TODO + LeftToRight, + + /// TODO + RightToLeft, +} + // ----------------------------------------------------------------------- // Generate common methods. // ----------------------------------------------------------------------- @@ -3613,6 +4291,11 @@ macro_rules! generate_conditional_common_methods { pub(crate) fn multi_range(&self) -> String { self.multi_range.clone() } + + /// Check if the conditional format uses Excel 2010+ extensions. + pub(crate) fn has_extensions(&self) -> bool { + self.has_extensions + } } )*) } @@ -3627,4 +4310,26 @@ generate_conditional_common_methods!( ConditionalFormatTop ConditionalFormat2ColorScale ConditionalFormat3ColorScale + ConditionalFormatDataBar ); + +// ----------------------------------------------------------------------- +// Common methods. +// ----------------------------------------------------------------------- + +// TODO COW +fn range_to_anchor(range: &str) -> String { + let mut anchor = range; + + // Extract cell/range from optional space separated list. + if let Some(position) = anchor.find(' ') { + anchor = &anchor[0..position]; + } + + // Extract cell from optional : separated range. + if let Some(position) = anchor.find(':') { + anchor = &anchor[0..position]; + } + + anchor.to_string() +} diff --git a/src/conditional_format/tests.rs b/src/conditional_format/tests.rs index 71dc423b..0c9ef081 100644 --- a/src/conditional_format/tests.rs +++ b/src/conditional_format/tests.rs @@ -16,14 +16,16 @@ mod conditional_format_tests { use crate::ConditionalFormatBlank; use crate::ConditionalFormatCell; use crate::ConditionalFormatCellCriteria; + use crate::ConditionalFormatDataBar; + use crate::ConditionalFormatDataBarDirection; use crate::ConditionalFormatDate; use crate::ConditionalFormatDateCriteria; use crate::ConditionalFormatDuplicate; use crate::ConditionalFormatError; - use crate::ConditionalFormatScaleType; use crate::ConditionalFormatText; use crate::ConditionalFormatTextCriteria; use crate::ConditionalFormatTop; + use crate::ConditionalFormatType; use crate::ExcelDateTime; use crate::Formula; use crate::XlsxError; @@ -35,7 +37,7 @@ mod conditional_format_tests { .set_criteria(ConditionalFormatCellCriteria::EqualTo) .set_value(5); - let got = conditional_format.get_rule_string(None, 1, ""); + let got = conditional_format.rule(None, 1, "", ""); let expected = r#"5"#; @@ -48,7 +50,7 @@ mod conditional_format_tests { .set_criteria(ConditionalFormatCellCriteria::EqualTo) .set_value("Foo"); - let got = conditional_format.get_rule_string(None, 1, ""); + let got = conditional_format.rule(None, 1, "", ""); let expected = r#""Foo""#; assert_eq!(expected, got); @@ -60,7 +62,7 @@ mod conditional_format_tests { .set_criteria(ConditionalFormatCellCriteria::EqualTo) .set_value("\"Foo\""); - let got = conditional_format.get_rule_string(None, 1, ""); + let got = conditional_format.rule(None, 1, "", ""); let expected = r#""Foo""#; assert_eq!(expected, got); @@ -72,7 +74,7 @@ mod conditional_format_tests { .set_criteria(ConditionalFormatCellCriteria::EqualTo) .set_value("Foo \" Bar"); - let got = conditional_format.get_rule_string(None, 1, ""); + let got = conditional_format.rule(None, 1, "", ""); let expected = r#""Foo "" Bar""#; assert_eq!(expected, got); @@ -1285,29 +1287,29 @@ mod conditional_format_tests { let conditional_format = ConditionalFormat2ColorScale::new() // String should be ignored. - .set_minimum(ConditionalFormatScaleType::Number, "Foo") - .set_maximum(ConditionalFormatScaleType::Number, "Foo") + .set_minimum(ConditionalFormatType::Number, "Foo") + .set_maximum(ConditionalFormatType::Number, "Foo") // High/low should be ignored. - .set_minimum(ConditionalFormatScaleType::Highest, 0) - .set_maximum(ConditionalFormatScaleType::Lowest, 0) + .set_minimum(ConditionalFormatType::Highest, 0) + .set_maximum(ConditionalFormatType::Lowest, 0) // > 100 should be ignored. - .set_minimum(ConditionalFormatScaleType::Percent, 101) - .set_maximum(ConditionalFormatScaleType::Percent, 101) + .set_minimum(ConditionalFormatType::Percent, 101) + .set_maximum(ConditionalFormatType::Percent, 101) // < 0 should be ignored. - .set_minimum(ConditionalFormatScaleType::Percentile, -1) - .set_maximum(ConditionalFormatScaleType::Percentile, -1) + .set_minimum(ConditionalFormatType::Percentile, -1) + .set_maximum(ConditionalFormatType::Percentile, -1) .set_minimum_color("FF0000") .set_maximum_color("FFFF00"); worksheet.add_conditional_format(0, 0, 9, 0, &conditional_format)?; let conditional_format = ConditionalFormat2ColorScale::new() - .set_minimum(ConditionalFormatScaleType::Number, 2.5) - .set_maximum(ConditionalFormatScaleType::Percent, 90); + .set_minimum(ConditionalFormatType::Number, 2.5) + .set_maximum(ConditionalFormatType::Percent, 90); worksheet.add_conditional_format(0, 2, 9, 2, &conditional_format)?; let conditional_format = ConditionalFormat2ColorScale::new() - .set_minimum(ConditionalFormatScaleType::Formula, Formula::new("=$M$20")) - .set_maximum(ConditionalFormatScaleType::Percentile, 90); + .set_minimum(ConditionalFormatType::Formula, Formula::new("=$M$20")) + .set_maximum(ConditionalFormatType::Percentile, 90); worksheet.add_conditional_format(0, 4, 9, 4, &conditional_format)?; worksheet.assemble_xml_file(); @@ -1487,4 +1489,737 @@ mod conditional_format_tests { Ok(()) } + + #[test] + fn conditional_format_14() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + worksheet.write(0, 0, 1)?; + worksheet.write(1, 0, 2)?; + worksheet.write(2, 0, 3)?; + worksheet.write(3, 0, 4)?; + worksheet.write(4, 0, 5)?; + worksheet.write(5, 0, 6)?; + worksheet.write(6, 0, 7)?; + worksheet.write(7, 0, 8)?; + worksheet.write(8, 0, 9)?; + worksheet.write(9, 0, 10)?; + worksheet.write(10, 0, 11)?; + worksheet.write(11, 0, 12)?; + + let conditional_format = ConditionalFormatDataBar::new().set_classic_style(); + + worksheet.add_conditional_format(0, 0, 11, 0, &conditional_format)?; + + 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 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn conditional_format_19() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + worksheet.write(0, 0, 1)?; + worksheet.write(1, 0, 2)?; + worksheet.write(2, 0, 3)?; + worksheet.write(3, 0, 4)?; + worksheet.write(4, 0, 5)?; + worksheet.write(5, 0, 6)?; + worksheet.write(6, 0, 7)?; + worksheet.write(7, 0, 8)?; + worksheet.write(8, 0, 9)?; + worksheet.write(9, 0, 10)?; + worksheet.write(10, 0, 11)?; + worksheet.write(11, 0, 12)?; + + let conditional_format = ConditionalFormatDataBar::new() + .set_minimum(ConditionalFormatType::Number, 5) + .set_maximum(ConditionalFormatType::Percent, 90) + .set_bar_color("8DB4E3") + .set_classic_style(); + + worksheet.add_conditional_format(0, 0, 11, 0, &conditional_format)?; + + 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 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + + + + + + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_bar_01() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let conditional_format = ConditionalFormatDataBar::new().set_classic_style(); + + worksheet.add_conditional_format(0, 0, 0, 0, &conditional_format)?; + + 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_bar_02() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let conditional_format = ConditionalFormatDataBar::new(); + + worksheet.add_conditional_format(0, 0, 0, 0, &conditional_format)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000001} + + + + + + + + + + + + + + + + + + + + A1 + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_bar_03() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let conditional_format = ConditionalFormatDataBar::new(); + worksheet.add_conditional_format(0, 0, 0, 0, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new().set_bar_color("63C384"); + worksheet.add_conditional_format(1, 0, 1, 1, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new().set_bar_color("FF555A"); + worksheet.add_conditional_format(2, 0, 2, 2, &conditional_format)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000001} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000002} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000003} + + + + + + + + + + + + + + + + + + + + A1 + + + + + + + + + + + + + A2:B2 + + + + + + + + + + + + + A3:C3 + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_bar_04() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let conditional_format = ConditionalFormatDataBar::new().set_solid_bar(true); + worksheet.add_conditional_format(0, 0, 0, 0, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new() + .set_bar_color("63C384") + .set_no_border(true); + worksheet.add_conditional_format(1, 0, 1, 1, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new() + .set_bar_color("FF555A") + .set_border_color("FF0000"); + worksheet.add_conditional_format(2, 0, 2, 2, &conditional_format)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000001} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000002} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000003} + + + + + + + + + + + + + + + + + + + + A1 + + + + + + + + + + + A2:B2 + + + + + + + + + + + + + A3:C3 + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } + + #[test] + fn data_bar_05() -> Result<(), XlsxError> { + let mut worksheet = Worksheet::new(); + worksheet.set_selected(true); + + let conditional_format = ConditionalFormatDataBar::new() + .set_direction(ConditionalFormatDataBarDirection::LeftToRight); + worksheet.add_conditional_format(0, 0, 0, 0, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new() + .set_bar_color("63C384") + .set_direction(ConditionalFormatDataBarDirection::RightToLeft); + worksheet.add_conditional_format(1, 0, 1, 1, &conditional_format)?; + + let conditional_format = ConditionalFormatDataBar::new() + .set_bar_color("FF555A") + .set_bar_negative_color("FFFF00"); + worksheet.add_conditional_format(2, 0, 2, 2, &conditional_format)?; + + worksheet.assemble_xml_file(); + + let got = worksheet.writer.read_to_str(); + let got = xml_to_vec(got); + + let expected = xml_to_vec( + r#" + + + + + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000001} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000002} + + + + + + + + + + + + + + {DA7ABA51-AAAA-BBBB-0001-000000000003} + + + + + + + + + + + + + + + + + + + + A1 + + + + + + + + + + + + + A2:B2 + + + + + + + + + + + + + A3:C3 + + + + + + "#, + ); + + assert_eq!(expected, got); + + Ok(()) + } } diff --git a/src/workbook.rs b/src/workbook.rs index acae8c99..87b93939 100644 --- a/src/workbook.rs +++ b/src/workbook.rs @@ -1173,6 +1173,9 @@ impl Workbook { // Perform the autofilter row hiding. worksheet.hide_autofilter_rows(); + + // Set the index of the worksheets. + worksheet.sheet_index = i; } // Convert the images in the workbooks into drawing files and rel links. diff --git a/src/worksheet.rs b/src/worksheet.rs index 6c68173e..b1b06209 100644 --- a/src/worksheet.rs +++ b/src/worksheet.rs @@ -124,6 +124,7 @@ const COLUMN_LETTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; pub struct Worksheet { pub(crate) writer: XMLWriter, pub(crate) name: String, + pub(crate) sheet_index: usize, pub(crate) active: bool, pub(crate) selected: bool, pub(crate) visible: Visible, @@ -212,6 +213,8 @@ pub struct Worksheet { has_drawing_object_linkage: bool, cells_with_autofilter: HashSet<(RowNum, ColNum)>, conditional_formats: BTreeMap>>, + use_x14_extensions: bool, + has_2010_data_bars: bool, } impl Default for Worksheet { @@ -305,6 +308,7 @@ impl Worksheet { Worksheet { writer, name: String::new(), + sheet_index: 0, active: false, selected: false, visible: Visible::Default, @@ -392,6 +396,8 @@ impl Worksheet { has_drawing_object_linkage: false, cells_with_autofilter: HashSet::new(), conditional_formats: BTreeMap::new(), + use_x14_extensions: false, + has_2010_data_bars: false, } } @@ -5162,6 +5168,12 @@ impl Worksheet { // Validate the conditional format. conditional_format.validate()?; + // Check for extended Excel 2010 data bars. + if conditional_format.has_extensions() { + self.use_x14_extensions = true; + self.has_2010_data_bars = true; + } + // Set the dxf format local index if required. if let Some(format) = conditional_format.format_as_mut() { format.dxf_index = self.format_dxf_index(format); @@ -9715,16 +9727,39 @@ impl Worksheet { self.write_table_parts(); } + // Write the extLst element. + if self.use_x14_extensions { + self.write_extensions(); + } + // Close the worksheet tag. self.writer.xml_end_tag("worksheet"); } // Write the element. fn write_worksheet(&mut self) { - let xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; - let xmlns_r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; + let mut attributes = vec![ + ( + "xmlns", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + ), + ( + "xmlns:r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + ), + ]; - let attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)]; + if self.use_x14_extensions { + attributes.push(( + "xmlns:mc", + "http://schemas.openxmlformats.org/markup-compatibility/2006", + )); + attributes.push(( + "xmlns:x14ac", + "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac", + )); + attributes.push(("mc:Ignorable", "x14ac")); + } self.writer.xml_start_tag("worksheet", &attributes); } @@ -9955,7 +9990,11 @@ impl Worksheet { // Write the element. fn write_sheet_format_pr(&mut self) { - let attributes = [("defaultRowHeight", "15")]; + let mut attributes = vec![("defaultRowHeight", "15")]; + + if self.use_x14_extensions { + attributes.push(("x14ac:dyDescent", "0.25")); + } self.writer.xml_empty_tag("sheetFormatPr", &attributes); } @@ -10010,19 +10049,18 @@ impl Worksheet { // Write the element. fn write_conditional_formats(&mut self) { let mut priority = 1; + let mut data_bar_count = 1; + for (cell_range, conditional_formats) in &self.conditional_formats { let attributes = [("sqref", cell_range.as_str())]; - let mut anchor = cell_range.as_str(); - // Extract cell/range from optional space separated list. - if let Some(position) = anchor.find(' ') { - anchor = &anchor[0..position]; - } - - // Extract cell from optional : separated range. - if let Some(position) = anchor.find(':') { - anchor = &anchor[0..position]; - } + // Create a pseudo GUID for each unique Excel 2010 data bar. + let guid = format!( + "{{DA7ABA51-AAAA-BBBB-{:04X}-{:012X}}}", + self.sheet_index + 1, + data_bar_count + ); + data_bar_count += 1; self.writer .xml_start_tag("conditionalFormatting", &attributes); @@ -10034,7 +10072,7 @@ impl Worksheet { dxf_index = Some(self.global_dxf_indices[local_index as usize]); } - let rule = conditional_format.get_rule_string(dxf_index, priority, anchor); + let rule = conditional_format.rule(dxf_index, priority, cell_range, &guid); self.writer.xml_raw_string(&rule); priority += 1; } @@ -10997,6 +11035,65 @@ impl Worksheet { self.writer.xml_empty_tag("brk", &attributes); } + + // Write the element. + fn write_extensions(&mut self) { + let attributes = [ + ( + "xmlns:x14", + "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main", + ), + ("uri", "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"), + ]; + + self.writer.xml_start_tag_only("extLst"); + self.writer.xml_start_tag("ext", &attributes); + + if self.has_2010_data_bars { + // Write the x14:conditionalFormattings element. + self.write_conditional_formattings(); + } + + self.writer.xml_end_tag("ext"); + + self.writer.xml_end_tag("extLst"); + } + + // Write the element. + fn write_conditional_formattings(&mut self) { + self.writer.xml_start_tag_only("x14:conditionalFormattings"); + + let mut data_bar_count = 1; + for (cell_range, conditional_formats) in &self.conditional_formats { + for conditional_format in conditional_formats { + if conditional_format.has_extensions() { + let attributes = [( + "xmlns:xm", + "http://schemas.microsoft.com/office/excel/2006/main", + )]; + + self.writer + .xml_start_tag("x14:conditionalFormatting", &attributes); + + // Create a pseudo GUID for each unique Excel 2010 data bar. + let guid = format!( + "{{DA7ABA51-AAAA-BBBB-{:04X}-{:012X}}}", + self.sheet_index + 1, + data_bar_count + ); + data_bar_count += 1; + + let rule = conditional_format.x14_rule(&guid); + self.writer.xml_raw_string(&rule); + + self.writer.xml_data_element_only("xm:sqref", cell_range); + self.writer.xml_end_tag("x14:conditionalFormatting"); + } + } + } + + self.writer.xml_end_tag("x14:conditionalFormattings"); + } } // -----------------------------------------------------------------------