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),
|
"OFFSET" => self.offset(args),
|
||||||
"INDIRECT" => self.indirect(args),
|
"INDIRECT" => self.indirect(args),
|
||||||
"INDEX" => self.index(args),
|
"INDEX" => self.index(args),
|
||||||
|
"XLOOKUP" => self.xlookup(args),
|
||||||
"CELL" => self.cell_info(args),
|
"CELL" => self.cell_info(args),
|
||||||
"TRIMRANGE" => self.trimrange(args),
|
"TRIMRANGE" => self.trimrange(args),
|
||||||
"SUBTOTAL" | "AGGREGATE" => self.subtotal(name, 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 {
|
fn cell_info(&mut self, args: &[Expr]) -> Ev {
|
||||||
let kind = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) {
|
let kind = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) {
|
||||||
Some(Ok(t)) => t.to_ascii_lowercase(),
|
Some(Ok(t)) => t.to_ascii_lowercase(),
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,39 @@ fn chains_out_of_order() {
|
||||||
assert_eq!(t.num("A5"), 5.0);
|
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]
|
#[test]
|
||||||
fn long_chain() {
|
fn long_chain() {
|
||||||
let mut t = T::new();
|
let mut t = T::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue