mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 20:09:57 -06:00
Merge PR #3173
This commit is contained in:
@@ -480,6 +480,45 @@ fn a_list_typed_without_quotes_is_saved_as_a_quoted_list() {
|
||||
assert_eq!(sheet_xml(&reopened), xml, "a second save writes the same rules");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relative_list_source_is_read_from_the_cell_being_checked() {
|
||||
// As in Excel, `=$B1:$B2` on A3:A4,A1:A2 is written for A1 (the top-left cell of all the
|
||||
// areas) and lists B4:B5 at A4. It used to list B1:B2 in every cell.
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "B1:B5", "values": [["a"], ["b"], ["c"], ["d"], ["e"]]})).unwrap();
|
||||
s.execute("data.validation", json!({"range": "A3:A4,A1:A2", "type": "list", "formula1": "=$B1:$B2"})).unwrap();
|
||||
let wb = s.doc().unwrap().wb.clone();
|
||||
let dv = wb.active().unwrap().validations[0].clone();
|
||||
for (cell, items) in [("A1", ["a", "b"]), ("A2", ["b", "c"]), ("A3", ["c", "d"]), ("A4", ["d", "e"])] {
|
||||
let at = CellRef::parse(cell).unwrap();
|
||||
assert_eq!(list_items(&wb, 0, at, &dv), items, "{cell}");
|
||||
for v in ["a", "b", "c", "d", "e"] {
|
||||
assert_eq!(check_validation(&wb, 0, at, v).is_none(), items.contains(&v), "{cell} with {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_list_source_that_takes_in_the_cell_does_not_list_the_value_being_typed() {
|
||||
// `=$A1:$A2` on A2:A3 is A1:A2 at A2 and A2:A3 at A3. The list is the one before the entry, as
|
||||
// in Excel; a new value used to land in its own source and pass.
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "A1:A2", "values": [["x"], ["y"]]})).unwrap();
|
||||
s.execute("data.validation", json!({"range": "A2:A3", "type": "list", "formula1": "=$A1:$A2"})).unwrap();
|
||||
let wb = &s.doc().unwrap().wb;
|
||||
for (cell, accepted, refused) in [("A2", &["x", "y"][..], &["new"][..]), ("A3", &["y"], &["x", "new"])] {
|
||||
let at = CellRef::parse(cell).unwrap();
|
||||
for v in accepted {
|
||||
assert!(check_validation(wb, 0, at, v).is_none(), "{cell} refuses {v}");
|
||||
}
|
||||
for v in refused {
|
||||
assert!(check_validation(wb, 0, at, v).is_some(), "{cell} accepts {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rule_with_an_unknown_type_operator_or_error_style_is_refused() {
|
||||
// A typo used to give a rule that validates nothing (`type="none"`) or the wrong check,
|
||||
|
||||
@@ -38,11 +38,18 @@ fn subtract_range(r: RangeRef, cut: &RangeRef) -> Vec<RangeRef> {
|
||||
}
|
||||
|
||||
/// Removes the cells of `cuts` from every validation, keeping the rest of each rule's ranges.
|
||||
/// When the rule's top-left cell goes, its relative formulas are rewritten for the new top-left
|
||||
/// cell, so the cells left keep checking the same cells.
|
||||
pub(in super::super) fn remove_validation_cells(validations: &mut Vec<Validation>, cuts: &[RangeRef]) {
|
||||
for dv in validations.iter_mut() {
|
||||
let before = RangeRef::top_left(&dv.ranges);
|
||||
for cut in cuts {
|
||||
dv.ranges = dv.ranges.iter().flat_map(|r| subtract_range(*r, cut)).collect();
|
||||
}
|
||||
if let (Some(from), Some(to)) = (before, RangeRef::top_left(&dv.ranges)) {
|
||||
dv.f1 = move_rule_formula(&dv.f1, from, to);
|
||||
dv.f2 = dv.f2.take().map(|f| move_rule_formula(&f, from, to));
|
||||
}
|
||||
}
|
||||
validations.retain(|d| !d.ranges.is_empty());
|
||||
}
|
||||
@@ -145,8 +152,10 @@ fn list_source(f: &str) -> String {
|
||||
format!("\"{}\"", t.replace('"', "\"\""))
|
||||
}
|
||||
|
||||
/// List entries of a list validation (literal or from a range).
|
||||
pub fn list_items(wb: &Workbook, sheet: usize, dv: &Validation) -> Vec<String> {
|
||||
/// List entries of a list validation (literal or from a range) for the cell `at`. A relative
|
||||
/// source is written for the rule's top-left cell and read from `at`, as in Excel (`=$B1:$B3` on
|
||||
/// A1:A9 lists B5:B7 at A5).
|
||||
pub fn list_items(wb: &Workbook, sheet: usize, at: CellRef, dv: &Validation) -> Vec<String> {
|
||||
let f = dv.f1.trim();
|
||||
if is_literal_list(f) {
|
||||
// A quoted list is a string literal: `""` inside stands for one `"`.
|
||||
@@ -156,8 +165,8 @@ 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();
|
||||
match gridcraft_calc::evaluate(wb, sheet, at, f) {
|
||||
let first = RangeRef::top_left(&dv.ranges).unwrap_or(at);
|
||||
match gridcraft_calc::evaluate(wb, sheet, at, &formula_at(f, first, at)) {
|
||||
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()],
|
||||
_ => vec![],
|
||||
@@ -175,9 +184,10 @@ 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 original = wb;
|
||||
let entered;
|
||||
let wb = if sh.value(at) == v {
|
||||
wb
|
||||
@@ -193,7 +203,9 @@ pub fn check_validation(wb: &Workbook, sheet: usize, at: CellRef, input: &str) -
|
||||
let eval = |f: &str| gridcraft_calc::evaluate(wb, sheet, at, &formula_at(f, first, at));
|
||||
let num = |f: &str| eval(f).to_number().ok();
|
||||
let ok = match dv.kind {
|
||||
ValidationKind::List => list_items(wb, sheet, dv).iter().any(|x| x.eq_ignore_ascii_case(input)),
|
||||
// The list is the one the dropdown showed before the entry, as in Excel: a source that takes
|
||||
// in the cell itself (`=$A1:$A2` on A2:A3 is A2:A3 at A3) doesn't list the value being typed.
|
||||
ValidationKind::List => list_items(original, sheet, at, dv).iter().any(|x| x.eq_ignore_ascii_case(input)),
|
||||
ValidationKind::Custom => eval(&dv.f1).to_bool().unwrap_or(false),
|
||||
ValidationKind::TextLength => {
|
||||
let n = input.chars().count() as f64;
|
||||
|
||||
@@ -284,7 +284,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)];
|
||||
|
||||
@@ -1733,7 +1733,13 @@ fn clear_cf(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
sh.cond_formats.clear();
|
||||
} else {
|
||||
for cf in sh.cond_formats.iter_mut() {
|
||||
let before = RangeRef::top_left(&cf.ranges);
|
||||
cf.ranges.retain(|r| !ranges.iter().any(|x| x.intersects(r)));
|
||||
// The relative formulas are written for the top-left cell: when it goes, they're
|
||||
// rewritten for the new one, so the areas left keep reading the same cells.
|
||||
if let (Some(from), Some(to)) = (before, RangeRef::top_left(&cf.ranges)) {
|
||||
rebase_cf_formulas(&mut cf.rule, from, to);
|
||||
}
|
||||
}
|
||||
sh.cond_formats.retain(|c| !c.ranges.is_empty());
|
||||
}
|
||||
@@ -1741,6 +1747,19 @@ fn clear_cf(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the relative references of a rule's per-cell formulas written for `from` to `to`.
|
||||
fn rebase_cf_formulas(rule: &mut CfRule, from: CellRef, to: CellRef) {
|
||||
let mv = |f: &str| crate::cmd::data::move_rule_formula(f, from, to);
|
||||
match rule {
|
||||
CfRule::CellIs { a, b, .. } => {
|
||||
*a = mv(a);
|
||||
*b = b.as_deref().map(mv);
|
||||
}
|
||||
CfRule::Expression { formula, .. } => *formula = mv(formula),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_cf(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let sheet = target_sheet(s, p)?;
|
||||
if p.get("delete").is_none() && p.get("moveUp").is_none() && p.get("moveDown").is_none() {
|
||||
|
||||
Reference in New Issue
Block a user