fix(functions): a blank lookup given as an argument is not an omitted one

FILTER(1,FALSE,VLOOKUP(...)) with a blank lookup result returned #CALC!
because util::has read the blank value as a left-out argument. Arguments
written in the formula that compute a blank are now marked as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-11-08 13:27:08 +01:00
1 parent dae5b52d78
commit 63150810b3
7 files changed
+21 -17

No files matched your search

+6 -4
View File
@@ -203,9 +203,9 @@ impl<'h> Evaluator<'h> {
let index = self.value(&args[0]);
if let Value::Array(_) = index {
let Some(spec) = gridcraft_functions::lookup("CHOOSE") else { return err(CellError::Name) };
let mut evaluated = vec![Arg { value: index, from_ref: false }];
let mut evaluated = vec![Arg::val(index)];
for argument in &args[1..] {
evaluated.push(Arg { value: self.value(argument), from_ref: false });
evaluated.push(Arg::val(self.value(argument)));
}
let mut context = FnCtx { ev: self, criteria: None, lambda: None };
return gridcraft_functions::call(spec, &evaluated, &mut context).pipe(Ev::V);
@@ -351,7 +351,9 @@ impl<'h> Evaluator<'h> {
}
ev => self.deref(ev),
};
evaluated.push(Arg { value, from_ref });
// Only an argument left out (`f(a,,b)`) is omitted; one that computes a blank is given.
let blank_value = !from_ref && value == Value::Empty && lambda_at != Some(i) && !matches!(a, Expr::Missing);
evaluated.push(Arg { value, from_ref, blank_value });
}
let mut ctx = FnCtx { ev: self, criteria, lambda };
gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
@@ -414,7 +416,7 @@ impl<'h> Evaluator<'h> {
let (Some(sum), Some(count)) = (gridcraft_functions::lookup("SUM"), gridcraft_functions::lookup("COUNT")) else {
return Err(err(CellError::Name));
};
let args = [Arg { value, from_ref }];
let args = [Arg { value, from_ref, blank_value: false }];
if kind != Agg::Count {
match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) {
Value::Number(n) => acc.sum += n,
+4 -3
View File
@@ -203,7 +203,7 @@ impl<'h> Evaluator<'h> {
let horizontal = l.range.height() == 1 && l.range.width() > 1;
let same_len = if vertical { r.range.height() == l.range.height() } else { r.range.width() == l.range.width() };
if !matches!(key, Value::Array(_)) && (vertical || horizontal) && same_len {
let mut xargs = vec![Arg { value: key, from_ref: false }, Arg { value: self.area_value(&l), from_ref: true }];
let mut xargs = vec![Arg::val(key), Arg::reference(self.area_value(&l))];
// An empty match_mode before a given search_mode is the default (0), not the end.
let given = |i: usize| args.get(i).is_some_and(|e| !matches!(e, Expr::Missing));
let last = if given(5) {
@@ -215,10 +215,11 @@ impl<'h> Evaluator<'h> {
};
for i in 4..=last {
match args.get(i) {
Some(Expr::Missing) | None => xargs.push(Arg { value: Value::Number(0.0), from_ref: false }),
Some(Expr::Missing) | None => xargs.push(Arg::val(Value::Number(0.0))),
Some(e) => {
let value = self.value(e);
xargs.push(Arg { value, from_ref: false });
let blank_value = value == Value::Empty;
xargs.push(Arg { value, from_ref: false, blank_value });
}
}
}
+3
View File
@@ -1842,6 +1842,9 @@ fn lookup_of_an_empty_cell_stays_blank() {
("D8", "=VLOOKUP(\"k\",A1:B2,2,0)", Value::Number(0.0)),
// Arithmetic still reads the blank as 0.
("D9", "=VLOOKUP(\"k\",A1:B2,2,0)+1", Value::Number(1.0)),
// A blank lookup given as an argument is still given, not omitted.
("D10", "=FILTER(1,FALSE,VLOOKUP(\"k\",A1:B2,2,FALSE))", Value::Number(0.0)),
("D11", "=XLOOKUP(\"z\",A1:A2,B1:B2,VLOOKUP(\"k\",A1:B2,2,0))", Value::Number(0.0)),
] {
t.set(at, f);
assert_eq!(t.get(at), want, "{f}");
+1 -1
View File
@@ -91,7 +91,7 @@ fn na(_a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
fn n_fn(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
// N(range) reads the range's first cell, but a computed array (N(A1:A3>0), N({...})) is
// converted element by element, so N can stand in for `--` inside SUMPRODUCT.
if let Some(Arg { value: Value::Array(arr), from_ref: false }) = a.first() {
if let Some(Arg { value: Value::Array(arr), from_ref: false, .. }) = a.first() {
let data = arr.data.iter().map(|v| n_of(v.clone()).unwrap_or_else(Value::Error)).collect();
return Array::new(arr.rows, arr.cols, data).map(Value::from).ok_or(CellError::Calc);
}
+5 -2
View File
@@ -87,16 +87,19 @@ pub use stat::{aggregate_values, aggregate_values_k};
pub struct Arg {
pub value: Value,
pub from_ref: bool,
/// A written argument whose value came out blank (a lookup of an empty cell): given, not
/// omitted, although its value is `Value::Empty`.
pub blank_value: bool,
}
impl Arg {
/// A literal (non-reference) argument.
pub fn val(value: impl Into<Value>) -> Arg {
Arg { value: value.into(), from_ref: false }
Arg { value: value.into(), from_ref: false, blank_value: false }
}
/// An argument that came from a cell or range reference.
pub fn reference(value: impl Into<Value>) -> Arg {
Arg { value: value.into(), from_ref: true }
Arg { value: value.into(), from_ref: true, blank_value: false }
}
}
+1 -6
View File
@@ -50,12 +50,7 @@ fn reject_multi_cell_refs(spec: &FnSpec, args: &[Arg]) -> Option<Vec<Arg>> {
if !args.iter().enumerate().any(|(i, a)| multi(i, a)) {
return None;
}
Some(
args.iter()
.enumerate()
.map(|(i, a)| if multi(i, a) { Arg { value: Value::Error(CellError::Value), from_ref: false } } else { a.clone() })
.collect(),
)
Some(args.iter().enumerate().map(|(i, a)| if multi(i, a) { Arg::val(Value::Error(CellError::Value)) } else { a.clone() }).collect())
}
/// Lookups whose every call searches a lookup vector from its start, so a call lifted over `n`
+1 -1
View File
@@ -51,7 +51,7 @@ pub(crate) fn arg(args: &[Arg], i: usize) -> R<&Arg> {
/// Whether argument `i` was supplied (present and not an empty literal like `F(1,)`).
pub(crate) fn has(args: &[Arg], i: usize) -> bool {
matches!(args.get(i), Some(a) if !matches!(a.value, Value::Empty) || a.from_ref)
matches!(args.get(i), Some(a) if !matches!(a.value, Value::Empty) || a.from_ref || a.blank_value)
}
/// Number coercion for a scalar parameter: empty = 0, TRUE = 1, numeric text parses.