mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 08:51:30 -06:00
239 lines
11 KiB
Rust
239 lines
11 KiB
Rust
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 => "<cfvo type=\"min\"/>".into(),
|
|
CfValueKind::Max => "<cfvo type=\"max\"/>".into(),
|
|
CfValueKind::Number(n) => format!("<cfvo type=\"num\" val=\"{}\"/>", num(*n)),
|
|
CfValueKind::Percent(n) => format!("<cfvo type=\"percent\" val=\"{}\"/>", num(*n)),
|
|
CfValueKind::Percentile(n) => format!("<cfvo type=\"percentile\" val=\"{}\"/>", num(*n)),
|
|
CfValueKind::Formula(f) => format!("<cfvo type=\"formula\" val=\"{}\"/>", 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!("<extLst><ext uri=\"{{B025F937-C7B1-47D3-B67F-A62EFF666E3E}}\" xmlns:x14=\"{X14_NS}\"><x14:id>{id}</x14:id></ext></extLst>")
|
|
}
|
|
|
|
/// 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 "<x14:cfvo type=\"min\"/>".into(),
|
|
CfValueKind::Max => return "<x14:cfvo type=\"max\"/>".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!("<x14:cfvo type=\"{ty}\"><xm:f>{}</xm:f></x14:cfvo>", esc(&val))
|
|
}
|
|
|
|
pub(super) fn cf_rule_xml(cf: &CondFormat, priority: u32, out: &mut Out) -> String {
|
|
let tl = cf.ranges.first().map(|r| r.start.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!("<formula>{}</formula>", esc(&text_to_file(x)));
|
|
let simple = |ty: &str, st: &Style, extra: &str, body: String, dxf: &mut dyn FnMut(&Style) -> u32| {
|
|
format!("<cfRule type=\"{ty}\" dxfId=\"{}\" priority=\"{priority}\"{stop}{extra}>{body}</cfRule>", 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("<colorScale>");
|
|
for (v, _) in stops {
|
|
b.push_str(&cfvo_xml(v));
|
|
}
|
|
for (_, c) in stops {
|
|
let _ = write!(b, "<color {}/>", color_attrs(c));
|
|
}
|
|
b.push_str("</colorScale>");
|
|
format!("<cfRule type=\"colorScale\" priority=\"{priority}\"{stop}>{b}</cfRule>")
|
|
}
|
|
CfRule::DataBar { min, max, color, show_value, gradient } => format!(
|
|
"<cfRule type=\"dataBar\" priority=\"{priority}\"{stop}><dataBar{}>{}{}<color {}/></dataBar>{}</cfRule>",
|
|
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!("<iconSet iconSet=\"{}\"", esc_attr(set));
|
|
if *reverse {
|
|
b.push_str(" reverse=\"1\"");
|
|
}
|
|
if !*show_value {
|
|
b.push_str(" showValue=\"0\"");
|
|
}
|
|
b.push('>');
|
|
// 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("</iconSet>");
|
|
format!("<cfRule type=\"iconSet\" priority=\"{priority}\"{stop}>{b}</cfRule>")
|
|
}
|
|
}
|
|
}
|
|
|
|
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!("<dataValidation type=\"{ty}\"");
|
|
match v.error_style {
|
|
ErrorStyle::Stop => {}
|
|
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, "<formula1>{}</formula1>", validation_formula(&v.f1));
|
|
}
|
|
if let Some(f2) = v.f2.as_ref().filter(|f| !f.is_empty()) {
|
|
let _ = write!(s, "<formula2>{}</formula2>", validation_formula(f2));
|
|
}
|
|
s.push_str("</dataValidation>");
|
|
s
|
|
}
|