mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 04:19:48 -06:00
Data validation and conditional formats: anchor relative formulas on the top-left cell of all areas (#3152)
Excel writes the relative formulas of a multi-area rule for the top-left cell of the whole sqref (lowest row, lowest column), whatever order the areas are listed in. GridCraft anchored them on the first area listed, so a validation on `H62:H68 H26:H27 H93:H95` with `MOD($H26,$F26)=0` checked row 26 - 36 at H26 and refused valid input. Add `RangeRef::top_left` and use it in validation checks, list sources, pasting a validation, conditional format lookups and the formulas the xlsx writer generates for text, blank and error rules.
This commit is contained in:
@@ -244,7 +244,7 @@ impl CfCache {
|
||||
let v = sh.value(c);
|
||||
let no_stats = RuleStats::default();
|
||||
for (idx, cf) in rules {
|
||||
let first = cf.ranges.first().map(|r| r.start).unwrap_or(c);
|
||||
let first = RangeRef::top_left(&cf.ranges).unwrap_or(c);
|
||||
let st: &RuleStats = match stats_need(&cf.rule) {
|
||||
Some(need) => &*self.stats.entry(idx).or_insert_with(|| RuleStats::build(sh, &cf.ranges, need)),
|
||||
None => &no_stats,
|
||||
|
||||
@@ -156,7 +156,7 @@ pub fn list_items(wb: &Workbook, sheet: usize, dv: &Validation) -> Vec<String> {
|
||||
};
|
||||
return items.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect();
|
||||
}
|
||||
let at = dv.ranges.first().map(|r| r.start).unwrap_or_default();
|
||||
let at = RangeRef::top_left(&dv.ranges).unwrap_or_default();
|
||||
match gridcraft_calc::evaluate(wb, sheet, at, f) {
|
||||
Value::Array(a) => a.data.iter().filter(|v| !v.is_empty()).map(Value::display).collect(),
|
||||
v if !v.is_empty() && !v.is_error() => vec![v.display()],
|
||||
@@ -175,9 +175,9 @@ pub fn check_validation(wb: &Workbook, sheet: usize, at: CellRef, input: &str) -
|
||||
return None;
|
||||
}
|
||||
let v = gridcraft_core::parse::parse_input(input, wb.date_system).value;
|
||||
// The formulas are written for the first cell of the range and see the value being entered,
|
||||
// The formulas are written for the top-left cell of all the areas and see the value being entered,
|
||||
// as in Excel (a custom `=A1>10` on A1:A9 checks A5's new value at A5).
|
||||
let first = dv.ranges.first().map(|r| r.start).unwrap_or(at);
|
||||
let first = RangeRef::top_left(&dv.ranges).unwrap_or(at);
|
||||
let entered;
|
||||
let wb = if sh.value(at) == v {
|
||||
wb
|
||||
|
||||
@@ -248,7 +248,7 @@ pub(super) fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
crate::cmd::data::remove_validation_cells(&mut sh.validations, &[RangeRef::new(dest, dest)]);
|
||||
if let Some(mut dv) = rule {
|
||||
// Relative references move with the rule, as pasted formulas do.
|
||||
let first = dv.ranges.first().map_or(sc, |r| r.start);
|
||||
let first = RangeRef::top_left(&dv.ranges).unwrap_or(sc);
|
||||
dv.f1 = crate::cmd::data::move_rule_formula(&dv.f1, first, dest);
|
||||
dv.f2 = dv.f2.map(|f| crate::cmd::data::move_rule_formula(&f, first, dest));
|
||||
dv.ranges = vec![RangeRef::new(dest, dest)];
|
||||
|
||||
@@ -2743,6 +2743,30 @@ fn validation_formulas_are_relative_and_see_the_new_value() {
|
||||
assert!(!ok(&mut s, "C1", "50"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_area_rule_formulas_are_relative_to_the_top_left_cell_of_all_areas() {
|
||||
// Issue #3152: Excel writes the formula for the top-left cell of all the areas (H26), whatever
|
||||
// order they are listed in, not for the first area listed (H62).
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "F26:F27", "values": [[4], [5]]})).unwrap();
|
||||
s.execute("data.validation", json!({"range": "H62:H68,H26:H27,H93:H95", "type": "custom", "formula1": "=MOD($H26,$F26)=0"})).unwrap();
|
||||
let ok = |s: &mut Session, cell: &str, input: &str| s.execute("data.validate", json!({"cell": cell, "input": input})).unwrap()["ok"] == true;
|
||||
assert!(ok(&mut s, "H26", "4"));
|
||||
assert!(ok(&mut s, "H26", "8"));
|
||||
assert!(!ok(&mut s, "H26", "3"));
|
||||
assert!(ok(&mut s, "H27", "10"));
|
||||
assert!(!ok(&mut s, "H27", "4"));
|
||||
|
||||
// Conditional formats anchor on the same cell: `=D2>0` on D5:D6,D2:D3 checks D2 at D2.
|
||||
s.execute("range.setValues", json!({"range": "D2:D6", "values": [[1], [0], [0], [1], [0]]})).unwrap();
|
||||
s.execute("home.conditionalFormat", json!({"range": "D5:D6,D2:D3", "rule": {"type": "expression", "formula": "=D2>0", "preset": "greenFill"}}))
|
||||
.unwrap();
|
||||
let wb = s.doc().unwrap().wb.clone();
|
||||
let mut cache = crate::cf::CfCache::new();
|
||||
let mut styled = |c: &str| cache.look(&wb, 0, CellRef::parse(c).unwrap()).and_then(|l| l.style).is_some();
|
||||
assert_eq!([styled("D2"), styled("D3"), styled("D5"), styled("D6")], [true, false, true, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samples_build() {
|
||||
for (name, _) in crate::sample::SAMPLES {
|
||||
|
||||
Reference in New Issue
Block a user