1use 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#[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 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 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#[derive(Debug, Clone, PartialEq)]
64pub enum CfValueType {
65 Num,
66 Percent,
67 Min,
68 Max,
69 Percentile,
70 Formula,
71}
72
73impl CfValueType {
74 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 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#[derive(Debug, Clone, PartialEq)]
102pub enum ConditionalFormatType {
103 CellIs {
105 operator: CfOperator,
106 formula: String,
107 formula2: Option<String>,
109 },
110 Expression { formula: String },
112 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 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 DuplicateValues,
135 UniqueValues,
137 Top10 { rank: u32, percent: bool },
139 Bottom10 { rank: u32, percent: bool },
141 AboveAverage { above: bool, equal_average: bool },
143 ContainsBlanks,
145 NotContainsBlanks,
147 ContainsErrors,
149 NotContainsErrors,
151 ContainsText { text: String },
153 NotContainsText { text: String },
155 BeginsWith { text: String },
157 EndsWith { text: String },
159}
160
161#[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#[derive(Debug, Clone)]
175pub struct ConditionalFormatRule {
176 pub rule_type: ConditionalFormatType,
178 pub format: Option<ConditionalStyle>,
180 pub priority: Option<u32>,
182 pub stop_if_true: bool,
184}
185
186fn 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
202fn 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
280fn 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
300fn 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
312fn 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
331fn 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
391fn 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
402fn 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
415fn 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
435fn 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
447fn 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, 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, 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
831fn 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 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 _ => 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
1011pub 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
1034pub 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
1054pub 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 #[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 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 #[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 #[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 #[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()); 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 #[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 #[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"); assert_eq!(rule.rank, Some(3));
1474 assert_eq!(rule.bottom, Some(true));
1475 }
1476
1477 #[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()); assert!(rule.equal_average.is_none()); }
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}