diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index 0f9cfb03d..dd50d832c 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -1015,12 +1015,17 @@ impl<'h> Evaluator<'h> { if (rows * cols) as u64 > MAX_CELLS { return err(CellError::Num); } + // The LAMBDA takes one parameter per array. + if l.params.len() != arrays.len() { + return err(CellError::Value); + } let mut out = Vec::with_capacity(rows * cols); for r in 0..rows { for c in 0..cols { let bound = l.params.iter().zip(arrays.iter()).map(|(p, a)| (p.clone(), Ev::V(a.get_broadcast(r, c)))).collect(); let v = self.call_lambda_values(&l, bound); - out.push(self.deref(v).scalar()); + let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; + out.push(one); } } Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) @@ -1041,7 +1046,8 @@ impl<'h> Evaluator<'h> { let r = self.call_lambda_values(&l, bound); acc = self.deref(r); if name == "SCAN" { - scan.push(acc.scalar()); + let Some(one) = single(acc.clone()) else { return err(CellError::Calc) }; + scan.push(one); } } if name == "SCAN" { @@ -1057,6 +1063,9 @@ impl<'h> Evaluator<'h> { let arr = as_array(self.value(&args[0])); let Some(l) = self.lambda_arg(args.get(1), 1) else { return err(CellError::Value) }; let Some(p) = l.params.first().cloned() else { return err(CellError::Value) }; + if l.params.len() != 1 { + return err(CellError::Value); + } let by_row = name == "BYROW"; let n = if by_row { arr.rows } else { arr.cols }; let mut out = Vec::with_capacity(n); @@ -1068,7 +1077,8 @@ impl<'h> Evaluator<'h> { }; let a = if by_row { Array::row(slice) } else { Array::column(slice) }; let r = self.call_lambda_values(&l, vec![(p.clone(), Ev::V(a.into()))]); - out.push(self.deref(r).scalar()); + let Some(one) = single(self.deref(r)) else { return err(CellError::Calc) }; + out.push(one); } let a = if by_row { Array::column(out) } else { Array::row(out) }; Ev::V(a.into()) @@ -1097,7 +1107,8 @@ impl<'h> Evaluator<'h> { (l.params[1].clone(), Ev::V(Value::number(c as f64 + 1.0))), ]; let v = self.call_lambda_values(&l, bound); - out.push(self.deref(v).scalar()); + let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; + out.push(one); } } Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) @@ -1655,6 +1666,15 @@ fn zip3(c: &Array, t: &Value, f: &Value, g: impl Fn(&Value, &Value, &Value) -> V Value::from(Array { rows, cols, data }) } +/// One cell of a LAMBDA helper's result, or `None` for an array of more than one value: arrays +/// can't nest, so the whole helper gives `#CALC!`, as in Excel. +fn single(v: Value) -> Option { + match v { + Value::Array(a) if a.rows * a.cols > 1 => None, + v => Some(v.scalar()), + } +} + pub fn binary_values(op: BinOp, x: &Value, y: &Value, sys: gridcraft_core::DateSystem) -> Value { zip2(x, y, |a, b| binary_scalar(op, a, b, sys)) } diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 196c57de7..efef610d7 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -310,6 +310,29 @@ fn date_text_coerces_in_the_workbook_date_system() { } } +#[test] +fn lambda_helpers_check_shapes_and_parameters() { + let mut t = T::new(); + let calc = Value::Error(CellError::Calc); + // A LAMBDA returning several values can't fill one cell: the whole result is #CALC!. + t.set("A1", "=MAP({1,2},LAMBDA(x,{1,2}))"); + assert_eq!((t.get("A1"), t.get("B1")), (calc.clone(), Value::Empty)); + t.set("A2", "=BYROW({1,2;3,4},LAMBDA(r,r))"); + assert_eq!((t.get("A2"), t.get("A3")), (calc.clone(), Value::Empty)); + t.set("A4", "=MAKEARRAY(1,2,LAMBDA(r,c,{1,2}))"); + assert_eq!(t.get("A4"), calc.clone()); + t.set("D1", "=SCAN(0,{1,2},LAMBDA(a,b,{1,2}))"); + assert_eq!((t.get("D1"), t.get("E1")), (calc, Value::Empty)); + // One parameter per array (MAP) or exactly one (BYROW/BYCOL). + t.set("A5", "=MAP({1,2},LAMBDA(x,y,x))"); + assert_eq!(t.get("A5"), Value::Error(CellError::Value)); + t.set("A6", "=BYCOL({1,2},LAMBDA(c,d,SUM(c)))"); + assert_eq!(t.get("A6"), Value::Error(CellError::Value)); + // A single value in an array is fine. + t.set("A7", "=MAP({1,2},LAMBDA(x,SEQUENCE(1)*x))"); + assert_eq!(t.num("B7"), 2.0); +} + #[test] fn operators() { let mut t = T::new();