From 342f4b401d0cb21bea2de64c089032a8922a4bc7 Mon Sep 17 00:00:00 2001 From: ljkx <177671075+ljkx@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:26:30 +0100 Subject: [PATCH] Check list validation against the list before the entry A source that takes in the cell being checked (=$A1:$A2 on A2:A3 is A2:A3 at A3) listed the value being typed, so any new value passed. The list is now read from the workbook as it was before the entry, as in Excel. --- crates/engine/src/cmd/data/tests.rs | 20 ++++++++++++++++++++ crates/engine/src/cmd/data/validation.rs | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/data/tests.rs b/crates/engine/src/cmd/data/tests.rs index 3bca70a87..3c2782444 100644 --- a/crates/engine/src/cmd/data/tests.rs +++ b/crates/engine/src/cmd/data/tests.rs @@ -499,6 +499,26 @@ fn a_relative_list_source_is_read_from_the_cell_being_checked() { } } +#[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, diff --git a/crates/engine/src/cmd/data/validation.rs b/crates/engine/src/cmd/data/validation.rs index 71b3b2190..2217a372a 100644 --- a/crates/engine/src/cmd/data/validation.rs +++ b/crates/engine/src/cmd/data/validation.rs @@ -187,6 +187,7 @@ pub fn check_validation(wb: &Workbook, sheet: usize, at: CellRef, input: &str) - // 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 = RangeRef::top_left(&dv.ranges).unwrap_or(at); + let original = wb; let entered; let wb = if sh.value(at) == v { wb @@ -202,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, at, 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;