diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index e5f3412..2f1bb08 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -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> { + /// 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 { + 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> { + if let Some(f) = e.and_then(|x| self.function_by_name(x)) { + let params: Vec = (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> = 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); } diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 6606786..2914734 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -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 { diff --git a/crates/formula/src/parser.rs b/crates/formula/src/parser.rs index 8aae981..1e3d96b 100644 --- a/crates/formula/src/parser.rs +++ b/crates/formula/src/parser.rs @@ -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())) } diff --git a/crates/formula/src/tests.rs b/crates/formula/src/tests.rs index fef3da3..6ae8039 100644 --- a/crates/formula/src/tests.rs +++ b/crates/formula/src/tests.rs @@ -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");