use super::*; fn cf_op_name(op: CfOperator) -> &'static str { match op { CfOperator::Between => "between", CfOperator::NotBetween => "notBetween", CfOperator::Equal => "equal", CfOperator::NotEqual => "notEqual", CfOperator::Greater => "greaterThan", CfOperator::Less => "lessThan", CfOperator::GreaterOrEqual => "greaterThanOrEqual", CfOperator::LessOrEqual => "lessThanOrEqual", } } fn cfvo_xml(v: &CfValueKind) -> String { match v { CfValueKind::Min => "".into(), CfValueKind::Max => "".into(), CfValueKind::Number(n) => format!("", num(*n)), CfValueKind::Percent(n) => format!("", num(*n)), CfValueKind::Percentile(n) => format!("", num(*n)), CfValueKind::Formula(f) => format!("", esc_attr(&text_to_file(f))), } } fn quote(t: &str) -> String { format!("\"{}\"", t.replace('"', "\"\"")) } /// The id linking a solid data bar's `cfRule` to its x14 extension; unique per sheet because priorities are. pub(super) fn solid_bar_id(priority: u32) -> String { format!("{{00000000-0000-0000-0000-{priority:012}}}") } /// The `cfRule` extension that points a solid data bar at its x14 rule. fn solid_bar_ext(priority: u32) -> String { let id = solid_bar_id(priority); format!("{id}") } /// An x14 `cfvo`: unlike the 2006 one it has no `val` attribute, the value is an `xm:f` child. pub(super) fn x14_cfvo(v: &CfValueKind) -> String { let (ty, val) = match v { CfValueKind::Min => return "".into(), CfValueKind::Max => return "".into(), CfValueKind::Number(n) => ("num", num(*n)), CfValueKind::Percent(n) => ("percent", num(*n)), CfValueKind::Percentile(n) => ("percentile", num(*n)), CfValueKind::Formula(f) => ("formula", text_to_file(f)), }; format!("{}", esc(&val)) } /// One `cfRule` element. The formulas generated for text, blank, error and date rules refer to the /// top-left cell of all the rule's areas, the cell Excel reads relative references from. pub(super) fn cf_rule_xml(cf: &CondFormat, priority: u32, out: &mut Out) -> String { let tl = RangeRef::top_left(&cf.ranges).map(|c| c.a1()).unwrap_or_else(|| "A1".into()); let stop = if cf.stop_if_true { " stopIfTrue=\"1\"" } else { "" }; let mut dxf = |st: &Style| dxf_id(&mut out.dxfs, st); let f = |x: &str| format!("{}", esc(&text_to_file(x))); let simple = |ty: &str, st: &Style, extra: &str, body: String, dxf: &mut dyn FnMut(&Style) -> u32| { format!("{body}", dxf(st)) }; match &cf.rule { CfRule::CellIs { op, a, b, style } => { let mut body = f(a); if let Some(b) = b { body.push_str(&f(b)); } simple("cellIs", style, &format!(" operator=\"{}\"", cf_op_name(*op)), body, &mut dxf) } CfRule::Expression { formula, style } => simple("expression", style, "", f(formula), &mut dxf), CfRule::ContainsText { text, style } => simple( "containsText", style, &format!(" operator=\"containsText\" text=\"{}\"", esc_attr(text)), f(&format!("NOT(ISERROR(SEARCH({},{tl})))", quote(text))), &mut dxf, ), CfRule::NotContainsText { text, style } => simple( "notContainsText", style, &format!(" operator=\"notContains\" text=\"{}\"", esc_attr(text)), f(&format!("ISERROR(SEARCH({},{tl}))", quote(text))), &mut dxf, ), CfRule::BeginsWith { text, style } => simple( "beginsWith", style, &format!(" operator=\"beginsWith\" text=\"{}\"", esc_attr(text)), f(&format!("LEFT({tl},LEN({0}))={0}", quote(text))), &mut dxf, ), CfRule::EndsWith { text, style } => simple( "endsWith", style, &format!(" operator=\"endsWith\" text=\"{}\"", esc_attr(text)), f(&format!("RIGHT({tl},LEN({0}))={0}", quote(text))), &mut dxf, ), CfRule::Blanks { style } => simple("containsBlanks", style, "", f(&format!("LEN(TRIM({tl}))=0")), &mut dxf), CfRule::NoBlanks { style } => simple("notContainsBlanks", style, "", f(&format!("LEN(TRIM({tl}))>0")), &mut dxf), CfRule::Errors { style } => simple("containsErrors", style, "", f(&format!("ISERROR({tl})")), &mut dxf), CfRule::NoErrors { style } => simple("notContainsErrors", style, "", f(&format!("NOT(ISERROR({tl}))")), &mut dxf), CfRule::Duplicate { style } => simple("duplicateValues", style, "", String::new(), &mut dxf), CfRule::Unique { style } => simple("uniqueValues", style, "", String::new(), &mut dxf), CfRule::Top10 { bottom, percent, rank, style } => { let extra = format!("{}{} rank=\"{}\"", if *percent { " percent=\"1\"" } else { "" }, if *bottom { " bottom=\"1\"" } else { "" }, rank); simple("top10", style, &extra, String::new(), &mut dxf) } CfRule::AboveAverage { below, equal, std_dev, style } => { let mut extra = String::new(); if *below { extra.push_str(" aboveAverage=\"0\""); } if *equal { extra.push_str(" equalAverage=\"1\""); } if *std_dev > 0 { let _ = write!(extra, " stdDev=\"{std_dev}\""); } simple("aboveAverage", style, &extra, String::new(), &mut dxf) } CfRule::TimePeriod { period, style } => { let d = format!("FLOOR({tl},1)"); let body = match period.as_str() { "yesterday" => format!("{d}=TODAY()-1"), "tomorrow" => format!("{d}=TODAY()+1"), "last7Days" => format!("AND(TODAY()-{d}<=6,{d}<=TODAY())"), "thisMonth" => format!("AND(MONTH({tl})=MONTH(TODAY()),YEAR({tl})=YEAR(TODAY()))"), "lastMonth" => format!("AND(MONTH({tl})=MONTH(EDATE(TODAY(),0-1)),YEAR({tl})=YEAR(EDATE(TODAY(),0-1)))"), "nextMonth" => format!("AND(MONTH({tl})=MONTH(EDATE(TODAY(),0+1)),YEAR({tl})=YEAR(EDATE(TODAY(),0+1)))"), "thisWeek" => format!("AND(TODAY()-ROUNDDOWN({tl},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN({tl},0)-TODAY()<=7-WEEKDAY(TODAY()))"), "lastWeek" => format!("AND(TODAY()-ROUNDDOWN({tl},0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN({tl},0)<(WEEKDAY(TODAY())+7))"), "nextWeek" => format!("AND(ROUNDDOWN({tl},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN({tl},0)-TODAY()<(15-WEEKDAY(TODAY())))"), _ => format!("{d}=TODAY()"), }; simple("timePeriod", style, &format!(" timePeriod=\"{}\"", esc_attr(period)), f(&body), &mut dxf) } CfRule::ColorScale { stops } => { let mut b = String::from(""); for (v, _) in stops { b.push_str(&cfvo_xml(v)); } for (_, c) in stops { let _ = write!(b, "", color_attrs(c)); } b.push_str(""); format!("{b}") } CfRule::DataBar { min, max, color, show_value, gradient } => format!( "{}{}{}", if *show_value { "" } else { " showValue=\"0\"" }, cfvo_xml(min), cfvo_xml(max), if *color == Color::Auto { "rgb=\"FF638EC6\"".to_string() } else { color_attrs(color) }, if *gradient { String::new() } else { solid_bar_ext(priority) } ), CfRule::IconSet { set, thresholds, reverse, show_value } => { let mut b = format!("'); // CT_IconSet needs one cfvo per icon (minOccurs 2); the first is the lower bound. let k = icon_count(set); let defaults = default_icon_thresholds(set); let cfvos: Vec<&CfValueKind> = match thresholds.len() { n if n == k => thresholds.iter().collect(), n if n + 1 == k => defaults.iter().take(1).chain(thresholds).collect(), _ => defaults.iter().collect(), }; for t in cfvos { b.push_str(&cfvo_xml(t)); } b.push_str(""); format!("{b}") } } } fn validation_formula(f: &str) -> String { let f = f.trim(); if f.starts_with('"') { return esc(f); } esc(&text_to_file(f.strip_prefix('=').unwrap_or(f))) } pub(super) fn validation_xml(v: &Validation) -> String { let ty = match v.kind { ValidationKind::Any => "none", ValidationKind::Whole => "whole", ValidationKind::Decimal => "decimal", ValidationKind::List => "list", ValidationKind::Date => "date", ValidationKind::Time => "time", ValidationKind::TextLength => "textLength", ValidationKind::Custom => "custom", }; let mut s = format!(" {} ErrorStyle::Warning => s.push_str(" errorStyle=\"warning\""), ErrorStyle::Information => s.push_str(" errorStyle=\"information\""), } if v.op != CfOperator::Between && !matches!(v.kind, ValidationKind::List | ValidationKind::Custom | ValidationKind::Any) { let _ = write!(s, " operator=\"{}\"", cf_op_name(v.op)); } if v.allow_blank { s.push_str(" allowBlank=\"1\""); } if !v.in_cell_dropdown { s.push_str(" showDropDown=\"1\""); } if v.show_input { s.push_str(" showInputMessage=\"1\""); } if v.show_error { s.push_str(" showErrorMessage=\"1\""); } for (a, t) in [("errorTitle", &v.error_title), ("error", &v.error_message), ("promptTitle", &v.input_title), ("prompt", &v.input_message)] { if !t.is_empty() { let _ = write!(s, " {a}=\"{}\"", esc_attr(t)); } } let _ = write!(s, " sqref=\"{}\">", sqref(&v.ranges)); if !v.f1.is_empty() { let _ = write!(s, "{}", validation_formula(&v.f1)); } if let Some(f2) = v.f2.as_ref().filter(|f| !f.is_empty()) { let _ = write!(s, "{}", validation_formula(f2)); } s.push_str(""); s }