Skip to main content

sheetkit_core/
conditional.rs

1//! Conditional formatting builder and utilities.
2//!
3//! Provides a high-level API for adding, querying, and removing conditional
4//! formatting rules on worksheet cells. Supports cell-is comparisons,
5//! formula-based rules, color scales, data bars, duplicate/unique values,
6//! top/bottom N, above/below average, and text-based rules.
7
8use crate::error::Result;
9use crate::style::{
10    BorderLineStyle, BorderSideStyle, BorderStyle, FillStyle, FontStyle, NumFmtStyle, PatternType,
11    StyleColor,
12};
13use sheetkit_xml::styles::{Dxf, Dxfs, NumFmt, StyleSheet};
14use sheetkit_xml::worksheet::{
15    CfColor, CfColorScale, CfDataBar, CfRule, CfVo, ConditionalFormatting, WorksheetXml,
16};
17
18/// Comparison operator for CellIs conditional formatting rules.
19#[derive(Debug, Clone, PartialEq)]
20pub enum CfOperator {
21    LessThan,
22    LessThanOrEqual,
23    Equal,
24    NotEqual,
25    GreaterThanOrEqual,
26    GreaterThan,
27    Between,
28    NotBetween,
29}
30
31impl CfOperator {
32    /// Convert to the XML attribute string.
33    pub fn as_str(&self) -> &str {
34        match self {
35            CfOperator::LessThan => "lessThan",
36            CfOperator::LessThanOrEqual => "lessThanOrEqual",
37            CfOperator::Equal => "equal",
38            CfOperator::NotEqual => "notEqual",
39            CfOperator::GreaterThanOrEqual => "greaterThanOrEqual",
40            CfOperator::GreaterThan => "greaterThan",
41            CfOperator::Between => "between",
42            CfOperator::NotBetween => "notBetween",
43        }
44    }
45
46    /// Parse from the XML attribute string.
47    pub fn parse(s: &str) -> Option<Self> {
48        match s {
49            "lessThan" => Some(CfOperator::LessThan),
50            "lessThanOrEqual" => Some(CfOperator::LessThanOrEqual),
51            "equal" => Some(CfOperator::Equal),
52            "notEqual" => Some(CfOperator::NotEqual),
53            "greaterThanOrEqual" => Some(CfOperator::GreaterThanOrEqual),
54            "greaterThan" => Some(CfOperator::GreaterThan),
55            "between" => Some(CfOperator::Between),
56            "notBetween" => Some(CfOperator::NotBetween),
57            _ => None,
58        }
59    }
60}
61
62/// Value type for color scale and data bar thresholds.
63#[derive(Debug, Clone, PartialEq)]
64pub enum CfValueType {
65    Num,
66    Percent,
67    Min,
68    Max,
69    Percentile,
70    Formula,
71}
72
73impl CfValueType {
74    /// Convert to the XML attribute string.
75    pub fn as_str(&self) -> &str {
76        match self {
77            CfValueType::Num => "num",
78            CfValueType::Percent => "percent",
79            CfValueType::Min => "min",
80            CfValueType::Max => "max",
81            CfValueType::Percentile => "percentile",
82            CfValueType::Formula => "formula",
83        }
84    }
85
86    /// Parse from the XML attribute string.
87    pub fn parse(s: &str) -> Option<Self> {
88        match s {
89            "num" => Some(CfValueType::Num),
90            "percent" => Some(CfValueType::Percent),
91            "min" => Some(CfValueType::Min),
92            "max" => Some(CfValueType::Max),
93            "percentile" => Some(CfValueType::Percentile),
94            "formula" => Some(CfValueType::Formula),
95            _ => None,
96        }
97    }
98}
99
100/// The type of conditional formatting rule.
101#[derive(Debug, Clone, PartialEq)]
102pub enum ConditionalFormatType {
103    /// Cell value comparison (e.g., greater than, between).
104    CellIs {
105        operator: CfOperator,
106        formula: String,
107        /// Optional second formula for Between/NotBetween.
108        formula2: Option<String>,
109    },
110    /// Formula-based rule.
111    Expression { formula: String },
112    /// Color scale (2 or 3 color gradient).
113    ColorScale {
114        min_type: CfValueType,
115        min_value: Option<String>,
116        min_color: String,
117        mid_type: Option<CfValueType>,
118        mid_value: Option<String>,
119        mid_color: Option<String>,
120        max_type: CfValueType,
121        max_value: Option<String>,
122        max_color: String,
123    },
124    /// Data bar visualization.
125    DataBar {
126        min_type: CfValueType,
127        min_value: Option<String>,
128        max_type: CfValueType,
129        max_value: Option<String>,
130        color: String,
131        show_value: bool,
132    },
133    /// Duplicate values.
134    DuplicateValues,
135    /// Unique values.
136    UniqueValues,
137    /// Top N values.
138    Top10 { rank: u32, percent: bool },
139    /// Bottom N values.
140    Bottom10 { rank: u32, percent: bool },
141    /// Above or below average.
142    AboveAverage { above: bool, equal_average: bool },
143    /// Cells that contain blanks.
144    ContainsBlanks,
145    /// Cells that do not contain blanks.
146    NotContainsBlanks,
147    /// Cells that contain errors.
148    ContainsErrors,
149    /// Cells that do not contain errors.
150    NotContainsErrors,
151    /// Cells containing specific text.
152    ContainsText { text: String },
153    /// Cells not containing specific text.
154    NotContainsText { text: String },
155    /// Cells beginning with specific text.
156    BeginsWith { text: String },
157    /// Cells ending with specific text.
158    EndsWith { text: String },
159}
160
161/// Style applied by a conditional formatting rule.
162///
163/// Uses differential formatting (DXF): only the properties that differ from
164/// the cell's base style are specified.
165#[derive(Debug, Clone, Default)]
166pub struct ConditionalStyle {
167    pub font: Option<FontStyle>,
168    pub fill: Option<FillStyle>,
169    pub border: Option<BorderStyle>,
170    pub num_fmt: Option<NumFmtStyle>,
171}
172
173/// A single conditional formatting rule with its style.
174#[derive(Debug, Clone)]
175pub struct ConditionalFormatRule {
176    /// The type of conditional formatting rule.
177    pub rule_type: ConditionalFormatType,
178    /// The differential style to apply when the rule matches.
179    pub format: Option<ConditionalStyle>,
180    /// Rule priority (lower numbers are evaluated first).
181    pub priority: Option<u32>,
182    /// If true, no rules with lower priority are applied when this rule matches.
183    pub stop_if_true: bool,
184}
185
186/// Convert a `ConditionalStyle` to an XML `Dxf` and add it to the stylesheet.
187/// Returns the DXF index.
188fn add_dxf(stylesheet: &mut StyleSheet, style: &ConditionalStyle) -> u32 {
189    let dxf = conditional_style_to_dxf(style);
190
191    let dxfs = stylesheet.dxfs.get_or_insert_with(|| Dxfs {
192        count: Some(0),
193        dxfs: Vec::new(),
194    });
195
196    let id = dxfs.dxfs.len() as u32;
197    dxfs.dxfs.push(dxf);
198    dxfs.count = Some(dxfs.dxfs.len() as u32);
199    id
200}
201
202/// Convert a `ConditionalStyle` to the XML `Dxf` struct.
203fn conditional_style_to_dxf(style: &ConditionalStyle) -> Dxf {
204    use sheetkit_xml::styles::{
205        BoolVal, Border, BorderSide, Fill, Font, FontName, FontSize, PatternFill, Underline,
206    };
207
208    let font = style.font.as_ref().map(|f| Font {
209        b: if f.bold {
210            Some(BoolVal { val: None })
211        } else {
212            None
213        },
214        i: if f.italic {
215            Some(BoolVal { val: None })
216        } else {
217            None
218        },
219        strike: if f.strikethrough {
220            Some(BoolVal { val: None })
221        } else {
222            None
223        },
224        u: if f.underline {
225            Some(Underline { val: None })
226        } else {
227            None
228        },
229        sz: f.size.map(|val| FontSize { val }),
230        color: f.color.as_ref().map(style_color_to_xml_color),
231        name: f.name.as_ref().map(|val| FontName { val: val.clone() }),
232        family: None,
233        scheme: None,
234    });
235
236    let fill = style.fill.as_ref().map(|f| Fill {
237        pattern_fill: Some(PatternFill {
238            pattern_type: Some(pattern_type_str(&f.pattern).to_string()),
239            fg_color: f.fg_color.as_ref().map(style_color_to_xml_color),
240            bg_color: f.bg_color.as_ref().map(style_color_to_xml_color),
241        }),
242        gradient_fill: None,
243    });
244
245    let border = style.border.as_ref().map(|b| {
246        let side_to_xml = |s: &BorderSideStyle| BorderSide {
247            style: Some(border_line_str(&s.style).to_string()),
248            color: s.color.as_ref().map(style_color_to_xml_color),
249        };
250        Border {
251            diagonal_up: None,
252            diagonal_down: None,
253            left: b.left.as_ref().map(side_to_xml),
254            right: b.right.as_ref().map(side_to_xml),
255            top: b.top.as_ref().map(side_to_xml),
256            bottom: b.bottom.as_ref().map(side_to_xml),
257            diagonal: b.diagonal.as_ref().map(side_to_xml),
258        }
259    });
260
261    let num_fmt = style.num_fmt.as_ref().map(|nf| match nf {
262        NumFmtStyle::Builtin(id) => NumFmt {
263            num_fmt_id: *id,
264            format_code: String::new(),
265        },
266        NumFmtStyle::Custom(code) => NumFmt {
267            num_fmt_id: 164,
268            format_code: code.clone(),
269        },
270    });
271
272    Dxf {
273        font,
274        num_fmt,
275        fill,
276        border,
277    }
278}
279
280/// Convert a `StyleColor` to an XML `Color`.
281fn style_color_to_xml_color(color: &StyleColor) -> sheetkit_xml::styles::Color {
282    sheetkit_xml::styles::Color {
283        auto: None,
284        indexed: match color {
285            StyleColor::Indexed(i) => Some(*i),
286            _ => None,
287        },
288        rgb: match color {
289            StyleColor::Rgb(rgb) => Some(rgb.clone()),
290            _ => None,
291        },
292        theme: match color {
293            StyleColor::Theme(t) => Some(*t),
294            _ => None,
295        },
296        tint: None,
297    }
298}
299
300/// Get the string for a PatternType.
301fn pattern_type_str(pattern: &PatternType) -> &str {
302    match pattern {
303        PatternType::None => "none",
304        PatternType::Solid => "solid",
305        PatternType::Gray125 => "gray125",
306        PatternType::DarkGray => "darkGray",
307        PatternType::MediumGray => "mediumGray",
308        PatternType::LightGray => "lightGray",
309    }
310}
311
312/// Get the string for a BorderLineStyle.
313fn border_line_str(style: &BorderLineStyle) -> &str {
314    match style {
315        BorderLineStyle::Thin => "thin",
316        BorderLineStyle::Medium => "medium",
317        BorderLineStyle::Thick => "thick",
318        BorderLineStyle::Dashed => "dashed",
319        BorderLineStyle::Dotted => "dotted",
320        BorderLineStyle::Double => "double",
321        BorderLineStyle::Hair => "hair",
322        BorderLineStyle::MediumDashed => "mediumDashed",
323        BorderLineStyle::DashDot => "dashDot",
324        BorderLineStyle::MediumDashDot => "mediumDashDot",
325        BorderLineStyle::DashDotDot => "dashDotDot",
326        BorderLineStyle::MediumDashDotDot => "mediumDashDotDot",
327        BorderLineStyle::SlantDashDot => "slantDashDot",
328    }
329}
330
331/// Convert an XML `Dxf` back to a `ConditionalStyle`.
332fn dxf_to_conditional_style(dxf: &Dxf) -> ConditionalStyle {
333    let font = dxf.font.as_ref().map(|f| FontStyle {
334        name: f.name.as_ref().map(|n| n.val.clone()),
335        size: f.sz.as_ref().map(|s| s.val),
336        bold: f.b.is_some(),
337        italic: f.i.is_some(),
338        underline: f.u.is_some(),
339        strikethrough: f.strike.is_some(),
340        color: f.color.as_ref().and_then(xml_color_to_style_color),
341    });
342
343    let fill = dxf.fill.as_ref().and_then(|f| {
344        let pf = f.pattern_fill.as_ref()?;
345        Some(FillStyle {
346            pattern: pf
347                .pattern_type
348                .as_ref()
349                .map(|s| parse_pattern_type(s))
350                .unwrap_or(PatternType::None),
351            fg_color: pf.fg_color.as_ref().and_then(xml_color_to_style_color),
352            bg_color: pf.bg_color.as_ref().and_then(xml_color_to_style_color),
353            gradient: None,
354        })
355    });
356
357    let border = dxf.border.as_ref().map(|b| {
358        let side = |s: &sheetkit_xml::styles::BorderSide| -> Option<BorderSideStyle> {
359            let style_str = s.style.as_ref()?;
360            let line_style = parse_border_line_style(style_str)?;
361            Some(BorderSideStyle {
362                style: line_style,
363                color: s.color.as_ref().and_then(xml_color_to_style_color),
364            })
365        };
366        BorderStyle {
367            left: b.left.as_ref().and_then(side),
368            right: b.right.as_ref().and_then(side),
369            top: b.top.as_ref().and_then(side),
370            bottom: b.bottom.as_ref().and_then(side),
371            diagonal: b.diagonal.as_ref().and_then(side),
372        }
373    });
374
375    let num_fmt = dxf.num_fmt.as_ref().map(|nf| {
376        if nf.format_code.is_empty() {
377            NumFmtStyle::Builtin(nf.num_fmt_id)
378        } else {
379            NumFmtStyle::Custom(nf.format_code.clone())
380        }
381    });
382
383    ConditionalStyle {
384        font,
385        fill,
386        border,
387        num_fmt,
388    }
389}
390
391/// Convert an XML Color to a StyleColor.
392fn xml_color_to_style_color(color: &sheetkit_xml::styles::Color) -> Option<StyleColor> {
393    if let Some(ref rgb) = color.rgb {
394        Some(StyleColor::Rgb(rgb.clone()))
395    } else if let Some(theme) = color.theme {
396        Some(StyleColor::Theme(theme))
397    } else {
398        color.indexed.map(StyleColor::Indexed)
399    }
400}
401
402/// Parse a pattern type string.
403fn parse_pattern_type(s: &str) -> PatternType {
404    match s {
405        "none" => PatternType::None,
406        "solid" => PatternType::Solid,
407        "gray125" => PatternType::Gray125,
408        "darkGray" => PatternType::DarkGray,
409        "mediumGray" => PatternType::MediumGray,
410        "lightGray" => PatternType::LightGray,
411        _ => PatternType::None,
412    }
413}
414
415/// Parse a border line style string.
416fn parse_border_line_style(s: &str) -> Option<BorderLineStyle> {
417    match s {
418        "thin" => Some(BorderLineStyle::Thin),
419        "medium" => Some(BorderLineStyle::Medium),
420        "thick" => Some(BorderLineStyle::Thick),
421        "dashed" => Some(BorderLineStyle::Dashed),
422        "dotted" => Some(BorderLineStyle::Dotted),
423        "double" => Some(BorderLineStyle::Double),
424        "hair" => Some(BorderLineStyle::Hair),
425        "mediumDashed" => Some(BorderLineStyle::MediumDashed),
426        "dashDot" => Some(BorderLineStyle::DashDot),
427        "mediumDashDot" => Some(BorderLineStyle::MediumDashDot),
428        "dashDotDot" => Some(BorderLineStyle::DashDotDot),
429        "mediumDashDotDot" => Some(BorderLineStyle::MediumDashDotDot),
430        "slantDashDot" => Some(BorderLineStyle::SlantDashDot),
431        _ => None,
432    }
433}
434
435/// Compute the next priority across all existing conditional formatting rules.
436fn next_priority(ws: &WorksheetXml) -> u32 {
437    let max = ws
438        .conditional_formatting
439        .iter()
440        .flat_map(|cf| cf.cf_rules.iter())
441        .map(|r| r.priority)
442        .max()
443        .unwrap_or(0);
444    max + 1
445}
446
447/// Convert a `ConditionalFormatRule` to an XML `CfRule`, adding a DXF to the
448/// stylesheet if needed. Returns the CfRule ready for insertion.
449fn rule_to_xml(rule: &ConditionalFormatRule, stylesheet: &mut StyleSheet, priority: u32) -> CfRule {
450    let dxf_id = rule.format.as_ref().map(|style| add_dxf(stylesheet, style));
451
452    let stop_if_true = if rule.stop_if_true { Some(true) } else { None };
453
454    match &rule.rule_type {
455        ConditionalFormatType::CellIs {
456            operator,
457            formula,
458            formula2,
459        } => {
460            let mut formulas = vec![formula.clone()];
461            if let Some(f2) = formula2 {
462                formulas.push(f2.clone());
463            }
464            CfRule {
465                rule_type: "cellIs".to_string(),
466                dxf_id,
467                priority,
468                operator: Some(operator.as_str().to_string()),
469                text: None,
470                stop_if_true,
471                above_average: None,
472                equal_average: None,
473                percent: None,
474                rank: None,
475                bottom: None,
476                formulas,
477                color_scale: None,
478                data_bar: None,
479                icon_set: None,
480            }
481        }
482        ConditionalFormatType::Expression { formula } => CfRule {
483            rule_type: "expression".to_string(),
484            dxf_id,
485            priority,
486            operator: None,
487            text: None,
488            stop_if_true,
489            above_average: None,
490            equal_average: None,
491            percent: None,
492            rank: None,
493            bottom: None,
494            formulas: vec![formula.clone()],
495            color_scale: None,
496            data_bar: None,
497            icon_set: None,
498        },
499        ConditionalFormatType::ColorScale {
500            min_type,
501            min_value,
502            min_color,
503            mid_type,
504            mid_value,
505            mid_color,
506            max_type,
507            max_value,
508            max_color,
509        } => {
510            let mut cfvos = vec![CfVo {
511                value_type: min_type.as_str().to_string(),
512                val: min_value.clone(),
513            }];
514            let mut colors = vec![CfColor {
515                rgb: Some(min_color.clone()),
516                theme: None,
517                tint: None,
518            }];
519
520            if let Some(mt) = mid_type {
521                cfvos.push(CfVo {
522                    value_type: mt.as_str().to_string(),
523                    val: mid_value.clone(),
524                });
525                colors.push(CfColor {
526                    rgb: mid_color.clone(),
527                    theme: None,
528                    tint: None,
529                });
530            }
531
532            cfvos.push(CfVo {
533                value_type: max_type.as_str().to_string(),
534                val: max_value.clone(),
535            });
536            colors.push(CfColor {
537                rgb: Some(max_color.clone()),
538                theme: None,
539                tint: None,
540            });
541
542            CfRule {
543                rule_type: "colorScale".to_string(),
544                dxf_id: None, // color scales do not use DXF
545                priority,
546                operator: None,
547                text: None,
548                stop_if_true,
549                above_average: None,
550                equal_average: None,
551                percent: None,
552                rank: None,
553                bottom: None,
554                formulas: vec![],
555                color_scale: Some(CfColorScale { cfvos, colors }),
556                data_bar: None,
557                icon_set: None,
558            }
559        }
560        ConditionalFormatType::DataBar {
561            min_type,
562            min_value,
563            max_type,
564            max_value,
565            color,
566            show_value,
567        } => {
568            let cfvos = vec![
569                CfVo {
570                    value_type: min_type.as_str().to_string(),
571                    val: min_value.clone(),
572                },
573                CfVo {
574                    value_type: max_type.as_str().to_string(),
575                    val: max_value.clone(),
576                },
577            ];
578            CfRule {
579                rule_type: "dataBar".to_string(),
580                dxf_id: None, // data bars do not use DXF
581                priority,
582                operator: None,
583                text: None,
584                stop_if_true,
585                above_average: None,
586                equal_average: None,
587                percent: None,
588                rank: None,
589                bottom: None,
590                formulas: vec![],
591                color_scale: None,
592                data_bar: Some(CfDataBar {
593                    show_value: if *show_value { None } else { Some(false) },
594                    cfvos,
595                    color: Some(CfColor {
596                        rgb: Some(color.clone()),
597                        theme: None,
598                        tint: None,
599                    }),
600                }),
601                icon_set: None,
602            }
603        }
604        ConditionalFormatType::DuplicateValues => CfRule {
605            rule_type: "duplicateValues".to_string(),
606            dxf_id,
607            priority,
608            operator: None,
609            text: None,
610            stop_if_true,
611            above_average: None,
612            equal_average: None,
613            percent: None,
614            rank: None,
615            bottom: None,
616            formulas: vec![],
617            color_scale: None,
618            data_bar: None,
619            icon_set: None,
620        },
621        ConditionalFormatType::UniqueValues => CfRule {
622            rule_type: "uniqueValues".to_string(),
623            dxf_id,
624            priority,
625            operator: None,
626            text: None,
627            stop_if_true,
628            above_average: None,
629            equal_average: None,
630            percent: None,
631            rank: None,
632            bottom: None,
633            formulas: vec![],
634            color_scale: None,
635            data_bar: None,
636            icon_set: None,
637        },
638        ConditionalFormatType::Top10 { rank, percent } => CfRule {
639            rule_type: "top10".to_string(),
640            dxf_id,
641            priority,
642            operator: None,
643            text: None,
644            stop_if_true,
645            above_average: None,
646            equal_average: None,
647            percent: if *percent { Some(true) } else { None },
648            rank: Some(*rank),
649            bottom: None,
650            formulas: vec![],
651            color_scale: None,
652            data_bar: None,
653            icon_set: None,
654        },
655        ConditionalFormatType::Bottom10 { rank, percent } => CfRule {
656            rule_type: "top10".to_string(),
657            dxf_id,
658            priority,
659            operator: None,
660            text: None,
661            stop_if_true,
662            above_average: None,
663            equal_average: None,
664            percent: if *percent { Some(true) } else { None },
665            rank: Some(*rank),
666            bottom: Some(true),
667            formulas: vec![],
668            color_scale: None,
669            data_bar: None,
670            icon_set: None,
671        },
672        ConditionalFormatType::AboveAverage {
673            above,
674            equal_average,
675        } => CfRule {
676            rule_type: "aboveAverage".to_string(),
677            dxf_id,
678            priority,
679            operator: None,
680            text: None,
681            stop_if_true,
682            above_average: if *above { None } else { Some(false) },
683            equal_average: if *equal_average { Some(true) } else { None },
684            percent: None,
685            rank: None,
686            bottom: None,
687            formulas: vec![],
688            color_scale: None,
689            data_bar: None,
690            icon_set: None,
691        },
692        ConditionalFormatType::ContainsBlanks => CfRule {
693            rule_type: "containsBlanks".to_string(),
694            dxf_id,
695            priority,
696            operator: None,
697            text: None,
698            stop_if_true,
699            above_average: None,
700            equal_average: None,
701            percent: None,
702            rank: None,
703            bottom: None,
704            formulas: vec![],
705            color_scale: None,
706            data_bar: None,
707            icon_set: None,
708        },
709        ConditionalFormatType::NotContainsBlanks => CfRule {
710            rule_type: "notContainsBlanks".to_string(),
711            dxf_id,
712            priority,
713            operator: None,
714            text: None,
715            stop_if_true,
716            above_average: None,
717            equal_average: None,
718            percent: None,
719            rank: None,
720            bottom: None,
721            formulas: vec![],
722            color_scale: None,
723            data_bar: None,
724            icon_set: None,
725        },
726        ConditionalFormatType::ContainsErrors => CfRule {
727            rule_type: "containsErrors".to_string(),
728            dxf_id,
729            priority,
730            operator: None,
731            text: None,
732            stop_if_true,
733            above_average: None,
734            equal_average: None,
735            percent: None,
736            rank: None,
737            bottom: None,
738            formulas: vec![],
739            color_scale: None,
740            data_bar: None,
741            icon_set: None,
742        },
743        ConditionalFormatType::NotContainsErrors => CfRule {
744            rule_type: "notContainsErrors".to_string(),
745            dxf_id,
746            priority,
747            operator: None,
748            text: None,
749            stop_if_true,
750            above_average: None,
751            equal_average: None,
752            percent: None,
753            rank: None,
754            bottom: None,
755            formulas: vec![],
756            color_scale: None,
757            data_bar: None,
758            icon_set: None,
759        },
760        ConditionalFormatType::ContainsText { text } => CfRule {
761            rule_type: "containsText".to_string(),
762            dxf_id,
763            priority,
764            operator: Some("containsText".to_string()),
765            text: Some(text.clone()),
766            stop_if_true,
767            above_average: None,
768            equal_average: None,
769            percent: None,
770            rank: None,
771            bottom: None,
772            formulas: vec![],
773            color_scale: None,
774            data_bar: None,
775            icon_set: None,
776        },
777        ConditionalFormatType::NotContainsText { text } => CfRule {
778            rule_type: "notContainsText".to_string(),
779            dxf_id,
780            priority,
781            operator: Some("notContains".to_string()),
782            text: Some(text.clone()),
783            stop_if_true,
784            above_average: None,
785            equal_average: None,
786            percent: None,
787            rank: None,
788            bottom: None,
789            formulas: vec![],
790            color_scale: None,
791            data_bar: None,
792            icon_set: None,
793        },
794        ConditionalFormatType::BeginsWith { text } => CfRule {
795            rule_type: "beginsWith".to_string(),
796            dxf_id,
797            priority,
798            operator: Some("beginsWith".to_string()),
799            text: Some(text.clone()),
800            stop_if_true,
801            above_average: None,
802            equal_average: None,
803            percent: None,
804            rank: None,
805            bottom: None,
806            formulas: vec![],
807            color_scale: None,
808            data_bar: None,
809            icon_set: None,
810        },
811        ConditionalFormatType::EndsWith { text } => CfRule {
812            rule_type: "endsWith".to_string(),
813            dxf_id,
814            priority,
815            operator: Some("endsWith".to_string()),
816            text: Some(text.clone()),
817            stop_if_true,
818            above_average: None,
819            equal_average: None,
820            percent: None,
821            rank: None,
822            bottom: None,
823            formulas: vec![],
824            color_scale: None,
825            data_bar: None,
826            icon_set: None,
827        },
828    }
829}
830
831/// Convert an XML `CfRule` to a `ConditionalFormatRule`, looking up the DXF
832/// style from the stylesheet.
833fn xml_to_rule(cf_rule: &CfRule, stylesheet: &StyleSheet) -> ConditionalFormatRule {
834    let format = cf_rule
835        .dxf_id
836        .and_then(|id| {
837            stylesheet
838                .dxfs
839                .as_ref()
840                .and_then(|dxfs| dxfs.dxfs.get(id as usize))
841        })
842        .map(dxf_to_conditional_style);
843
844    let rule_type = match cf_rule.rule_type.as_str() {
845        "cellIs" => {
846            let operator = cf_rule
847                .operator
848                .as_deref()
849                .and_then(CfOperator::parse)
850                .unwrap_or(CfOperator::Equal);
851            let formula = cf_rule.formulas.first().cloned().unwrap_or_default();
852            let formula2 = cf_rule.formulas.get(1).cloned();
853            ConditionalFormatType::CellIs {
854                operator,
855                formula,
856                formula2,
857            }
858        }
859        "expression" => {
860            let formula = cf_rule.formulas.first().cloned().unwrap_or_default();
861            ConditionalFormatType::Expression { formula }
862        }
863        "colorScale" => {
864            if let Some(cs) = &cf_rule.color_scale {
865                let get_cfvo = |idx: usize| -> (CfValueType, Option<String>) {
866                    cs.cfvos
867                        .get(idx)
868                        .map(|v| {
869                            (
870                                CfValueType::parse(&v.value_type).unwrap_or(CfValueType::Min),
871                                v.val.clone(),
872                            )
873                        })
874                        .unwrap_or((CfValueType::Min, None))
875                };
876                let get_color = |idx: usize| -> Option<String> {
877                    cs.colors.get(idx).and_then(|c| c.rgb.clone())
878                };
879
880                let (min_type, min_value) = get_cfvo(0);
881                let min_color = get_color(0).unwrap_or_default();
882
883                let is_three_color = cs.cfvos.len() >= 3;
884                let (mid_type, mid_value, mid_color) = if is_three_color {
885                    let (mt, mv) = get_cfvo(1);
886                    (Some(mt), mv, get_color(1))
887                } else {
888                    (None, None, None)
889                };
890
891                let max_idx = if is_three_color { 2 } else { 1 };
892                let (max_type, max_value) = get_cfvo(max_idx);
893                let max_color = get_color(max_idx).unwrap_or_default();
894
895                ConditionalFormatType::ColorScale {
896                    min_type,
897                    min_value,
898                    min_color,
899                    mid_type,
900                    mid_value,
901                    mid_color,
902                    max_type,
903                    max_value,
904                    max_color,
905                }
906            } else {
907                // Fallback for malformed data
908                ConditionalFormatType::ColorScale {
909                    min_type: CfValueType::Min,
910                    min_value: None,
911                    min_color: String::new(),
912                    mid_type: None,
913                    mid_value: None,
914                    mid_color: None,
915                    max_type: CfValueType::Max,
916                    max_value: None,
917                    max_color: String::new(),
918                }
919            }
920        }
921        "dataBar" => {
922            if let Some(db) = &cf_rule.data_bar {
923                let get_cfvo = |idx: usize| -> (CfValueType, Option<String>) {
924                    db.cfvos
925                        .get(idx)
926                        .map(|v| {
927                            (
928                                CfValueType::parse(&v.value_type).unwrap_or(CfValueType::Min),
929                                v.val.clone(),
930                            )
931                        })
932                        .unwrap_or((CfValueType::Min, None))
933                };
934                let (min_type, min_value) = get_cfvo(0);
935                let (max_type, max_value) = get_cfvo(1);
936                let color = db
937                    .color
938                    .as_ref()
939                    .and_then(|c| c.rgb.clone())
940                    .unwrap_or_default();
941                let show_value = db.show_value.unwrap_or(true);
942
943                ConditionalFormatType::DataBar {
944                    min_type,
945                    min_value,
946                    max_type,
947                    max_value,
948                    color,
949                    show_value,
950                }
951            } else {
952                ConditionalFormatType::DataBar {
953                    min_type: CfValueType::Min,
954                    min_value: None,
955                    max_type: CfValueType::Max,
956                    max_value: None,
957                    color: String::new(),
958                    show_value: true,
959                }
960            }
961        }
962        "duplicateValues" => ConditionalFormatType::DuplicateValues,
963        "uniqueValues" => ConditionalFormatType::UniqueValues,
964        "top10" => {
965            let rank = cf_rule.rank.unwrap_or(10);
966            let percent = cf_rule.percent.unwrap_or(false);
967            if cf_rule.bottom == Some(true) {
968                ConditionalFormatType::Bottom10 { rank, percent }
969            } else {
970                ConditionalFormatType::Top10 { rank, percent }
971            }
972        }
973        "aboveAverage" => {
974            let above = cf_rule.above_average.unwrap_or(true);
975            let equal_average = cf_rule.equal_average.unwrap_or(false);
976            ConditionalFormatType::AboveAverage {
977                above,
978                equal_average,
979            }
980        }
981        "containsBlanks" => ConditionalFormatType::ContainsBlanks,
982        "notContainsBlanks" => ConditionalFormatType::NotContainsBlanks,
983        "containsErrors" => ConditionalFormatType::ContainsErrors,
984        "notContainsErrors" => ConditionalFormatType::NotContainsErrors,
985        "containsText" => ConditionalFormatType::ContainsText {
986            text: cf_rule.text.clone().unwrap_or_default(),
987        },
988        "notContainsText" => ConditionalFormatType::NotContainsText {
989            text: cf_rule.text.clone().unwrap_or_default(),
990        },
991        "beginsWith" => ConditionalFormatType::BeginsWith {
992            text: cf_rule.text.clone().unwrap_or_default(),
993        },
994        "endsWith" => ConditionalFormatType::EndsWith {
995            text: cf_rule.text.clone().unwrap_or_default(),
996        },
997        // Unknown rule types default to expression
998        _ => ConditionalFormatType::Expression {
999            formula: cf_rule.formulas.first().cloned().unwrap_or_default(),
1000        },
1001    };
1002
1003    ConditionalFormatRule {
1004        rule_type,
1005        format,
1006        priority: Some(cf_rule.priority),
1007        stop_if_true: cf_rule.stop_if_true.unwrap_or(false),
1008    }
1009}
1010
1011/// Set conditional formatting rules on a cell range. Each call adds a new
1012/// `conditionalFormatting` element with one or more `cfRule` children.
1013pub fn set_conditional_format(
1014    ws: &mut WorksheetXml,
1015    stylesheet: &mut StyleSheet,
1016    sqref: &str,
1017    rules: &[ConditionalFormatRule],
1018) -> Result<()> {
1019    let mut xml_rules = Vec::with_capacity(rules.len());
1020    for rule in rules {
1021        let priority = rule.priority.unwrap_or_else(|| next_priority(ws));
1022        let cf_rule = rule_to_xml(rule, stylesheet, priority);
1023        xml_rules.push(cf_rule);
1024    }
1025
1026    ws.conditional_formatting.push(ConditionalFormatting {
1027        sqref: sqref.to_string(),
1028        cf_rules: xml_rules,
1029    });
1030
1031    Ok(())
1032}
1033
1034/// Get all conditional formatting rules from a worksheet.
1035///
1036/// Returns a list of `(sqref, rules)` pairs.
1037pub fn get_conditional_formats(
1038    ws: &WorksheetXml,
1039    stylesheet: &StyleSheet,
1040) -> Vec<(String, Vec<ConditionalFormatRule>)> {
1041    ws.conditional_formatting
1042        .iter()
1043        .map(|cf| {
1044            let rules = cf
1045                .cf_rules
1046                .iter()
1047                .map(|r| xml_to_rule(r, stylesheet))
1048                .collect();
1049            (cf.sqref.clone(), rules)
1050        })
1051        .collect()
1052}
1053
1054/// Delete all conditional formatting rules for a specific cell range.
1055pub fn delete_conditional_format(ws: &mut WorksheetXml, sqref: &str) -> Result<()> {
1056    ws.conditional_formatting.retain(|cf| cf.sqref != sqref);
1057    Ok(())
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063
1064    fn default_stylesheet() -> StyleSheet {
1065        StyleSheet::default()
1066    }
1067
1068    // CellIs tests
1069
1070    #[test]
1071    fn test_cell_is_greater_than() {
1072        let mut ws = WorksheetXml::default();
1073        let mut ss = default_stylesheet();
1074        let rules = vec![ConditionalFormatRule {
1075            rule_type: ConditionalFormatType::CellIs {
1076                operator: CfOperator::GreaterThan,
1077                formula: "100".to_string(),
1078                formula2: None,
1079            },
1080            format: Some(ConditionalStyle {
1081                font: Some(FontStyle {
1082                    bold: true,
1083                    color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1084                    ..FontStyle::default()
1085                }),
1086                ..ConditionalStyle::default()
1087            }),
1088            priority: None,
1089            stop_if_true: false,
1090        }];
1091
1092        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1093
1094        assert_eq!(ws.conditional_formatting.len(), 1);
1095        assert_eq!(ws.conditional_formatting[0].sqref, "A1:A100");
1096        assert_eq!(ws.conditional_formatting[0].cf_rules.len(), 1);
1097
1098        let rule = &ws.conditional_formatting[0].cf_rules[0];
1099        assert_eq!(rule.rule_type, "cellIs");
1100        assert_eq!(rule.operator, Some("greaterThan".to_string()));
1101        assert_eq!(rule.formulas, vec!["100".to_string()]);
1102        assert!(rule.dxf_id.is_some());
1103        assert_eq!(rule.priority, 1);
1104
1105        // Verify DXF was added
1106        let dxfs = ss.dxfs.as_ref().unwrap();
1107        assert_eq!(dxfs.dxfs.len(), 1);
1108        assert!(dxfs.dxfs[0].font.is_some());
1109    }
1110
1111    #[test]
1112    fn test_cell_is_between() {
1113        let mut ws = WorksheetXml::default();
1114        let mut ss = default_stylesheet();
1115        let rules = vec![ConditionalFormatRule {
1116            rule_type: ConditionalFormatType::CellIs {
1117                operator: CfOperator::Between,
1118                formula: "10".to_string(),
1119                formula2: Some("20".to_string()),
1120            },
1121            format: None,
1122            priority: None,
1123            stop_if_true: false,
1124        }];
1125
1126        set_conditional_format(&mut ws, &mut ss, "B1:B50", &rules).unwrap();
1127
1128        let rule = &ws.conditional_formatting[0].cf_rules[0];
1129        assert_eq!(rule.operator, Some("between".to_string()));
1130        assert_eq!(rule.formulas, vec!["10".to_string(), "20".to_string()]);
1131        assert!(rule.dxf_id.is_none());
1132    }
1133
1134    #[test]
1135    fn test_cell_is_equal() {
1136        let mut ws = WorksheetXml::default();
1137        let mut ss = default_stylesheet();
1138        let rules = vec![ConditionalFormatRule {
1139            rule_type: ConditionalFormatType::CellIs {
1140                operator: CfOperator::Equal,
1141                formula: "\"Yes\"".to_string(),
1142                formula2: None,
1143            },
1144            format: Some(ConditionalStyle {
1145                fill: Some(FillStyle {
1146                    pattern: PatternType::Solid,
1147                    fg_color: Some(StyleColor::Rgb("FF00FF00".to_string())),
1148                    bg_color: None,
1149                    gradient: None,
1150                }),
1151                ..ConditionalStyle::default()
1152            }),
1153            priority: None,
1154            stop_if_true: false,
1155        }];
1156
1157        set_conditional_format(&mut ws, &mut ss, "C1:C10", &rules).unwrap();
1158
1159        let rule = &ws.conditional_formatting[0].cf_rules[0];
1160        assert_eq!(rule.operator, Some("equal".to_string()));
1161        assert_eq!(rule.formulas, vec!["\"Yes\"".to_string()]);
1162    }
1163
1164    #[test]
1165    fn test_cell_is_less_than() {
1166        let mut ws = WorksheetXml::default();
1167        let mut ss = default_stylesheet();
1168        let rules = vec![ConditionalFormatRule {
1169            rule_type: ConditionalFormatType::CellIs {
1170                operator: CfOperator::LessThan,
1171                formula: "0".to_string(),
1172                formula2: None,
1173            },
1174            format: None,
1175            priority: None,
1176            stop_if_true: false,
1177        }];
1178
1179        set_conditional_format(&mut ws, &mut ss, "D1:D10", &rules).unwrap();
1180        let rule = &ws.conditional_formatting[0].cf_rules[0];
1181        assert_eq!(rule.operator, Some("lessThan".to_string()));
1182    }
1183
1184    #[test]
1185    fn test_cell_is_not_between() {
1186        let mut ws = WorksheetXml::default();
1187        let mut ss = default_stylesheet();
1188        let rules = vec![ConditionalFormatRule {
1189            rule_type: ConditionalFormatType::CellIs {
1190                operator: CfOperator::NotBetween,
1191                formula: "1".to_string(),
1192                formula2: Some("10".to_string()),
1193            },
1194            format: None,
1195            priority: None,
1196            stop_if_true: false,
1197        }];
1198
1199        set_conditional_format(&mut ws, &mut ss, "E1:E10", &rules).unwrap();
1200        let rule = &ws.conditional_formatting[0].cf_rules[0];
1201        assert_eq!(rule.operator, Some("notBetween".to_string()));
1202        assert_eq!(rule.formulas.len(), 2);
1203    }
1204
1205    // Expression tests
1206
1207    #[test]
1208    fn test_expression_rule() {
1209        let mut ws = WorksheetXml::default();
1210        let mut ss = default_stylesheet();
1211        let rules = vec![ConditionalFormatRule {
1212            rule_type: ConditionalFormatType::Expression {
1213                formula: "MOD(ROW(),2)=0".to_string(),
1214            },
1215            format: Some(ConditionalStyle {
1216                fill: Some(FillStyle {
1217                    pattern: PatternType::Solid,
1218                    fg_color: Some(StyleColor::Rgb("FFEEEEEE".to_string())),
1219                    bg_color: None,
1220                    gradient: None,
1221                }),
1222                ..ConditionalStyle::default()
1223            }),
1224            priority: None,
1225            stop_if_true: false,
1226        }];
1227
1228        set_conditional_format(&mut ws, &mut ss, "A1:Z100", &rules).unwrap();
1229
1230        let rule = &ws.conditional_formatting[0].cf_rules[0];
1231        assert_eq!(rule.rule_type, "expression");
1232        assert_eq!(rule.formulas, vec!["MOD(ROW(),2)=0".to_string()]);
1233        assert!(rule.dxf_id.is_some());
1234    }
1235
1236    // ColorScale tests
1237
1238    #[test]
1239    fn test_two_color_scale() {
1240        let mut ws = WorksheetXml::default();
1241        let mut ss = default_stylesheet();
1242        let rules = vec![ConditionalFormatRule {
1243            rule_type: ConditionalFormatType::ColorScale {
1244                min_type: CfValueType::Min,
1245                min_value: None,
1246                min_color: "FFF8696B".to_string(),
1247                mid_type: None,
1248                mid_value: None,
1249                mid_color: None,
1250                max_type: CfValueType::Max,
1251                max_value: None,
1252                max_color: "FF63BE7B".to_string(),
1253            },
1254            format: None,
1255            priority: None,
1256            stop_if_true: false,
1257        }];
1258
1259        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1260
1261        let rule = &ws.conditional_formatting[0].cf_rules[0];
1262        assert_eq!(rule.rule_type, "colorScale");
1263        assert!(rule.dxf_id.is_none());
1264        let cs = rule.color_scale.as_ref().unwrap();
1265        assert_eq!(cs.cfvos.len(), 2);
1266        assert_eq!(cs.colors.len(), 2);
1267        assert_eq!(cs.cfvos[0].value_type, "min");
1268        assert_eq!(cs.cfvos[1].value_type, "max");
1269        assert_eq!(cs.colors[0].rgb, Some("FFF8696B".to_string()));
1270        assert_eq!(cs.colors[1].rgb, Some("FF63BE7B".to_string()));
1271    }
1272
1273    #[test]
1274    fn test_three_color_scale() {
1275        let mut ws = WorksheetXml::default();
1276        let mut ss = default_stylesheet();
1277        let rules = vec![ConditionalFormatRule {
1278            rule_type: ConditionalFormatType::ColorScale {
1279                min_type: CfValueType::Min,
1280                min_value: None,
1281                min_color: "FFF8696B".to_string(),
1282                mid_type: Some(CfValueType::Percentile),
1283                mid_value: Some("50".to_string()),
1284                mid_color: Some("FFFFEB84".to_string()),
1285                max_type: CfValueType::Max,
1286                max_value: None,
1287                max_color: "FF63BE7B".to_string(),
1288            },
1289            format: None,
1290            priority: None,
1291            stop_if_true: false,
1292        }];
1293
1294        set_conditional_format(&mut ws, &mut ss, "B1:B100", &rules).unwrap();
1295
1296        let rule = &ws.conditional_formatting[0].cf_rules[0];
1297        let cs = rule.color_scale.as_ref().unwrap();
1298        assert_eq!(cs.cfvos.len(), 3);
1299        assert_eq!(cs.colors.len(), 3);
1300        assert_eq!(cs.cfvos[1].value_type, "percentile");
1301        assert_eq!(cs.cfvos[1].val, Some("50".to_string()));
1302        assert_eq!(cs.colors[1].rgb, Some("FFFFEB84".to_string()));
1303    }
1304
1305    // DataBar tests
1306
1307    #[test]
1308    fn test_data_bar() {
1309        let mut ws = WorksheetXml::default();
1310        let mut ss = default_stylesheet();
1311        let rules = vec![ConditionalFormatRule {
1312            rule_type: ConditionalFormatType::DataBar {
1313                min_type: CfValueType::Min,
1314                min_value: None,
1315                max_type: CfValueType::Max,
1316                max_value: None,
1317                color: "FF638EC6".to_string(),
1318                show_value: true,
1319            },
1320            format: None,
1321            priority: None,
1322            stop_if_true: false,
1323        }];
1324
1325        set_conditional_format(&mut ws, &mut ss, "C1:C50", &rules).unwrap();
1326
1327        let rule = &ws.conditional_formatting[0].cf_rules[0];
1328        assert_eq!(rule.rule_type, "dataBar");
1329        let db = rule.data_bar.as_ref().unwrap();
1330        assert!(db.show_value.is_none()); // true is default, not serialized
1331        assert_eq!(db.cfvos.len(), 2);
1332        assert_eq!(db.color.as_ref().unwrap().rgb, Some("FF638EC6".to_string()));
1333    }
1334
1335    #[test]
1336    fn test_data_bar_hidden_value() {
1337        let mut ws = WorksheetXml::default();
1338        let mut ss = default_stylesheet();
1339        let rules = vec![ConditionalFormatRule {
1340            rule_type: ConditionalFormatType::DataBar {
1341                min_type: CfValueType::Num,
1342                min_value: Some("0".to_string()),
1343                max_type: CfValueType::Num,
1344                max_value: Some("100".to_string()),
1345                color: "FF638EC6".to_string(),
1346                show_value: false,
1347            },
1348            format: None,
1349            priority: None,
1350            stop_if_true: false,
1351        }];
1352
1353        set_conditional_format(&mut ws, &mut ss, "D1:D50", &rules).unwrap();
1354
1355        let db = ws.conditional_formatting[0].cf_rules[0]
1356            .data_bar
1357            .as_ref()
1358            .unwrap();
1359        assert_eq!(db.show_value, Some(false));
1360        assert_eq!(db.cfvos[0].value_type, "num");
1361        assert_eq!(db.cfvos[0].val, Some("0".to_string()));
1362    }
1363
1364    // DuplicateValues / UniqueValues tests
1365
1366    #[test]
1367    fn test_duplicate_values() {
1368        let mut ws = WorksheetXml::default();
1369        let mut ss = default_stylesheet();
1370        let rules = vec![ConditionalFormatRule {
1371            rule_type: ConditionalFormatType::DuplicateValues,
1372            format: Some(ConditionalStyle {
1373                fill: Some(FillStyle {
1374                    pattern: PatternType::Solid,
1375                    fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1376                    bg_color: None,
1377                    gradient: None,
1378                }),
1379                ..ConditionalStyle::default()
1380            }),
1381            priority: None,
1382            stop_if_true: false,
1383        }];
1384
1385        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1386
1387        let rule = &ws.conditional_formatting[0].cf_rules[0];
1388        assert_eq!(rule.rule_type, "duplicateValues");
1389        assert!(rule.dxf_id.is_some());
1390    }
1391
1392    #[test]
1393    fn test_unique_values() {
1394        let mut ws = WorksheetXml::default();
1395        let mut ss = default_stylesheet();
1396        let rules = vec![ConditionalFormatRule {
1397            rule_type: ConditionalFormatType::UniqueValues,
1398            format: None,
1399            priority: None,
1400            stop_if_true: false,
1401        }];
1402
1403        set_conditional_format(&mut ws, &mut ss, "B1:B50", &rules).unwrap();
1404
1405        let rule = &ws.conditional_formatting[0].cf_rules[0];
1406        assert_eq!(rule.rule_type, "uniqueValues");
1407    }
1408
1409    // Top10 / Bottom10 tests
1410
1411    #[test]
1412    fn test_top_10() {
1413        let mut ws = WorksheetXml::default();
1414        let mut ss = default_stylesheet();
1415        let rules = vec![ConditionalFormatRule {
1416            rule_type: ConditionalFormatType::Top10 {
1417                rank: 5,
1418                percent: false,
1419            },
1420            format: None,
1421            priority: None,
1422            stop_if_true: false,
1423        }];
1424
1425        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1426
1427        let rule = &ws.conditional_formatting[0].cf_rules[0];
1428        assert_eq!(rule.rule_type, "top10");
1429        assert_eq!(rule.rank, Some(5));
1430        assert!(rule.percent.is_none());
1431        assert!(rule.bottom.is_none());
1432    }
1433
1434    #[test]
1435    fn test_top_10_percent() {
1436        let mut ws = WorksheetXml::default();
1437        let mut ss = default_stylesheet();
1438        let rules = vec![ConditionalFormatRule {
1439            rule_type: ConditionalFormatType::Top10 {
1440                rank: 10,
1441                percent: true,
1442            },
1443            format: None,
1444            priority: None,
1445            stop_if_true: false,
1446        }];
1447
1448        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1449
1450        let rule = &ws.conditional_formatting[0].cf_rules[0];
1451        assert_eq!(rule.percent, Some(true));
1452        assert!(rule.bottom.is_none());
1453    }
1454
1455    #[test]
1456    fn test_bottom_10() {
1457        let mut ws = WorksheetXml::default();
1458        let mut ss = default_stylesheet();
1459        let rules = vec![ConditionalFormatRule {
1460            rule_type: ConditionalFormatType::Bottom10 {
1461                rank: 3,
1462                percent: false,
1463            },
1464            format: None,
1465            priority: None,
1466            stop_if_true: false,
1467        }];
1468
1469        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1470
1471        let rule = &ws.conditional_formatting[0].cf_rules[0];
1472        assert_eq!(rule.rule_type, "top10"); // Bottom uses top10 type with bottom=true
1473        assert_eq!(rule.rank, Some(3));
1474        assert_eq!(rule.bottom, Some(true));
1475    }
1476
1477    // AboveAverage tests
1478
1479    #[test]
1480    fn test_above_average() {
1481        let mut ws = WorksheetXml::default();
1482        let mut ss = default_stylesheet();
1483        let rules = vec![ConditionalFormatRule {
1484            rule_type: ConditionalFormatType::AboveAverage {
1485                above: true,
1486                equal_average: false,
1487            },
1488            format: None,
1489            priority: None,
1490            stop_if_true: false,
1491        }];
1492
1493        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1494
1495        let rule = &ws.conditional_formatting[0].cf_rules[0];
1496        assert_eq!(rule.rule_type, "aboveAverage");
1497        assert!(rule.above_average.is_none()); // true is default
1498        assert!(rule.equal_average.is_none()); // false is default
1499    }
1500
1501    #[test]
1502    fn test_below_average() {
1503        let mut ws = WorksheetXml::default();
1504        let mut ss = default_stylesheet();
1505        let rules = vec![ConditionalFormatRule {
1506            rule_type: ConditionalFormatType::AboveAverage {
1507                above: false,
1508                equal_average: true,
1509            },
1510            format: None,
1511            priority: None,
1512            stop_if_true: false,
1513        }];
1514
1515        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1516
1517        let rule = &ws.conditional_formatting[0].cf_rules[0];
1518        assert_eq!(rule.above_average, Some(false));
1519        assert_eq!(rule.equal_average, Some(true));
1520    }
1521
1522    // Text-based tests
1523
1524    #[test]
1525    fn test_contains_text() {
1526        let mut ws = WorksheetXml::default();
1527        let mut ss = default_stylesheet();
1528        let rules = vec![ConditionalFormatRule {
1529            rule_type: ConditionalFormatType::ContainsText {
1530                text: "error".to_string(),
1531            },
1532            format: None,
1533            priority: None,
1534            stop_if_true: false,
1535        }];
1536
1537        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1538
1539        let rule = &ws.conditional_formatting[0].cf_rules[0];
1540        assert_eq!(rule.rule_type, "containsText");
1541        assert_eq!(rule.text, Some("error".to_string()));
1542        assert_eq!(rule.operator, Some("containsText".to_string()));
1543    }
1544
1545    #[test]
1546    fn test_not_contains_text() {
1547        let mut ws = WorksheetXml::default();
1548        let mut ss = default_stylesheet();
1549        let rules = vec![ConditionalFormatRule {
1550            rule_type: ConditionalFormatType::NotContainsText {
1551                text: "done".to_string(),
1552            },
1553            format: None,
1554            priority: None,
1555            stop_if_true: false,
1556        }];
1557
1558        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1559
1560        let rule = &ws.conditional_formatting[0].cf_rules[0];
1561        assert_eq!(rule.rule_type, "notContainsText");
1562        assert_eq!(rule.operator, Some("notContains".to_string()));
1563    }
1564
1565    #[test]
1566    fn test_begins_with() {
1567        let mut ws = WorksheetXml::default();
1568        let mut ss = default_stylesheet();
1569        let rules = vec![ConditionalFormatRule {
1570            rule_type: ConditionalFormatType::BeginsWith {
1571                text: "Total".to_string(),
1572            },
1573            format: None,
1574            priority: None,
1575            stop_if_true: false,
1576        }];
1577
1578        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1579
1580        let rule = &ws.conditional_formatting[0].cf_rules[0];
1581        assert_eq!(rule.rule_type, "beginsWith");
1582        assert_eq!(rule.text, Some("Total".to_string()));
1583    }
1584
1585    #[test]
1586    fn test_ends_with() {
1587        let mut ws = WorksheetXml::default();
1588        let mut ss = default_stylesheet();
1589        let rules = vec![ConditionalFormatRule {
1590            rule_type: ConditionalFormatType::EndsWith {
1591                text: "Inc.".to_string(),
1592            },
1593            format: None,
1594            priority: None,
1595            stop_if_true: false,
1596        }];
1597
1598        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1599
1600        let rule = &ws.conditional_formatting[0].cf_rules[0];
1601        assert_eq!(rule.rule_type, "endsWith");
1602        assert_eq!(rule.text, Some("Inc.".to_string()));
1603    }
1604
1605    // Blanks / Errors tests
1606
1607    #[test]
1608    fn test_contains_blanks() {
1609        let mut ws = WorksheetXml::default();
1610        let mut ss = default_stylesheet();
1611        let rules = vec![ConditionalFormatRule {
1612            rule_type: ConditionalFormatType::ContainsBlanks,
1613            format: None,
1614            priority: None,
1615            stop_if_true: false,
1616        }];
1617
1618        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1619        assert_eq!(
1620            ws.conditional_formatting[0].cf_rules[0].rule_type,
1621            "containsBlanks"
1622        );
1623    }
1624
1625    #[test]
1626    fn test_not_contains_blanks() {
1627        let mut ws = WorksheetXml::default();
1628        let mut ss = default_stylesheet();
1629        let rules = vec![ConditionalFormatRule {
1630            rule_type: ConditionalFormatType::NotContainsBlanks,
1631            format: None,
1632            priority: None,
1633            stop_if_true: false,
1634        }];
1635
1636        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1637        assert_eq!(
1638            ws.conditional_formatting[0].cf_rules[0].rule_type,
1639            "notContainsBlanks"
1640        );
1641    }
1642
1643    #[test]
1644    fn test_contains_errors() {
1645        let mut ws = WorksheetXml::default();
1646        let mut ss = default_stylesheet();
1647        let rules = vec![ConditionalFormatRule {
1648            rule_type: ConditionalFormatType::ContainsErrors,
1649            format: None,
1650            priority: None,
1651            stop_if_true: false,
1652        }];
1653
1654        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1655        assert_eq!(
1656            ws.conditional_formatting[0].cf_rules[0].rule_type,
1657            "containsErrors"
1658        );
1659    }
1660
1661    #[test]
1662    fn test_not_contains_errors() {
1663        let mut ws = WorksheetXml::default();
1664        let mut ss = default_stylesheet();
1665        let rules = vec![ConditionalFormatRule {
1666            rule_type: ConditionalFormatType::NotContainsErrors,
1667            format: None,
1668            priority: None,
1669            stop_if_true: false,
1670        }];
1671
1672        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1673        assert_eq!(
1674            ws.conditional_formatting[0].cf_rules[0].rule_type,
1675            "notContainsErrors"
1676        );
1677    }
1678
1679    // Delete tests
1680
1681    #[test]
1682    fn test_delete_conditional_format() {
1683        let mut ws = WorksheetXml::default();
1684        let mut ss = default_stylesheet();
1685
1686        let rules1 = vec![ConditionalFormatRule {
1687            rule_type: ConditionalFormatType::DuplicateValues,
1688            format: None,
1689            priority: None,
1690            stop_if_true: false,
1691        }];
1692        let rules2 = vec![ConditionalFormatRule {
1693            rule_type: ConditionalFormatType::UniqueValues,
1694            format: None,
1695            priority: None,
1696            stop_if_true: false,
1697        }];
1698
1699        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules1).unwrap();
1700        set_conditional_format(&mut ws, &mut ss, "B1:B100", &rules2).unwrap();
1701        assert_eq!(ws.conditional_formatting.len(), 2);
1702
1703        delete_conditional_format(&mut ws, "A1:A100").unwrap();
1704        assert_eq!(ws.conditional_formatting.len(), 1);
1705        assert_eq!(ws.conditional_formatting[0].sqref, "B1:B100");
1706    }
1707
1708    #[test]
1709    fn test_delete_nonexistent_conditional_format() {
1710        let mut ws = WorksheetXml::default();
1711        delete_conditional_format(&mut ws, "Z1:Z99").unwrap();
1712        assert!(ws.conditional_formatting.is_empty());
1713    }
1714
1715    // Multiple rules on same range
1716
1717    #[test]
1718    fn test_multiple_rules_same_range() {
1719        let mut ws = WorksheetXml::default();
1720        let mut ss = default_stylesheet();
1721
1722        let rules = vec![
1723            ConditionalFormatRule {
1724                rule_type: ConditionalFormatType::CellIs {
1725                    operator: CfOperator::GreaterThan,
1726                    formula: "90".to_string(),
1727                    formula2: None,
1728                },
1729                format: Some(ConditionalStyle {
1730                    fill: Some(FillStyle {
1731                        pattern: PatternType::Solid,
1732                        fg_color: Some(StyleColor::Rgb("FF00FF00".to_string())),
1733                        bg_color: None,
1734                        gradient: None,
1735                    }),
1736                    ..ConditionalStyle::default()
1737                }),
1738                priority: None,
1739                stop_if_true: false,
1740            },
1741            ConditionalFormatRule {
1742                rule_type: ConditionalFormatType::CellIs {
1743                    operator: CfOperator::LessThan,
1744                    formula: "50".to_string(),
1745                    formula2: None,
1746                },
1747                format: Some(ConditionalStyle {
1748                    fill: Some(FillStyle {
1749                        pattern: PatternType::Solid,
1750                        fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1751                        bg_color: None,
1752                        gradient: None,
1753                    }),
1754                    ..ConditionalStyle::default()
1755                }),
1756                priority: None,
1757                stop_if_true: false,
1758            },
1759        ];
1760
1761        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1762
1763        assert_eq!(ws.conditional_formatting.len(), 1);
1764        assert_eq!(ws.conditional_formatting[0].cf_rules.len(), 2);
1765
1766        let dxfs = ss.dxfs.as_ref().unwrap();
1767        assert_eq!(dxfs.dxfs.len(), 2);
1768    }
1769
1770    // Get (roundtrip) tests
1771
1772    #[test]
1773    fn test_get_conditional_formats_cell_is() {
1774        let mut ws = WorksheetXml::default();
1775        let mut ss = default_stylesheet();
1776
1777        let rules = vec![ConditionalFormatRule {
1778            rule_type: ConditionalFormatType::CellIs {
1779                operator: CfOperator::GreaterThanOrEqual,
1780                formula: "50".to_string(),
1781                formula2: None,
1782            },
1783            format: Some(ConditionalStyle {
1784                font: Some(FontStyle {
1785                    bold: true,
1786                    ..FontStyle::default()
1787                }),
1788                ..ConditionalStyle::default()
1789            }),
1790            priority: None,
1791            stop_if_true: true,
1792        }];
1793
1794        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1795
1796        let formats = get_conditional_formats(&ws, &ss);
1797        assert_eq!(formats.len(), 1);
1798        assert_eq!(formats[0].0, "A1:A100");
1799        assert_eq!(formats[0].1.len(), 1);
1800
1801        let rule = &formats[0].1[0];
1802        match &rule.rule_type {
1803            ConditionalFormatType::CellIs {
1804                operator,
1805                formula,
1806                formula2,
1807            } => {
1808                assert_eq!(*operator, CfOperator::GreaterThanOrEqual);
1809                assert_eq!(formula, "50");
1810                assert!(formula2.is_none());
1811            }
1812            _ => panic!("expected CellIs rule type"),
1813        }
1814        assert!(rule.stop_if_true);
1815        assert!(rule.format.is_some());
1816        assert!(rule.format.as_ref().unwrap().font.as_ref().unwrap().bold);
1817    }
1818
1819    #[test]
1820    fn test_get_conditional_formats_color_scale() {
1821        let mut ws = WorksheetXml::default();
1822        let mut ss = default_stylesheet();
1823
1824        let rules = vec![ConditionalFormatRule {
1825            rule_type: ConditionalFormatType::ColorScale {
1826                min_type: CfValueType::Min,
1827                min_value: None,
1828                min_color: "FFF8696B".to_string(),
1829                mid_type: Some(CfValueType::Percentile),
1830                mid_value: Some("50".to_string()),
1831                mid_color: Some("FFFFEB84".to_string()),
1832                max_type: CfValueType::Max,
1833                max_value: None,
1834                max_color: "FF63BE7B".to_string(),
1835            },
1836            format: None,
1837            priority: None,
1838            stop_if_true: false,
1839        }];
1840
1841        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1842
1843        let formats = get_conditional_formats(&ws, &ss);
1844        let rule = &formats[0].1[0];
1845        match &rule.rule_type {
1846            ConditionalFormatType::ColorScale {
1847                min_type,
1848                min_color,
1849                mid_type,
1850                mid_color,
1851                max_type,
1852                max_color,
1853                ..
1854            } => {
1855                assert_eq!(*min_type, CfValueType::Min);
1856                assert_eq!(min_color, "FFF8696B");
1857                assert_eq!(*mid_type, Some(CfValueType::Percentile));
1858                assert_eq!(*mid_color, Some("FFFFEB84".to_string()));
1859                assert_eq!(*max_type, CfValueType::Max);
1860                assert_eq!(max_color, "FF63BE7B");
1861            }
1862            _ => panic!("expected ColorScale rule type"),
1863        }
1864    }
1865
1866    #[test]
1867    fn test_get_conditional_formats_data_bar() {
1868        let mut ws = WorksheetXml::default();
1869        let mut ss = default_stylesheet();
1870
1871        let rules = vec![ConditionalFormatRule {
1872            rule_type: ConditionalFormatType::DataBar {
1873                min_type: CfValueType::Min,
1874                min_value: None,
1875                max_type: CfValueType::Max,
1876                max_value: None,
1877                color: "FF638EC6".to_string(),
1878                show_value: true,
1879            },
1880            format: None,
1881            priority: None,
1882            stop_if_true: false,
1883        }];
1884
1885        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1886
1887        let formats = get_conditional_formats(&ws, &ss);
1888        let rule = &formats[0].1[0];
1889        match &rule.rule_type {
1890            ConditionalFormatType::DataBar {
1891                min_type,
1892                max_type,
1893                color,
1894                show_value,
1895                ..
1896            } => {
1897                assert_eq!(*min_type, CfValueType::Min);
1898                assert_eq!(*max_type, CfValueType::Max);
1899                assert_eq!(color, "FF638EC6");
1900                assert!(*show_value);
1901            }
1902            _ => panic!("expected DataBar rule type"),
1903        }
1904    }
1905
1906    #[test]
1907    fn test_get_conditional_formats_duplicate_values() {
1908        let mut ws = WorksheetXml::default();
1909        let mut ss = default_stylesheet();
1910
1911        let rules = vec![ConditionalFormatRule {
1912            rule_type: ConditionalFormatType::DuplicateValues,
1913            format: None,
1914            priority: None,
1915            stop_if_true: false,
1916        }];
1917
1918        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1919
1920        let formats = get_conditional_formats(&ws, &ss);
1921        let rule = &formats[0].1[0];
1922        assert_eq!(rule.rule_type, ConditionalFormatType::DuplicateValues);
1923    }
1924
1925    #[test]
1926    fn test_get_conditional_formats_top10() {
1927        let mut ws = WorksheetXml::default();
1928        let mut ss = default_stylesheet();
1929
1930        let rules = vec![ConditionalFormatRule {
1931            rule_type: ConditionalFormatType::Top10 {
1932                rank: 5,
1933                percent: true,
1934            },
1935            format: None,
1936            priority: None,
1937            stop_if_true: false,
1938        }];
1939
1940        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1941
1942        let formats = get_conditional_formats(&ws, &ss);
1943        let rule = &formats[0].1[0];
1944        match &rule.rule_type {
1945            ConditionalFormatType::Top10 { rank, percent } => {
1946                assert_eq!(*rank, 5);
1947                assert!(*percent);
1948            }
1949            _ => panic!("expected Top10 rule type"),
1950        }
1951    }
1952
1953    #[test]
1954    fn test_get_conditional_formats_bottom10() {
1955        let mut ws = WorksheetXml::default();
1956        let mut ss = default_stylesheet();
1957
1958        let rules = vec![ConditionalFormatRule {
1959            rule_type: ConditionalFormatType::Bottom10 {
1960                rank: 3,
1961                percent: false,
1962            },
1963            format: None,
1964            priority: None,
1965            stop_if_true: false,
1966        }];
1967
1968        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1969
1970        let formats = get_conditional_formats(&ws, &ss);
1971        let rule = &formats[0].1[0];
1972        match &rule.rule_type {
1973            ConditionalFormatType::Bottom10 { rank, percent } => {
1974                assert_eq!(*rank, 3);
1975                assert!(!(*percent));
1976            }
1977            _ => panic!("expected Bottom10 rule type"),
1978        }
1979    }
1980
1981    #[test]
1982    fn test_get_conditional_formats_above_average() {
1983        let mut ws = WorksheetXml::default();
1984        let mut ss = default_stylesheet();
1985
1986        let rules = vec![ConditionalFormatRule {
1987            rule_type: ConditionalFormatType::AboveAverage {
1988                above: false,
1989                equal_average: true,
1990            },
1991            format: None,
1992            priority: None,
1993            stop_if_true: false,
1994        }];
1995
1996        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
1997
1998        let formats = get_conditional_formats(&ws, &ss);
1999        let rule = &formats[0].1[0];
2000        match &rule.rule_type {
2001            ConditionalFormatType::AboveAverage {
2002                above,
2003                equal_average,
2004            } => {
2005                assert!(!above);
2006                assert!(*equal_average);
2007            }
2008            _ => panic!("expected AboveAverage rule type"),
2009        }
2010    }
2011
2012    #[test]
2013    fn test_get_conditional_formats_empty() {
2014        let ws = WorksheetXml::default();
2015        let ss = default_stylesheet();
2016        let formats = get_conditional_formats(&ws, &ss);
2017        assert!(formats.is_empty());
2018    }
2019
2020    // Priority auto-increment
2021
2022    #[test]
2023    fn test_priority_auto_increment() {
2024        let mut ws = WorksheetXml::default();
2025        let mut ss = default_stylesheet();
2026
2027        let rules1 = vec![ConditionalFormatRule {
2028            rule_type: ConditionalFormatType::DuplicateValues,
2029            format: None,
2030            priority: None,
2031            stop_if_true: false,
2032        }];
2033        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules1).unwrap();
2034
2035        let rules2 = vec![ConditionalFormatRule {
2036            rule_type: ConditionalFormatType::UniqueValues,
2037            format: None,
2038            priority: None,
2039            stop_if_true: false,
2040        }];
2041        set_conditional_format(&mut ws, &mut ss, "B1:B100", &rules2).unwrap();
2042
2043        assert_eq!(ws.conditional_formatting[0].cf_rules[0].priority, 1);
2044        assert_eq!(ws.conditional_formatting[1].cf_rules[0].priority, 2);
2045    }
2046
2047    #[test]
2048    fn test_explicit_priority() {
2049        let mut ws = WorksheetXml::default();
2050        let mut ss = default_stylesheet();
2051
2052        let rules = vec![ConditionalFormatRule {
2053            rule_type: ConditionalFormatType::DuplicateValues,
2054            format: None,
2055            priority: Some(42),
2056            stop_if_true: false,
2057        }];
2058        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
2059
2060        assert_eq!(ws.conditional_formatting[0].cf_rules[0].priority, 42);
2061    }
2062
2063    // Stop if true
2064
2065    #[test]
2066    fn test_stop_if_true() {
2067        let mut ws = WorksheetXml::default();
2068        let mut ss = default_stylesheet();
2069
2070        let rules = vec![ConditionalFormatRule {
2071            rule_type: ConditionalFormatType::DuplicateValues,
2072            format: None,
2073            priority: None,
2074            stop_if_true: true,
2075        }];
2076        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
2077
2078        assert_eq!(
2079            ws.conditional_formatting[0].cf_rules[0].stop_if_true,
2080            Some(true)
2081        );
2082    }
2083
2084    // DXF style roundtrip
2085
2086    #[test]
2087    fn test_dxf_style_roundtrip_font() {
2088        let style = ConditionalStyle {
2089            font: Some(FontStyle {
2090                bold: true,
2091                italic: true,
2092                color: Some(StyleColor::Rgb("FFFF0000".to_string())),
2093                size: Some(14.0),
2094                name: Some("Arial".to_string()),
2095                ..FontStyle::default()
2096            }),
2097            ..ConditionalStyle::default()
2098        };
2099
2100        let dxf = conditional_style_to_dxf(&style);
2101        let roundtrip = dxf_to_conditional_style(&dxf);
2102
2103        let font = roundtrip.font.unwrap();
2104        assert!(font.bold);
2105        assert!(font.italic);
2106        assert_eq!(font.color, Some(StyleColor::Rgb("FFFF0000".to_string())));
2107        assert_eq!(font.size, Some(14.0));
2108        assert_eq!(font.name, Some("Arial".to_string()));
2109    }
2110
2111    #[test]
2112    fn test_dxf_style_roundtrip_fill() {
2113        let style = ConditionalStyle {
2114            fill: Some(FillStyle {
2115                pattern: PatternType::Solid,
2116                fg_color: Some(StyleColor::Rgb("FFFFFF00".to_string())),
2117                bg_color: None,
2118                gradient: None,
2119            }),
2120            ..ConditionalStyle::default()
2121        };
2122
2123        let dxf = conditional_style_to_dxf(&style);
2124        let roundtrip = dxf_to_conditional_style(&dxf);
2125
2126        let fill = roundtrip.fill.unwrap();
2127        assert_eq!(fill.pattern, PatternType::Solid);
2128        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFFFFF00".to_string())));
2129    }
2130
2131    #[test]
2132    fn test_dxf_style_roundtrip_border() {
2133        let style = ConditionalStyle {
2134            border: Some(BorderStyle {
2135                left: Some(BorderSideStyle {
2136                    style: BorderLineStyle::Thin,
2137                    color: Some(StyleColor::Rgb("FF000000".to_string())),
2138                }),
2139                ..BorderStyle::default()
2140            }),
2141            ..ConditionalStyle::default()
2142        };
2143
2144        let dxf = conditional_style_to_dxf(&style);
2145        let roundtrip = dxf_to_conditional_style(&dxf);
2146
2147        let border = roundtrip.border.unwrap();
2148        let left = border.left.unwrap();
2149        assert_eq!(left.style, BorderLineStyle::Thin);
2150        assert_eq!(left.color, Some(StyleColor::Rgb("FF000000".to_string())));
2151    }
2152
2153    // XML serialization roundtrip
2154
2155    #[test]
2156    fn test_xml_serialization_roundtrip() {
2157        let mut ws = WorksheetXml::default();
2158        let mut ss = default_stylesheet();
2159
2160        let rules = vec![ConditionalFormatRule {
2161            rule_type: ConditionalFormatType::CellIs {
2162                operator: CfOperator::GreaterThan,
2163                formula: "100".to_string(),
2164                formula2: None,
2165            },
2166            format: Some(ConditionalStyle {
2167                font: Some(FontStyle {
2168                    bold: true,
2169                    ..FontStyle::default()
2170                }),
2171                ..ConditionalStyle::default()
2172            }),
2173            priority: None,
2174            stop_if_true: false,
2175        }];
2176
2177        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
2178
2179        let xml = quick_xml::se::to_string(&ws).unwrap();
2180        assert!(xml.contains("conditionalFormatting"));
2181        assert!(xml.contains("cfRule"));
2182        assert!(xml.contains("cellIs"));
2183
2184        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
2185        assert_eq!(parsed.conditional_formatting.len(), 1);
2186        assert_eq!(parsed.conditional_formatting[0].sqref, "A1:A100");
2187        assert_eq!(parsed.conditional_formatting[0].cf_rules.len(), 1);
2188        assert_eq!(
2189            parsed.conditional_formatting[0].cf_rules[0].rule_type,
2190            "cellIs"
2191        );
2192    }
2193
2194    #[test]
2195    fn test_color_scale_xml_roundtrip() {
2196        let mut ws = WorksheetXml::default();
2197        let mut ss = default_stylesheet();
2198
2199        let rules = vec![ConditionalFormatRule {
2200            rule_type: ConditionalFormatType::ColorScale {
2201                min_type: CfValueType::Min,
2202                min_value: None,
2203                min_color: "FFF8696B".to_string(),
2204                mid_type: None,
2205                mid_value: None,
2206                mid_color: None,
2207                max_type: CfValueType::Max,
2208                max_value: None,
2209                max_color: "FF63BE7B".to_string(),
2210            },
2211            format: None,
2212            priority: None,
2213            stop_if_true: false,
2214        }];
2215
2216        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
2217
2218        let xml = quick_xml::se::to_string(&ws).unwrap();
2219        assert!(xml.contains("colorScale"));
2220
2221        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
2222        let cs = parsed.conditional_formatting[0].cf_rules[0]
2223            .color_scale
2224            .as_ref()
2225            .unwrap();
2226        assert_eq!(cs.cfvos.len(), 2);
2227        assert_eq!(cs.colors.len(), 2);
2228    }
2229
2230    #[test]
2231    fn test_data_bar_xml_roundtrip() {
2232        let mut ws = WorksheetXml::default();
2233        let mut ss = default_stylesheet();
2234
2235        let rules = vec![ConditionalFormatRule {
2236            rule_type: ConditionalFormatType::DataBar {
2237                min_type: CfValueType::Min,
2238                min_value: None,
2239                max_type: CfValueType::Max,
2240                max_value: None,
2241                color: "FF638EC6".to_string(),
2242                show_value: true,
2243            },
2244            format: None,
2245            priority: None,
2246            stop_if_true: false,
2247        }];
2248
2249        set_conditional_format(&mut ws, &mut ss, "A1:A100", &rules).unwrap();
2250
2251        let xml = quick_xml::se::to_string(&ws).unwrap();
2252        assert!(xml.contains("dataBar"));
2253
2254        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
2255        let db = parsed.conditional_formatting[0].cf_rules[0]
2256            .data_bar
2257            .as_ref()
2258            .unwrap();
2259        assert_eq!(db.cfvos.len(), 2);
2260    }
2261
2262    // CfOperator / CfValueType parse roundtrip
2263
2264    #[test]
2265    fn test_cf_operator_roundtrip() {
2266        let operators = [
2267            CfOperator::LessThan,
2268            CfOperator::LessThanOrEqual,
2269            CfOperator::Equal,
2270            CfOperator::NotEqual,
2271            CfOperator::GreaterThanOrEqual,
2272            CfOperator::GreaterThan,
2273            CfOperator::Between,
2274            CfOperator::NotBetween,
2275        ];
2276        for op in &operators {
2277            let s = op.as_str();
2278            let parsed = CfOperator::parse(s).unwrap();
2279            assert_eq!(*op, parsed);
2280        }
2281    }
2282
2283    #[test]
2284    fn test_cf_value_type_roundtrip() {
2285        let types = [
2286            CfValueType::Num,
2287            CfValueType::Percent,
2288            CfValueType::Min,
2289            CfValueType::Max,
2290            CfValueType::Percentile,
2291            CfValueType::Formula,
2292        ];
2293        for vt in &types {
2294            let s = vt.as_str();
2295            let parsed = CfValueType::parse(s).unwrap();
2296            assert_eq!(*vt, parsed);
2297        }
2298    }
2299}