From 85ca8f6af57cca475886d6b9179abb4cfb057060 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:54:04 -0500 Subject: [PATCH 1/2] INDEX: arrays of row and column numbers pick a value each INDEX took one row and one column number, so INDEX(A1:C3,{1;3},{1,3}) gave a single value and SUM(INDEX(A1:C3,{1,2,3},2)) summed one cell. In Excel, arrays of positions return an array of the values at each (broadcast) position. A position that picks a whole row or column gives #CALC! there, as arrays can't nest. --- crates/calc/src/eval.rs | 57 +++++++++++++++++++++++++++++++++------- crates/calc/src/tests.rs | 18 +++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index 3d4a743d9..9aa80fdca 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -1085,18 +1085,57 @@ impl<'h> Evaluator<'h> { return err(CellError::Value); } let src = self.eval(&args[0]); - let row = match self.arg_num(args, 1, 0.0) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - let col = match self.arg_num(args, 2, 0.0) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), + let mut arg = |i: usize| match args.get(i) { + None | Some(Expr::Missing) => None, + Some(e) => Some(self.value(e)), }; + let (rows, cols) = (arg(1), arg(2)); let area_n = match self.arg_num(args, 3, 1.0) { Ok(n) => n.trunc() as i64, Err(e) => return err(e), }; + let two = args.len() == 2; + let num = |v: &Option| v.as_ref().map_or(Ok(0.0), |v| v.to_number()).map(|n| n.trunc() as i64); + // Arrays of positions pick a value for each: INDEX(A1:C10,{1;2},{1,3}) is 2×2. + if let (Some(Value::Array(_)), _) | (_, Some(Value::Array(_))) = (&rows, &cols) { + let as_array = |v: &Option| match v { + Some(Value::Array(a)) => Arc::clone(a), + Some(v) => Arc::new(Array::scalar(v.clone())), + None => Arc::new(Array::scalar(Value::Number(0.0))), + }; + let (ra, ca) = (as_array(&rows), as_array(&cols)); + let (h, w) = (ra.rows.max(ca.rows), ra.cols.max(ca.cols)); + if (h * w) as u64 > MAX_CELLS { + return err(CellError::Num); + } + let mut out = Vec::with_capacity(h * w); + for r in 0..h { + for c in 0..w { + let pos = (num(&Some(ra.get_broadcast(r, c))), num(&Some(ca.get_broadcast(r, c)))); + let v = match pos { + (Ok(row), Ok(col)) => { + let one = self.index_one(src.clone(), row, col, area_n, two); + match self.deref(one) { + Value::Array(a) if a.rows * a.cols > 1 => Value::Error(CellError::Calc), + v => v.scalar(), + } + } + (Err(e), _) | (_, Err(e)) => Value::Error(e), + }; + out.push(v); + } + } + return Ev::V(Array::new(h, w, out).map(Value::from).unwrap_or(Value::Error(CellError::Value))); + } + let (row, col) = match (num(&rows), num(&cols)) { + (Ok(r), Ok(c)) => (r, c), + (Err(e), _) | (_, Err(e)) => return err(e), + }; + self.index_one(src, row, col, area_n, two) + } + + /// INDEX for one row and column position (0 = the whole column or row). + fn index_one(&mut self, src: Ev, row: i64, col: i64, area_n: i64, two_args: bool) -> Ev { if row < 0 || col < 0 { return err(CellError::Value); } @@ -1105,7 +1144,7 @@ impl<'h> Evaluator<'h> { let Some(a) = areas.get((area_n - 1).max(0) as usize).copied() else { return err(CellError::Ref) }; let r = a.range; // A single row or column with one index picks along it. - let (row, col) = if args.len() == 2 && r.height() == 1 { (1, row) } else { (row, col) }; + let (row, col) = if two_args && r.height() == 1 { (1, row) } else { (row, col) }; if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 { return err(CellError::Ref); } @@ -1114,7 +1153,7 @@ impl<'h> Evaluator<'h> { Ev::R(vec![Area { sheet: a.sheet, range: RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1)) }]) } Ev::V(Value::Array(arr)) => { - let (row, col) = if args.len() == 2 && arr.rows == 1 { (1, row) } else { (row, col) }; + let (row, col) = if two_args && arr.rows == 1 { (1, row) } else { (row, col) }; if row as usize > arr.rows || col as usize > arr.cols { return err(CellError::Ref); } diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 2205a1e61..50c4353bc 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -106,6 +106,24 @@ fn long_chain() { assert_eq!(t.num("A3000"), 3009.0); } +#[test] +fn index_with_arrays_of_positions() { + let mut t = T::new(); + for (i, row) in [[1, 2, 3], [4, 5, 6], [7, 8, 9]].iter().enumerate() { + for (j, v) in row.iter().enumerate() { + t.set(&format!("{}{}", ["A", "B", "C"][j], i + 1), &v.to_string()); + } + } + t.set("E1", "=INDEX(A1:C3,{1;3},{1,3})"); + assert_eq!([t.num("E1"), t.num("F1"), t.num("E2"), t.num("F2")], [1.0, 3.0, 7.0, 9.0]); + t.set("E4", "=SUM(INDEX(A1:C3,{1,2,3},2))"); + assert_eq!(t.num("E4"), 15.0); + t.set("E5", "=INDEX({10,20,30},{3,1})"); + assert_eq!([t.num("E5"), t.num("F5")], [30.0, 10.0]); + t.set("E7", "=INDEX(A1:C3,2,3)"); + assert_eq!(t.num("E7"), 6.0); +} + #[test] fn errors_and_cycles() { let mut t = T::new(); From fca801c17e0ec88379839c99dafed25e5c38b603 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 11 Oct 2026 15:43:11 -0400 Subject: [PATCH 2/2] INDEX: compare the result size with saturating u64 math, so it can't overflow on 32-bit targets (review fix-up for #400) Co-Authored-By: Claude Opus 5.5 --- crates/calc/src/eval.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index adaeeba3b..a7c677dc2 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -1112,7 +1112,7 @@ impl<'h> Evaluator<'h> { }; let (ra, ca) = (as_array(&rows), as_array(&cols)); let (h, w) = (ra.rows.max(ca.rows), ra.cols.max(ca.cols)); - if (h * w) as u64 > MAX_CELLS { + if (h as u64).saturating_mul(w as u64) > MAX_CELLS { return err(CellError::Num); } let mut out = Vec::with_capacity(h * w);