Functions: VLOOKUP/HLOOKUP/LOOKUP/XLOOKUP keep an empty target blank (#374)

A looked-up empty cell was turned into the number 0 inside the function,
so ISBLANK saw FALSE and & / CONCATENATE inserted "0". The value now stays
blank, like INDEX; a bare lookup still displays 0 as a formula's final result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-12 19:17:48 +02:00
co-authored by Claude Opus 5.5
parent fc45b7f6d8
commit 691883f99c
2 changed files with 33 additions and 12 deletions
+23
View File
@@ -968,3 +968,26 @@ fn trim_references() {
t.set("K20", "=SUM(Q1:.Q9)");
assert_eq!(t.get("K20"), Value::Error(CellError::Calc));
}
#[test]
fn lookup_of_an_empty_cell_stays_blank() {
// #374: B1 (the value for key "k") is empty.
let mut t = T::new();
t.set("A1", "k");
t.set("A2", "j");
t.set("B2", "v");
for (at, f, want) in [
("D1", "=ISBLANK(VLOOKUP(\"k\",A1:B2,2,0))", Value::Bool(true)),
("D2", "=\"x\"&VLOOKUP(\"k\",A1:B2,2,0)&\"y\"", Value::from("xy")),
("D3", "=CONCATENATE(\"x\",VLOOKUP(\"k\",A1:B2,2,0),\"y\")", Value::from("xy")),
("D4", "=ISBLANK(XLOOKUP(\"k\",A1:A2,B1:B2))", Value::Bool(true)),
("D5", "=\"x\"&XLOOKUP(\"k\",A1:A2,B1:B2)&\"y\"", Value::from("xy")),
("D6", "=ISBLANK(LOOKUP(\"k\",A1:A1,B1:B1))", Value::Bool(true)),
("D7", "=ISBLANK(HLOOKUP(\"j\",A2:B3,2,0))", Value::Bool(true)),
// A bare lookup still shows 0, like a reference to an empty cell.
("D8", "=VLOOKUP(\"k\",A1:B2,2,0)", Value::Number(0.0)),
] {
t.set(at, f);
assert_eq!(t.get(at), want, "{f}");
}
}
+10 -12
View File
@@ -119,11 +119,6 @@ fn row(a: &Array, r: usize) -> Vec<Value> {
(0..a.cols).map(|c| a.get(r, c).cloned().unwrap_or(Value::Empty)).collect()
}
/// A looked-up cell value: blanks read as 0 like a reference to an empty cell.
fn cell_result(v: Value) -> Value {
if matches!(v, Value::Empty) { Value::Number(0.0) } else { v }
}
// ---------------------------------------------------------------------------------------------
// VLOOKUP / HLOOKUP / LOOKUP / MATCH
@@ -146,7 +141,9 @@ fn vh_lookup(args: &[Arg], vertical: bool) -> R<Value> {
let pos = if approx { bsearch_last(&x, &keys, false) } else { find_exact(&x, &keys, true, false) };
let pos = pos.ok_or(CellError::NA)?;
let v = if vertical { table.get(pos, idx) } else { table.get(idx, pos) };
Ok(cell_result(v.cloned().unwrap_or(Value::Empty)))
// An empty target stays blank, as in Excel: ISBLANK sees it as blank and `&` /
// CONCATENATE treat it as ""; only a formula's final result displays 0 (#374).
Ok(v.cloned().unwrap_or(Value::Empty))
}
fn vlookup(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
@@ -179,18 +176,18 @@ fn lookup(args: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
} else {
None
};
return v.map(cell_result).ok_or(CellError::NA);
return v.ok_or(CellError::NA);
}
// Array form: search the first row of a wide array, else the first column; return the
// matching entry from the last row / column.
if la.cols > la.rows {
let keys = row(&la, 0);
let pos = bsearch_last(&x, &keys, false).ok_or(CellError::NA)?;
Ok(cell_result(la.get(la.rows - 1, pos).cloned().unwrap_or(Value::Empty)))
Ok(la.get(la.rows - 1, pos).cloned().unwrap_or(Value::Empty))
} else {
let keys = column(&la, 0);
let pos = bsearch_last(&x, &keys, false).ok_or(CellError::NA)?;
Ok(cell_result(la.get(pos, la.cols - 1).cloned().unwrap_or(Value::Empty)))
Ok(la.get(pos, la.cols - 1).cloned().unwrap_or(Value::Empty))
}
}
@@ -311,13 +308,13 @@ fn xlookup(args: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
if vertical {
let r = row(&ra, p);
if r.len() == 1 {
return Ok(cell_result(r.into_iter().next().unwrap_or(Value::Empty)));
return Ok(r.into_iter().next().unwrap_or(Value::Empty));
}
array_val(1, r.len(), r)
} else {
let c = column(&ra, p);
if c.len() == 1 {
return Ok(cell_result(c.into_iter().next().unwrap_or(Value::Empty)));
return Ok(c.into_iter().next().unwrap_or(Value::Empty));
}
array_val(c.len(), 1, c)
}
@@ -581,7 +578,8 @@ mod tests {
is_err(ev("VLOOKUP", vec![n(4.0), rf(table()), n(2.0), b(false)]), CellError::NA);
is_err(ev("VLOOKUP", vec![n(4.0), rf(table()), n(2.0), empty()]), CellError::NA);
close(ev("VLOOKUP", vec![n(3.0), rf(table()), n(3.0), b(false)]), 30.0);
close(ev("VLOOKUP", vec![n(5.0), rf(table()), n(3.0), b(false)]), 0.0);
// An empty target stays blank (the cell displays 0 only as a final result) (#374).
assert_eq!(ev("VLOOKUP", vec![n(5.0), rf(table()), n(3.0), b(false)]), Value::Empty);
is_err(ev("VLOOKUP", vec![n(3.0), rf(table()), n(4.0)]), CellError::Ref);
is_err(ev("VLOOKUP", vec![n(3.0), rf(table()), n(0.0)]), CellError::Value);
close(ev("VLOOKUP", vec![t("b*"), rf(names()), n(2.0), b(false)]), 2.0);