Merge pull request #486 from nihilist-narwhal:fix-function-name-arguments

This commit is contained in:
Brandon Thomas 2026-10-13 00:07:16 -04:00
commit eb5f366786
4 changed files with 61 additions and 5 deletions

View File

@ -750,6 +750,20 @@ impl<'h> Evaluator<'h> {
Ev::V(Value::Bool(r))
}
"MAP" | "REDUCE" | "SCAN" | "BYROW" | "BYCOL" | "MAKEARRAY" => self.higher_order(name, args),
"GROUPBY" | "PIVOTBY" => {
// The aggregation may be a function given by name (`SUM`): pass it as its name.
let i = if name == "GROUPBY" { 2 } else { 3 };
match args.get(i).and_then(|a| self.function_by_name(a)) {
Some(f) => {
let mut args = args.to_vec();
if let Some(a) = args.get_mut(i) {
*a = Expr::Text(f.into());
}
self.builtin(name, &args)
}
None => self.builtin(name, args),
}
}
"NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" => {
self.host.note_volatile();
self.builtin(name, args)
@ -921,7 +935,26 @@ impl<'h> Evaluator<'h> {
r
}
fn lambda_arg(&mut self, e: Option<&Expr>) -> Option<Arc<Lambda>> {
/// A worksheet function given by name where a function is expected (`BYROW(A1:C3,SUM)`,
/// `GROUPBY(A2:A9,B2:B9,SUM)`): its name, unless a LET/LAMBDA variable or a defined name has
/// that name.
fn function_by_name(&self, e: &Expr) -> Option<String> {
let Expr::Name(n) = e else { return None };
if self.env.iter().any(|(k, _)| k.eq_ignore_ascii_case(n)) || self.host.workbook().name(n, self.sheet).is_some() {
return None;
}
let u = n.to_ascii_uppercase();
gridcraft_functions::lookup(&u).map(|_| u)
}
/// The function argument of a LAMBDA helper; a function given by name takes `arity`
/// arguments.
fn lambda_arg(&mut self, e: Option<&Expr>, arity: usize) -> Option<Arc<Lambda>> {
if let Some(f) = e.and_then(|x| self.function_by_name(x)) {
let params: Vec<String> = (0..arity).map(|i| format!("_xleta.{i}")).collect();
let body = Expr::Call(f, params.iter().map(|p| Expr::Name(p.clone())).collect());
return Some(Arc::new(Lambda { params, body, env: Vec::new() }));
}
match e.map(|x| self.eval(x)) {
Some(Ev::L(l)) => Some(l),
_ => None,
@ -940,7 +973,7 @@ impl<'h> Evaluator<'h> {
if args.len() < 2 {
return err(CellError::Value);
}
let Some(l) = self.lambda_arg(args.last()) else { return err(CellError::Value) };
let Some(l) = self.lambda_arg(args.last(), args.len() - 1) else { return err(CellError::Value) };
let arrays: Vec<Arc<Array>> = args[..args.len() - 1].iter().map(|a| as_array(self.value(a))).collect();
let rows = arrays.iter().map(|a| a.rows).max().unwrap_or(1);
let cols = arrays.iter().map(|a| a.cols).max().unwrap_or(1);
@ -963,7 +996,7 @@ impl<'h> Evaluator<'h> {
}
let mut acc = self.value(&args[0]);
let arr = as_array(self.value(&args[1]));
let Some(l) = self.lambda_arg(args.get(2)) else { return err(CellError::Value) };
let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) };
if l.params.len() != 2 {
return err(CellError::Value);
}
@ -987,7 +1020,7 @@ impl<'h> Evaluator<'h> {
return err(CellError::Value);
}
let arr = as_array(self.value(&args[0]));
let Some(l) = self.lambda_arg(args.get(1)) else { return err(CellError::Value) };
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) };
let by_row = name == "BYROW";
let n = if by_row { arr.rows } else { arr.cols };
@ -1016,7 +1049,7 @@ impl<'h> Evaluator<'h> {
if rows < 1.0 || cols < 1.0 || rows * cols > MAX_CELLS as f64 {
return err(CellError::Value);
}
let Some(l) = self.lambda_arg(args.get(2)) else { return err(CellError::Value) };
let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) };
if l.params.len() != 2 {
return err(CellError::Value);
}

View File

@ -553,6 +553,24 @@ fn let_lambda() {
}
#[test]
fn functions_given_by_name() {
let mut t = T::new();
for (i, (k, v)) in [("a", 1), ("b", -2), ("a", 3)].iter().enumerate() {
t.set(&format!("A{}", i + 1), k);
t.set(&format!("B{}", i + 1), &v.to_string());
}
t.set("D1", "=BYROW(B1:B3,SUM)");
assert_eq!(t.num("D2"), -2.0);
t.set("E1", "=MAP(B1:B3,ABS)");
assert_eq!(t.num("E2"), 2.0);
t.set("F1", "=REDUCE(0,B1:B3,SUM)");
assert_eq!(t.num("F1"), 2.0);
t.set("G1", "=GROUPBY(A1:A3,B1:B3,SUM,0,0)");
assert_eq!(t.get("G1"), Value::from("a"));
assert_eq!(t.num("H1"), 4.0);
// A LET variable of that name is still the variable.
t.set("J1", "=LET(ABS,LAMBDA(x,x*10),SUM(MAP(B1:B3,ABS)))");
assert_eq!(t.num("J1"), 20.0);
fn lambda_argument_count() {
let mut t = T::new();
t.wb.names.push(gridcraft_model::DefinedName {

View File

@ -362,6 +362,10 @@ impl Parser {
if w.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return Err(self.err(format!("unexpected `{w}`")));
}
// A function passed by name is saved as `_xleta.SUM`.
if let Some(f) = w.get(..7).filter(|p| p.eq_ignore_ascii_case("_xleta.")).and_then(|_| w.get(7..)) {
return Ok(Expr::Name(f.to_ascii_uppercase()));
}
Ok(Expr::Name(w.to_string()))
}

View File

@ -64,6 +64,7 @@ fn normalises() {
assert_eq!(rt("=sum(a1:b2)"), "SUM(A1:B2)");
assert_eq!(rt("= 1 + 2"), "1+2");
assert_eq!(rt("_xlfn.XLOOKUP(1,A:A,B:B)"), "XLOOKUP(1,A:A,B:B)");
assert_eq!(rt("_xlfn.GROUPBY(A1:A3,B1:B3,_xleta.SUM)"), "GROUPBY(A1:A3,B1:B3,SUM)");
assert_eq!(rt("_XLUDF.concat(\"a\",\"b\")"), "_xludf.CONCAT(\"a\",\"b\")");
assert_eq!(rt("true"), "TRUE");
assert_eq!(rt("'Sheet1'!A1"), "Sheet1!A1");