LAMBDA helpers: #CALC! for nested arrays, #VALUE! for wrong parameter counts

MAP, BYROW, BYCOL, SCAN and MAKEARRAY put one value in each cell, but a
LAMBDA returning several values silently gave its first one:
MAP({1,2},LAMBDA(x,{1,2})) was {1,1} and BYROW(…,LAMBDA(r,r)) the first
column. As in Excel, the whole result is now #CALC!, as arrays can't
nest (a single-value array is still fine). MAP with a LAMBDA taking a
different number of parameters than it has arrays, and BYROW or BYCOL
with one taking other than one, ran anyway; Excel gives #VALUE!.
This commit is contained in:
nihilist-narwhal
2026-10-13 06:54:46 -05:00
parent 84acf6293b
commit 93338d06ce
2 changed files with 47 additions and 4 deletions
+24 -4
View File
@@ -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<Value> {
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))
}
+23
View File
@@ -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();