Merge pull request #396 from nihilist-narwhal/fix-xlookup-reference
XLOOKUP: return a reference when the result range is one
This commit is contained in:
commit
baaa606036
|
|
@ -688,6 +688,7 @@ impl<'h> Evaluator<'h> {
|
|||
"OFFSET" => self.offset(args),
|
||||
"INDIRECT" => self.indirect(args),
|
||||
"INDEX" => self.index(args),
|
||||
"XLOOKUP" => self.xlookup(args),
|
||||
"CELL" => self.cell_info(args),
|
||||
"TRIMRANGE" => self.trimrange(args),
|
||||
"SUBTOTAL" | "AGGREGATE" => self.subtotal(name, args),
|
||||
|
|
@ -1183,6 +1184,66 @@ impl<'h> Evaluator<'h> {
|
|||
}
|
||||
}
|
||||
|
||||
/// XLOOKUP whose result range is a reference returns a reference, as in Excel, so that it can
|
||||
/// end a range (`SUM(XLOOKUP(a,…):XLOOKUP(b,…))`) or feed CELL. Everything else (arrays of
|
||||
/// lookup values, array arguments, errors) goes to the library function.
|
||||
fn xlookup(&mut self, args: &[Expr]) -> Ev {
|
||||
if (3..=6).contains(&args.len())
|
||||
&& let (Ev::R(look), Ev::R(ret)) = (self.eval(&args[1]), self.eval(&args[2]))
|
||||
&& let ([l], [r]) = (look.as_slice(), ret.as_slice())
|
||||
&& let Some(spec) = gridcraft_functions::lookup("XMATCH")
|
||||
{
|
||||
let key = self.value(&args[0]);
|
||||
let (l, r) = (*l, *r);
|
||||
let vertical = l.range.width() == 1 && l.range.height() > 1;
|
||||
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 }];
|
||||
// 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) {
|
||||
5
|
||||
} else if given(4) {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
};
|
||||
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(e) => {
|
||||
let value = self.value(e);
|
||||
xargs.push(Arg { value, from_ref: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ctx = FnCtx { ev: self };
|
||||
return match gridcraft_functions::call(spec, &xargs, &mut ctx) {
|
||||
Value::Number(p) if p >= 1.0 => {
|
||||
let k = p as u32 - 1;
|
||||
let range = if vertical {
|
||||
RangeRef::new(
|
||||
CellRef::new(r.range.start.row + k, r.range.start.col),
|
||||
CellRef::new(r.range.start.row + k, r.range.end.col),
|
||||
)
|
||||
} else {
|
||||
RangeRef::new(
|
||||
CellRef::new(r.range.start.row, r.range.start.col + k),
|
||||
CellRef::new(r.range.end.row, r.range.start.col + k),
|
||||
)
|
||||
};
|
||||
Ev::R(vec![Area { sheet: r.sheet, range }])
|
||||
}
|
||||
Value::Error(CellError::NA) if args.get(3).is_some_and(|e| !matches!(e, Expr::Missing)) => self.eval(&args[3]),
|
||||
Value::Error(e) => err(e),
|
||||
_ => err(CellError::Value),
|
||||
};
|
||||
}
|
||||
}
|
||||
self.builtin("XLOOKUP", args)
|
||||
}
|
||||
|
||||
fn cell_info(&mut self, args: &[Expr]) -> Ev {
|
||||
let kind = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) {
|
||||
Some(Ok(t)) => t.to_ascii_lowercase(),
|
||||
|
|
|
|||
|
|
@ -94,6 +94,39 @@ fn chains_out_of_order() {
|
|||
assert_eq!(t.num("A5"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xlookup_returns_a_reference() {
|
||||
let mut t = T::new();
|
||||
for (i, (k, v)) in [("a", 1), ("b", 2), ("c", 3), ("d", 4)].iter().enumerate() {
|
||||
t.set(&format!("A{}", i + 1), k);
|
||||
t.set(&format!("B{}", i + 1), &v.to_string());
|
||||
}
|
||||
t.set("D1", "=SUM(XLOOKUP(\"b\",A1:A4,B1:B4):XLOOKUP(\"d\",A1:A4,B1:B4))");
|
||||
assert_eq!(t.num("D1"), 9.0);
|
||||
t.set("D2", "=SUM(B1:XLOOKUP(\"c\",A1:A4,B1:B4))");
|
||||
assert_eq!(t.num("D2"), 6.0);
|
||||
t.set("D3", "=CELL(\"address\",XLOOKUP(\"c\",A1:A4,B1:B4))");
|
||||
assert_eq!(t.get("D3"), Value::from("$B$3"));
|
||||
// Not found: if_not_found, else #N/A, as before.
|
||||
t.set("D4", "=XLOOKUP(\"z\",A1:A4,B1:B4,\"none\")");
|
||||
assert_eq!(t.get("D4"), Value::from("none"));
|
||||
t.set("D5", "=XLOOKUP(\"z\",A1:A4,B1:B4)");
|
||||
assert_eq!(t.get("D5"), Value::Error(CellError::NA));
|
||||
// A match on an empty cell is a blank reference: ISBLANK is TRUE and & adds nothing (#374).
|
||||
t.set("A5", "e");
|
||||
t.set("D6", "=ISBLANK(XLOOKUP(\"e\",A1:A5,B1:B5))");
|
||||
assert_eq!(t.get("D6"), Value::Bool(true));
|
||||
t.set("D7", "=\"x\"&XLOOKUP(\"e\",A1:A5,B1:B5)&\"y\"");
|
||||
assert_eq!(t.get("D7"), Value::from("xy"));
|
||||
// An empty match_mode keeps the search_mode after it: search last to first.
|
||||
t.set("A6", "b");
|
||||
t.set("B6", "20");
|
||||
t.set("D8", "=XLOOKUP(\"b\",A1:A6,B1:B6,,,-1)");
|
||||
assert_eq!(t.num("D8"), 20.0);
|
||||
t.set("D9", "=XLOOKUP(\"b\",A1:A6,B1:B6)");
|
||||
assert_eq!(t.num("D9"), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_chain() {
|
||||
let mut t = T::new();
|
||||
|
|
|
|||
Loading…
Reference in New Issue