mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 19:56:48 -06:00
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:
+24
-4
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user