diff --git a/crates/engine/src/cmd/data/tests.rs b/crates/engine/src/cmd/data/tests.rs index 5b848e5c0..3bca70a87 100644 --- a/crates/engine/src/cmd/data/tests.rs +++ b/crates/engine/src/cmd/data/tests.rs @@ -480,6 +480,25 @@ 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_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 58c445225..71b3b2190 100644 --- a/crates/engine/src/cmd/data/validation.rs +++ b/crates/engine/src/cmd/data/validation.rs @@ -152,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 { +/// 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 { let f = dv.f1.trim(); if is_literal_list(f) { // A quoted list is a string literal: `""` inside stands for one `"`. @@ -163,8 +165,8 @@ pub fn list_items(wb: &Workbook, sheet: usize, dv: &Validation) -> Vec { }; return items.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(); } - let at = RangeRef::top_left(&dv.ranges).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![], @@ -200,7 +202,7 @@ 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)), + ValidationKind::List => list_items(wb, 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; diff --git a/crates/ui-egui/src/grid/editor.rs b/crates/ui-egui/src/grid/editor.rs index 59f375e80..72b7cc7bb 100644 --- a/crates/ui-egui/src/grid/editor.rs +++ b/crates/ui-egui/src/grid/editor.rs @@ -125,7 +125,7 @@ pub(super) fn list_picker(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_ app.grid.list_picker = None; return; }; - let items = gridcraft_engine::cmd::data::list_items(&wb, si, &dv); + let items = gridcraft_engine::cmd::data::list_items(&wb, si, c, &dv); let r = geo.cell_rect(sh, c); // Fit the popup to its rows (up to LIST_PICKER_MAX_ROWS); without a minimum the scroll area collapses to its 64 px default. let height = list_picker_height(items.len(), LIST_PICKER_ROW_H, ui.spacing().item_spacing.y);