diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index 4bdb3a30d..45d81ccca 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -11,6 +11,18 @@ use gridcraft_formula::{BinOp, Expr, RefKind, Reference, SheetSel, StructItem, S use gridcraft_functions::{Arg, Ctx}; use gridcraft_model::Workbook; +mod array; +mod call; +mod format; +mod lambda; +mod reference_fns; +mod special; + +use array::map_unary; +pub use array::{binary_values, zip2}; +pub use format::r1c1_to_a1; +pub use special::{SPECIAL_FUNCTIONS, is_known_function, precedents, resized_sum_range}; + /// Largest array a reference may expand to. pub const MAX_CELLS: u64 = 16_000_000; /// References up to this many cells (one whole row) are always read at their full size. @@ -615,1021 +627,6 @@ impl<'h> Evaluator<'h> { } } } - - fn call(&mut self, name: &str, args: &[Expr]) -> Ev { - match name { - "IF" => { - if args.is_empty() || args.len() > 3 { - return err(CellError::Value); - } - let c = self.arg_val(args, 0).unwrap_or_default(); - if let Value::Array(arr) = &c { - // Array condition: element-wise. - let t = self.arg_val(args, 1).unwrap_or(Value::Bool(true)); - let f = if args.len() > 2 { self.arg_val(args, 2).unwrap_or(Value::Bool(false)) } else { Value::Bool(false) }; - return Ev::V(zip3(arr, &t, &f, |c, t, f| match c.to_bool() { - Ok(true) => t.clone(), - Ok(false) => f.clone(), - Err(e) => Value::Error(e), - })); - } - match c.to_bool() { - Ok(true) => match args.get(1) { - Some(Expr::Missing) => Ev::V(Value::Number(0.0)), - Some(e) => self.eval(e), - None => Ev::V(Value::Bool(true)), - }, - Ok(false) => match args.get(2) { - Some(Expr::Missing) => Ev::V(Value::Number(0.0)), - Some(e) => self.eval(e), - None => Ev::V(Value::Bool(false)), - }, - Err(e) => err(e), - } - } - "IFS" => { - if args.len() < 2 || !args.len().is_multiple_of(2) { - return err(CellError::Value); - } - for pair in args.chunks(2) { - let [c, v] = pair else { break }; - let cv = self.value(c); - match cv.scalar().to_bool() { - Ok(true) => return self.eval(v), - Ok(false) => {} - Err(e) => return err(e), - } - } - err(CellError::NA) - } - "IFERROR" | "IFNA" => { - if args.len() != 2 { - return err(CellError::Value); - } - let v = self.arg_ev(args, 0).unwrap_or(Ev::V(Value::Empty)); - let na_only = name == "IFNA"; - let catch = |e: CellError| if na_only { e == CellError::NA } else { true }; - match &v { - Ev::V(Value::Error(e)) if catch(*e) => self.eval(&args[1]), - Ev::V(Value::Array(a)) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => { - let alt = self.arg_val(args, 1).unwrap_or_default(); - let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect(); - Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value))) - } - Ev::R(_) => { - let val = self.deref(v.clone()); - match val { - Value::Error(e) if catch(e) => self.eval(&args[1]), - Value::Array(ref a) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => { - let alt = self.arg_val(args, 1).unwrap_or_default(); - let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect(); - Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value))) - } - _ => v, - } - } - _ => v, - } - } - "CHOOSE" => { - if args.len() < 2 { - return err(CellError::Value); - } - let index = self.value(&args[0]); - if let Value::Array(_) = index { - // An array of indexes picks element by element: `CHOOSE({1,2},A1:A3,B1:B3)` - // is a two-column array. The library CHOOSE lifts over its arguments. - let Some(spec) = gridcraft_functions::lookup(name) else { return err(CellError::Name) }; - let mut evaluated = vec![Arg { value: index, from_ref: false }]; - for a in &args[1..] { - let value = self.value(a); - evaluated.push(Arg { value, from_ref: false }); - } - let mut ctx = FnCtx { ev: self, criteria: None, lambda: None }; - return gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V); - } - let i = match index.to_number() { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - if i < 1 || i as usize >= args.len() { - return err(CellError::Value); - } - self.eval(&args[i as usize]) - } - "SWITCH" => { - if args.len() < 3 { - return err(CellError::Value); - } - let v = self.arg_val(args, 0).unwrap_or_default().scalar(); - if let Value::Error(e) = v { - return err(e); - } - let rest = &args[1..]; - let mut i = 0; - while i + 1 < rest.len() { - let cand = self.value(&rest[i]).scalar(); - if !cand.is_error() && compare(&v, &cand) == std::cmp::Ordering::Equal && same_kind(&v, &cand) { - return self.eval(&rest[i + 1]); - } - i += 2; - } - if rest.len() % 2 == 1 { rest.last().map_or(err(CellError::NA), |d| self.eval(d)) } else { err(CellError::NA) } - } - "ROW" | "COLUMN" => { - let is_row = name == "ROW"; - if args.is_empty() || matches!(args.first(), Some(Expr::Missing)) { - return Ev::V(Value::number(if is_row { self.at.row + 1 } else { self.at.col + 1 } as f64)); - } - match self.eval(&args[0]) { - Ev::R(a) => { - let Some(a) = a.first() else { return err(CellError::Ref) }; - let r = self.trim_lines(a); - if is_row { - let rows: Vec = (r.start.row..=r.end.row).map(|x| Value::number(x as f64 + 1.0)).collect(); - if rows.len() == 1 { Ev::V(rows.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::column(rows).into()) } - } else { - let cols: Vec = (r.start.col..=r.end.col).map(|x| Value::number(x as f64 + 1.0)).collect(); - if cols.len() == 1 { Ev::V(cols.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::row(cols).into()) } - } - } - Ev::V(Value::Error(e)) => err(e), - _ => err(CellError::Value), - } - } - "ROWS" | "COLUMNS" => { - if args.len() != 1 { - return err(CellError::Value); - } - let is_rows = name == "ROWS"; - match self.eval(&args[0]) { - Ev::R(a) => match a.as_slice() { - [a] => Ev::V(Value::number(if is_rows { a.range.height() } else { a.range.width() } as f64)), - _ => err(CellError::Ref), - }, - Ev::V(Value::Array(arr)) => Ev::V(Value::number(if is_rows { arr.rows } else { arr.cols } as f64)), - Ev::V(Value::Error(e)) => err(e), - Ev::V(_) => Ev::V(Value::Number(1.0)), - Ev::L(_) => err(CellError::Value), - } - } - "AREAS" => match args.first().map(|a| self.eval(a)) { - Some(Ev::R(a)) => Ev::V(Value::number(a.len() as f64)), - Some(Ev::V(Value::Error(e))) => err(e), - _ => err(CellError::Value), - }, - "ISREF" => Ev::V(Value::Bool(matches!(args.first().map(|a| self.eval(a)), Some(Ev::R(_))))), - "ISFORMULA" | "FORMULATEXT" => { - let Some(Ev::R(a)) = args.first().map(|a| self.eval(a)) else { return err(CellError::NA) }; - let Some(a) = a.first() else { return err(CellError::NA) }; - let f = self.host.workbook().sheet(a.sheet).and_then(|s| s.cell(a.range.start)).and_then(|c| c.formula.clone()); - if name == "ISFORMULA" { - Ev::V(Value::Bool(f.is_some())) - } else { - match f { - Some(f) => Ev::V(Value::text(format!("={}", f.text))), - None => err(CellError::NA), - } - } - } - "SHEET" | "SHEETS" => { - let wb = self.host.workbook(); - if args.is_empty() { - return Ev::V(Value::number(if name == "SHEET" { self.sheet + 1 } else { wb.sheets.len() } as f64)); - } - match self.eval(&args[0]) { - Ev::R(a) => { - if name == "SHEET" { - a.first().map_or(err(CellError::Ref), |a| Ev::V(Value::number(a.sheet as f64 + 1.0))) - } else { - let mut s: Vec = a.iter().map(|x| x.sheet).collect(); - s.dedup(); - Ev::V(Value::number(s.len() as f64)) - } - } - Ev::V(Value::Text(t)) if name == "SHEET" => { - let wb = self.host.workbook(); - wb.sheet_index(&t).map_or(err(CellError::NA), |i| Ev::V(Value::number(i as f64 + 1.0))) - } - _ => err(CellError::NA), - } - } - "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), - "TEXT" => { - if args.len() != 2 { - return err(CellError::Value); - } - let v = self.arg_val(args, 0).unwrap_or_default(); - let f = self.arg_val(args, 1).unwrap_or_default(); - let sys = self.host.workbook().date_system; - let one = |v: &Value, f: &Value| -> Value { - if let Value::Error(e) = v { - return Value::Error(*e); - } - let code = match f.to_text() { - Ok(c) => c, - Err(e) => return Value::Error(e), - }; - match gridcraft_numfmt::text_function(v, &code, sys) { - Ok(s) => Value::text(s), - Err(e) => Value::Error(e), - } - }; - Ev::V(zip2(&v, &f, one)) - } - "LET" => self.let_(args), - "LAMBDA" => { - if args.is_empty() { - return err(CellError::Value); - } - let mut params = Vec::new(); - for p in &args[..args.len() - 1] { - match p { - Expr::Name(n) => params.push(n.trim_start_matches('[').trim_end_matches(']').to_string()), - _ => return err(CellError::Value), - } - } - let Some(body) = args.last() else { return err(CellError::Value) }; - Ev::L(Arc::new(Lambda { params, body: body.clone(), env: self.env.clone() })) - } - "ISOMITTED" => { - let r = matches!(args.first(), Some(Expr::Name(n)) if self.env.iter().rev().find(|(k, _)| gridcraft_formula::same_name(k, n)).is_some_and(|(_, v)| matches!(v, Ev::V(Value::Empty)))); - 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) - } - _ => self.builtin(name, args), - } - } - - fn builtin(&mut self, name: &str, args: &[Expr]) -> Ev { - let Some(spec) = gridcraft_functions::lookup(name) else { - // A defined name holding a LAMBDA, or a LET-bound lambda. - return match self.name(name) { - Ev::L(l) => self.apply_lambda(&l, args), - Ev::V(Value::Error(x)) => err(x), - _ => err(CellError::Name), - }; - }; - if args.len() < spec.min_args || args.len() > spec.max_args { - return err(CellError::Value); - } - if let Some(kind) = Agg::of(spec.name) { - return self.aggregate(kind, args); - } - let mut evaluated = Vec::with_capacity(args.len()); - // SUMIF/AVERAGEIF: the size of the criteria range (argument 1), which the sum range takes. - let mut criteria_shape = None; - // Database functions: the criteria range (argument 2), whose formulas are computed criteria. - let mut criteria = None; - // GROUPBY/PIVOTBY: the LAMBDA given as their function. - let mut lambda = None; - let lambda_at = match name { - "GROUPBY" => Some(2), - "PIVOTBY" => Some(3), - _ => None, - }; - for (i, a) in args.iter().enumerate() { - let mut ev = self.eval(a); - if lambda_at == Some(i) - && let Ev::L(l) = &ev - { - lambda = Some(l.clone()); - ev = Ev::V(Value::Empty); - } - if spec.category == gridcraft_functions::Category::Database - && i == 2 - && let Ev::R(areas) = &ev - && let [area] = areas.as_slice() - { - criteria = Some(*area); - } - if resizes_sum_range(name, args.len()) { - if i == 0 { - criteria_shape = ev_shape(&ev); - } else if i == 2 - && let (Some((h, w)), Ev::R(areas)) = (criteria_shape, &ev) - && let [area] = areas.as_slice() - { - ev = Ev::R(vec![Area { sheet: area.sheet, range: resized_sum_range(area.range, h, w) }]); - } - } - let from_ref = matches!(ev, Ev::R(_)); - let value = match ev { - Ev::R(areas) if areas.len() > 1 => { - // Multi-area references (unions): flatten into one row for aggregates. - let mut vals = Vec::new(); - for ar in &areas { - match self.area_value(ar) { - Value::Array(a) => vals.extend(a.data.iter().cloned()), - v => vals.push(v), - } - } - Value::from(Array::row(vals)) - } - ev => self.deref(ev), - }; - evaluated.push(Arg { value, from_ref }); - } - let mut ctx = FnCtx { ev: self, criteria, lambda }; - gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V) - } - - /// Performs aggregation operations without copying its ranges: blocks are folded over the stored - /// cells, so no array is built and there is no `MAX_CELLS` limit. Arguments are evaluated and read one - /// after another, as the array path does, so volatile formulas (like `RAND`) see the same order. - /// Only `SUM`, `COUNT` and `AVG` are supported currently. - fn aggregate(&mut self, kind: Agg, args: &[Expr]) -> Ev { - let mut acc = Acc::default(); - let mut first_error = None; - for a in args { - let ev = self.eval(a); - let steps = match ev { - Ev::R(areas) => areas.iter().map(|area| self.aggregate_area(kind, area, &mut acc)).collect(), - ev => { - let value = self.deref(ev); - vec![self.aggregate_value(kind, value, false, &mut acc)] - } - }; - if first_error.is_none() { - first_error = steps.into_iter().find_map(Result::err); - } - } - first_error.unwrap_or_else(|| Ev::V(acc.finish(kind))) - } - - /// Adds one area to `acc`; `Err` is the result the function ends with. - fn aggregate_area(&mut self, kind: Agg, a: &Area, acc: &mut Acc) -> Result<(), Ev> { - if !a.range.is_single() { - let mut error = None; - let walked = self.host.visit_range(a.sheet, a.range, &mut |v| match acc.add_cell(kind, v) { - Ok(()) => true, - Err(e) => { - error = Some(e); - false - } - }); - if walked { - return error.map_or(Ok(()), |e| Err(err(e))); - } - } - let value = self.area_value(a); - self.aggregate_value(kind, value, true, acc) - } - - /// Adds one argument's value by calling `SUM` and `COUNT` on it, so coercion and error rules - /// stay in one place (`AVERAGE` of an argument is its `SUM` over its `COUNT`). - fn aggregate_value(&mut self, kind: Agg, value: Value, from_ref: bool, acc: &mut Acc) -> Result<(), Ev> { - let (Some(sum), Some(count)) = (gridcraft_functions::lookup("SUM"), gridcraft_functions::lookup("COUNT")) else { - return Err(err(CellError::Name)); - }; - let args = [Arg { value, from_ref }]; - if kind != Agg::Count { - match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) { - Value::Number(n) => acc.sum += n, - other => return Err(Ev::V(other)), - } - } - if kind != Agg::Sum { - match gridcraft_functions::call(count, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) { - Value::Number(n) => acc.count = acc.count.saturating_add(n as u64), - other => return Err(Ev::V(other)), - } - } - Ok(()) - } - - pub fn apply_lambda(&mut self, l: &Lambda, args: &[Expr]) -> Ev { - // Too many or too few argument positions is #VALUE!. An explicitly empty position, as in - // `(1,)`, is `Expr::Missing` and still counts as a position (it binds as omitted). `f()` - // parses to no arguments, which for a one-parameter lambda can also be read as one empty - // position, so that call still binds the parameter as omitted. - let positions_ok = args.len() == l.params.len() || (args.is_empty() && l.params.len() == 1); - if !positions_ok { - return err(CellError::Value); - } - let mut bound = Vec::with_capacity(l.params.len()); - for (i, p) in l.params.iter().enumerate() { - let v = match args.get(i) { - Some(Expr::Missing) | None => Ev::V(Value::Empty), - Some(e) => self.eval(e), - }; - bound.push((p.clone(), v)); - } - self.call_lambda_values(l, bound) - } - - fn call_lambda_values(&mut self, l: &Lambda, bound: Vec<(String, Ev)>) -> Ev { - if self.calls >= MAX_LAMBDA_CALLS { - return err(CellError::Num); - } - // #2382: one charge per invocation, so a cell's total work stays bounded however the - // lambdas nest. Foregrounding the charge here means every path into a body (direct - // `(2,3)` application, `MAP`/`REDUCE`/`BYROW` helpers, D-function criteria) meters. - self.fuel = match self.fuel.checked_sub(1) { - Some(f) => f, - None => return err(CellError::Calc), - }; - self.calls += 1; - let saved = std::mem::replace(&mut self.env, l.env.clone()); - self.env.extend(bound); - let r = self.eval(&l.body); - self.env = saved; - self.calls -= 1; - r - } - - fn let_(&mut self, args: &[Expr]) -> Ev { - if args.len() < 3 || args.len().is_multiple_of(2) { - return err(CellError::Value); - } - let base = self.env.len(); - for pair in args[..args.len() - 1].chunks(2) { - let [Expr::Name(n), v] = pair else { - self.env.truncate(base); - return err(CellError::Value); - }; - let val = self.eval(v); - self.env.push((n.clone(), val)); - } - let r = match args.last() { - Some(body) => self.eval(body), - None => err(CellError::Value), - }; - self.env.truncate(base); - r - } - - /// 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, _)| gridcraft_formula::same_name(k, 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, - } - } - - fn higher_order(&mut self, name: &str, args: &[Expr]) -> Ev { - let as_array = |v: Value| -> Arc { - match v { - Value::Array(a) => a, - other => Arc::new(Array::scalar(other)), - } - }; - match name { - "MAP" => { - if args.len() < 2 { - 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); - let Some(n) = broadcast_len(rows, cols) else { return err(CellError::Num) }; - // The LAMBDA takes one parameter per array. - if l.params.len() != arrays.len() { - return err(CellError::Value); - } - let mut out = Vec::with_capacity(n); - for r in 0..rows { - for c in 0..cols { - let bound = l.params.iter().zip(arrays.iter()).map(|(p, a)| (p.clone(), Ev::V(a.get_broadcast(r, c)))).collect(); - let v = self.call_lambda_values(&l, bound); - let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; - out.push(one); - } - } - Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) - } - "REDUCE" | "SCAN" => { - if args.len() != 3 { - return err(CellError::Value); - } - let mut acc = self.value(&args[0]); - let arr = as_array(self.value(&args[1])); - let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) }; - if l.params.len() != 2 { - return err(CellError::Value); - } - let mut scan = Vec::with_capacity(arr.data.len()); - for v in arr.data.iter() { - let bound = vec![(l.params[0].clone(), Ev::V(acc.clone())), (l.params[1].clone(), Ev::V(v.clone()))]; - let r = self.call_lambda_values(&l, bound); - acc = self.deref(r); - if name == "SCAN" { - let Some(one) = single(acc.clone()) else { return err(CellError::Calc) }; - scan.push(one); - } - } - if name == "SCAN" { - Ev::V(Array::new(arr.rows, arr.cols, scan).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) - } else { - Ev::V(acc) - } - } - "BYROW" | "BYCOL" => { - if args.len() != 2 { - return err(CellError::Value); - } - let arr = as_array(self.value(&args[0])); - 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) }; - if l.params.len() != 1 { - return err(CellError::Value); - } - let by_row = name == "BYROW"; - let n = if by_row { arr.rows } else { arr.cols }; - let mut out = Vec::with_capacity(n); - for i in 0..n { - let slice: Vec = if by_row { - (0..arr.cols).map(|c| arr.get(i, c).cloned().unwrap_or_default()).collect() - } else { - (0..arr.rows).map(|r| arr.get(r, i).cloned().unwrap_or_default()).collect() - }; - let a = if by_row { Array::row(slice) } else { Array::column(slice) }; - let r = self.call_lambda_values(&l, vec![(p.clone(), Ev::V(a.into()))]); - let Some(one) = single(self.deref(r)) else { return err(CellError::Calc) }; - out.push(one); - } - let a = if by_row { Array::column(out) } else { Array::row(out) }; - Ev::V(a.into()) - } - "MAKEARRAY" => { - if args.len() != 3 { - return err(CellError::Value); - } - let (rows, cols) = match (self.arg_num(args, 0, 0.0), self.arg_num(args, 1, 0.0)) { - (Ok(r), Ok(c)) => (r.trunc(), c.trunc()), - (Err(e), _) | (_, Err(e)) => return err(e), - }; - 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), 2) else { return err(CellError::Value) }; - if l.params.len() != 2 { - return err(CellError::Value); - } - let (rows, cols) = (rows as usize, cols as usize); - let mut out = Vec::with_capacity(rows * cols); - for r in 0..rows { - for c in 0..cols { - let bound = vec![ - (l.params[0].clone(), Ev::V(Value::number(r as f64 + 1.0))), - (l.params[1].clone(), Ev::V(Value::number(c as f64 + 1.0))), - ]; - let v = self.call_lambda_values(&l, bound); - let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; - out.push(one); - } - } - Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) - } - _ => err(CellError::Name), - } - } - - /// Whole rows/columns trimmed for ROW()/COLUMN() arrays. - fn trim_lines(&mut self, a: &Area) -> RangeRef { - if a.range.is_full_cols() || a.range.is_full_rows() { self.trim(a) } else { a.range } - } - - fn offset(&mut self, args: &[Expr]) -> Ev { - self.host.note_volatile(); - if args.len() < 3 || args.len() > 5 { - return err(CellError::Value); - } - let base = match self.eval(&args[0]) { - Ev::R(a) if a.len() == 1 => a[0], - Ev::V(Value::Error(e)) => return err(e), - _ => return err(CellError::Value), - }; - let rows = match self.arg_num(args, 1, 0.0) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - let cols = match self.arg_num(args, 2, 0.0) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - let h = match self.arg_num(args, 3, base.range.height() as f64) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - let w = match self.arg_num(args, 4, base.range.width() as f64) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - if h == 0 || w == 0 { - return err(CellError::Ref); - } - let r0 = (base.range.start.row as i64).saturating_add(rows); - let c0 = (base.range.start.col as i64).saturating_add(cols); - let (r0, r1) = if h > 0 { (r0, r0.saturating_add(h - 1)) } else { (r0.saturating_add(h + 1), r0) }; - let (c0, c1) = if w > 0 { (c0, c0.saturating_add(w - 1)) } else { (c0.saturating_add(w + 1), c0) }; - if r0 < 0 || c0 < 0 || r1 >= MAX_ROWS as i64 || c1 >= MAX_COLS as i64 { - return err(CellError::Ref); - } - Ev::R(vec![Area { sheet: base.sheet, range: RangeRef::new(CellRef::new(r0 as u32, c0 as u32), CellRef::new(r1 as u32, c1 as u32)) }]) - } - - fn indirect(&mut self, args: &[Expr]) -> Ev { - self.host.note_volatile(); - if args.is_empty() || args.len() > 2 { - return err(CellError::Value); - } - let text = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) { - Some(Ok(t)) => t, - Some(Err(e)) => return err(e), - None => return err(CellError::Value), - }; - let a1 = match args.get(1) { - None | Some(Expr::Missing) => true, - Some(e) => { - let v = self.value(e); - v.scalar().to_bool().unwrap_or(true) - } - }; - // With a1 = FALSE, text that isn't R1C1 can still be a name, but never an A1 reference. - let (text, a1_ok) = if a1 { - (text, true) - } else { - match r1c1_to_a1(&text, self.at) { - Ok(Some(a1_text)) => (a1_text, true), - Ok(None) => (text, false), - Err(e) => return err(e), - } - }; - match gridcraft_formula::parse(&text) { - Ok(Expr::Ref(_)) if !a1_ok => err(CellError::Ref), - Ok(e @ (Expr::Ref(_) | Expr::Name(_) | Expr::Struct(_))) => match self.eval(&e) { - r @ Ev::R(_) => r, - _ => err(CellError::Ref), - }, - _ => err(CellError::Ref), - } - } - - fn index(&mut self, args: &[Expr]) -> Ev { - if args.is_empty() || args.len() > 4 { - return err(CellError::Value); - } - let src = self.eval(&args[0]); - let mut arg = |i: usize| match args.get(i) { - None | Some(Expr::Missing) => None, - Some(e) => Some(self.value(e)), - }; - let (rows, cols) = (arg(1), arg(2)); - let area_n = match self.arg_num(args, 3, 1.0) { - Ok(n) => n.trunc() as i64, - Err(e) => return err(e), - }; - let two = args.len() == 2; - let num = |v: &Option| v.as_ref().map_or(Ok(0.0), |v| v.to_number()).map(|n| n.trunc() as i64); - // Arrays of positions pick a value for each: INDEX(A1:C10,{1;2},{1,3}) is 2×2. - if let (Some(Value::Array(_)), _) | (_, Some(Value::Array(_))) = (&rows, &cols) { - let as_array = |v: &Option| match v { - Some(Value::Array(a)) => Arc::clone(a), - Some(v) => Arc::new(Array::scalar(v.clone())), - None => Arc::new(Array::scalar(Value::Number(0.0))), - }; - let (ra, ca) = (as_array(&rows), as_array(&cols)); - let (h, w) = (ra.rows.max(ca.rows), ra.cols.max(ca.cols)); - if (h as u64).saturating_mul(w as u64) > MAX_CELLS { - return err(CellError::Num); - } - let mut out = Vec::with_capacity(h * w); - for r in 0..h { - for c in 0..w { - let pos = (num(&Some(ra.get_broadcast(r, c))), num(&Some(ca.get_broadcast(r, c)))); - let v = match pos { - (Ok(row), Ok(col)) => { - let one = self.index_one(src.clone(), row, col, area_n, two); - match self.deref(one) { - Value::Array(a) if a.rows * a.cols > 1 => Value::Error(CellError::Calc), - v => v.scalar(), - } - } - (Err(e), _) | (_, Err(e)) => Value::Error(e), - }; - out.push(v); - } - } - return Ev::V(Array::new(h, w, out).map(Value::from).unwrap_or(Value::Error(CellError::Value))); - } - let (row, col) = match (num(&rows), num(&cols)) { - (Ok(r), Ok(c)) => (r, c), - (Err(e), _) | (_, Err(e)) => return err(e), - }; - self.index_one(src, row, col, area_n, two) - } - - /// INDEX for one row and column position (0 = the whole column or row). - fn index_one(&mut self, src: Ev, row: i64, col: i64, area_n: i64, two_args: bool) -> Ev { - if row < 0 || col < 0 { - return err(CellError::Value); - } - match src { - Ev::R(areas) => { - let area = usize::try_from((area_n as u64).saturating_sub(1)).ok(); - let Some(a) = area.and_then(|i| areas.get(i)).copied() else { return err(CellError::Ref) }; - let r = a.range; - // A single row or column with one index picks along it. An area with several rows - // and columns needs both: `INDEX(A1:C3,2)` is #REF! in Excel (an empty third - // argument, `INDEX(A1:C3,2,)`, still means the whole row). Arrays differ: - // `INDEX({1,2;3,4},2)` is the row. - if two_args && r.height() > 1 && r.width() > 1 { - return err(CellError::Ref); - } - let (row, col) = if two_args && r.height() == 1 { (1, row) } else { (row, col) }; - if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 { - return err(CellError::Ref); - } - let (r0, r1) = if row == 0 { (r.start.row, r.end.row) } else { (r.start.row + row as u32 - 1, r.start.row + row as u32 - 1) }; - let (c0, c1) = if col == 0 { (r.start.col, r.end.col) } else { (r.start.col + col as u32 - 1, r.start.col + col as u32 - 1) }; - Ev::R(vec![Area { sheet: a.sheet, range: RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1)) }]) - } - Ev::V(Value::Array(arr)) => { - let (row, col) = if two_args && arr.rows == 1 { (1, row) } else { (row, col) }; - if row as usize > arr.rows || col as usize > arr.cols { - return err(CellError::Ref); - } - match (row, col) { - (0, 0) => Ev::V(Value::Array(arr)), - (0, c) => Ev::V(Array::column((0..arr.rows).map(|r| arr.get(r, c as usize - 1).cloned().unwrap_or_default()).collect()).into()), - (r, 0) => Ev::V(Array::row((0..arr.cols).map(|c| arr.get(r as usize - 1, c).cloned().unwrap_or_default()).collect()).into()), - (r, c) => Ev::V(arr.get(r as usize - 1, c as usize - 1).cloned().unwrap_or(Value::Error(CellError::Ref))), - } - } - Ev::V(Value::Error(e)) => err(e), - Ev::V(v) => { - if row <= 1 && col <= 1 { - Ev::V(v) - } else { - err(CellError::Ref) - } - } - Ev::L(_) => err(CellError::Value), - } - } - - /// 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, criteria: None, lambda: None }; - 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(), - Some(Err(e)) => return err(e), - None => return err(CellError::Value), - }; - let area = match args.get(1).map(|a| self.eval(a)) { - Some(Ev::R(a)) => a.first().copied(), - None => Some(Area { sheet: self.sheet, range: RangeRef::cell(self.at) }), - _ => return err(CellError::Value), - }; - let Some(a) = area else { return err(CellError::Ref) }; - let c = a.range.start; - let wb = self.host.workbook(); - let sheet = wb.sheet(a.sheet); - match kind.as_str() { - "address" => Ev::V(Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1))), - "row" => Ev::V(Value::number(c.row as f64 + 1.0)), - "col" => Ev::V(Value::number(c.col as f64 + 1.0)), - "contents" => Ev::V(self.host.cell_value(a.sheet, c)), - "type" => { - let v = self.host.cell_value(a.sheet, c); - Ev::V(Value::text(match v { - Value::Empty => "b", - Value::Text(_) => "l", - _ => "v", - })) - } - "width" => Ev::V(Value::number(sheet.map_or(8.43, |s| gridcraft_model::points_to_chars(s.col_width(c.col) as f64)).round())), - // Excel's `/[]`, which the classic sheet-name formula splits on - // the `]`. Empty until the workbook has a file of its own, as in Excel (#1489). - "filename" => Ev::V(Value::text(match &wb.source_path { - Some(file) => { - let name = file.file_name().unwrap_or_default().to_string_lossy(); - let on = sheet.map_or("", |s| s.name.as_str()); - file.parent().unwrap_or(std::path::Path::new("")).join(format!("[{name}]{on}")).to_string_lossy().into_owned() - } - None => String::new(), - })), - "sheetname" => Ev::V(Value::text(sheet.map(|s| s.name.clone()).unwrap_or_default())), - "protect" => Ev::V(Value::number(if sheet.is_some_and(|s| wb.styles.get(s.style_id(c)).protection.locked) { 1.0 } else { 0.0 })), - "format" => { - let code = sheet.map(|s| wb.styles.get(s.style_id(c)).num_fmt.as_str().to_string()).unwrap_or_default(); - Ev::V(Value::text(cell_format_code(&code))) - } - "color" | "parentheses" => Ev::V(Value::Number(0.0)), - "prefix" => Ev::V(Value::text("")), - _ => err(CellError::Value), - } - } - - fn subtotal(&mut self, name: &str, args: &[Expr]) -> Ev { - // SUBTOTAL needs function_num and ref1; AGGREGATE needs function_num, options and ref1. - let min_args = if name == "AGGREGATE" { 3 } else { 2 }; - if args.len() < min_args { - return err(CellError::Value); - } - let code = match self.arg_num(args, 0, 0.0) { - Ok(n) => n.trunc() as u32, - Err(e) => return err(e), - }; - let (func, options, rest) = if name == "AGGREGATE" { - let opt = match self.arg_num(args, 1, 0.0) { - Ok(n) => n.trunc() as u32, - Err(e) => return err(e), - }; - (code, opt, &args[2..]) - } else { - let (f, ignore_hidden) = if code > 100 { (code - 100, true) } else { (code, false) }; - (f, if ignore_hidden { 5 } else { 0 }, &args[1..]) - }; - if !(1..=19).contains(&func) || (name == "SUBTOTAL" && func > 11) { - return err(CellError::Value); - } - let skip_hidden = matches!(options, 1 | 3 | 5 | 7); - // SUBTOTAL leaves out rows a filter hides whatever the function number (Microsoft's - // SUBTOTAL documentation); 101-111 also leave out rows hidden by hand. - let skip_filtered = name == "SUBTOTAL"; - let skip_errors = matches!(options, 2 | 3 | 6 | 7); - // SUBTOTAL always leaves out nested SUBTOTAL/AGGREGATE cells (101-111 too); AGGREGATE does so for options 0-3. - let skip_nested = name == "SUBTOTAL" || options <= 3; - let mut values = Vec::new(); - let mut k = None; - let refs = if name == "AGGREGATE" && func >= 14 { &rest[..1.min(rest.len())] } else { rest }; - if name == "AGGREGATE" && func >= 14 { - k = rest.get(1).map(|e| self.value(e)); - } - // Every cell visited (even one skipped as hidden, nested or an error) and every value - // counts against one budget for the whole call. All arguments are evaluated (references - // stay lazy) and charged before any cell is read: a value stored in XFD1048576 makes - // `A:XFD` trim to the whole sheet, ~17 billion cells (#2175). - enum Input { - Cells(usize, RangeRef), - Values(Value), - } - let mut inputs = Vec::new(); - let mut budget = MAX_CELLS; - for e in refs { - match self.eval(e) { - Ev::R(areas) => { - for a in areas { - let range = self.trim(&a); - budget = match budget.checked_sub(range.count()) { - Some(left) => left, - None => return err(CellError::Num), - }; - inputs.push(Input::Cells(a.sheet, range)); - } - } - Ev::V(v) => { - let n = match &v { - Value::Array(a) => u64::try_from(a.data.len()).unwrap_or(u64::MAX), - _ => 1, - }; - budget = match budget.checked_sub(n) { - Some(left) => left, - None => return err(CellError::Num), - }; - inputs.push(Input::Values(v)); - } - Ev::L(_) => return err(CellError::Value), - } - } - for input in inputs { - match input { - Input::Cells(sheet, range) => { - for c in range.iter() { - let (hidden, nested) = { - let wb = self.host.workbook(); - let s = wb.sheet(sheet); - let hidden = - s.is_some_and(|s| if skip_hidden { s.is_row_hidden(c.row) } else { skip_filtered && s.is_row_filtered(c.row) }); - let nested = skip_nested - && s.and_then(|s| s.cell(c)).and_then(|cell| cell.formula.as_ref()).is_some_and(|f| { - let u = f.text.to_ascii_uppercase(); - u.contains("SUBTOTAL(") || u.contains("AGGREGATE(") - }); - (hidden, nested) - }; - if hidden || nested { - continue; - } - let v = self.host.cell_value(sheet, c); - if skip_errors && v.is_error() { - continue; - } - values.push(v); - } - } - Input::Values(Value::Array(a)) => values.extend(a.data.iter().filter(|v| !(skip_errors && v.is_error())).cloned()), - Input::Values(v) => values.push(v), - } - } - if let Some(k) = k { - return Ev::V(gridcraft_functions::aggregate_values_k(func, &values, &k)); - } - Ev::V(gridcraft_functions::aggregate_values(func, &values)) - } } trait Pipe: Sized { @@ -1639,424 +636,6 @@ trait Pipe: Sized { } impl Pipe for T {} -/// CELL("format") code for a number format. -fn cell_format_code(code: &str) -> String { - let c = code.to_ascii_lowercase(); - if c == "general" { - return "G".into(); - } - if c.contains('%') { - return format!("P{}", decimals(&c)); - } - if c.contains('e') && c.contains('0') { - return format!("S{}", decimals(&c)); - } - if c.contains('$') { - return format!("C{}", decimals(&c)); - } - if c.contains('y') || c.contains('d') { - return "D1".into(); - } - if c.contains('h') { - return "D9".into(); - } - if c.contains(',') { - return format!(",{}", decimals(&c)); - } - format!("F{}", decimals(&c)) -} - -fn decimals(c: &str) -> usize { - c.split_once('.').map_or(0, |(_, d)| d.chars().take_while(|ch| *ch == '0').count()) -} - -fn same_kind(a: &Value, b: &Value) -> bool { - std::mem::discriminant(a) == std::mem::discriminant(b) || (a.is_empty() || b.is_empty()) -} - -/// Converts an R1C1 reference text to A1 (for INDIRECT(…, FALSE)). `Ok(None)` means the text is -/// not R1C1; `Err` means it is R1C1 but points past the sheet. -pub fn r1c1_to_a1(s: &str, at: CellRef) -> Result, CellError> { - let (sheet, body) = match s.rsplit_once('!') { - Some((sh, b)) => (format!("{sh}!"), b), - None => (String::new(), s), - }; - let conv = |part: &str| -> Result, CellError> { - let u = part.to_ascii_uppercase(); - let Some((rpart, cpart)) = u.strip_prefix('R').and_then(|rest| rest.split_once('C')) else { return Ok(None) }; - // `None`: not R1C1. `Some(None)`: R1C1, but past the sheet. - let num = |p: &str, base: u32, max: u32| -> Option> { - let (v, absolute) = if p.is_empty() { - (Some(i64::from(base)), false) - } else if let Some(inner) = p.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { - (i64::from(base).checked_add(inner.parse().ok()?), false) - } else { - (p.parse::().ok()?.checked_sub(1).map(i64::from), true) - }; - Some(v.filter(|v| (0..i64::from(max)).contains(v)).map(|v| (v as u32, absolute))) - }; - let (Some(r), Some(c)) = (num(rpart, at.row, MAX_ROWS), num(cpart, at.col, MAX_COLS)) else { return Ok(None) }; - let (Some((r, ra)), Some((c, ca))) = (r, c) else { return Err(CellError::Ref) }; - Ok(Some(format!("{}{}{}{}", if ca { "$" } else { "" }, gridcraft_core::col_to_letters(c), if ra { "$" } else { "" }, r + 1))) - }; - let out = match body.split_once(':') { - Some((a, b)) => match (conv(a)?, conv(b)?) { - (Some(a), Some(b)) => format!("{a}:{b}"), - _ => return Ok(None), - }, - None => match conv(body)? { - Some(x) => x, - None => return Ok(None), - }, - }; - Ok(Some(format!("{sheet}{out}"))) -} - -/// The aggregates that can fold a reference without building an array. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Agg { - Sum, - Count, - Average, -} - -impl Agg { - fn of(name: &str) -> Option { - match name { - "SUM" => Some(Agg::Sum), - "COUNT" => Some(Agg::Count), - "AVERAGE" => Some(Agg::Average), - _ => None, - } - } -} - -/// What a folded aggregate carries from one value to the next. -#[derive(Default)] -struct Acc { - sum: f64, - count: u64, -} - -impl Acc { - /// Takes a value found inside a reference: only numbers count. `Err` is an error value that - /// ends `SUM` and `AVERAGE`; `COUNT` skips errors. - fn add_cell(&mut self, kind: Agg, v: &Value) -> Result<(), CellError> { - match v { - Value::Number(n) => { - self.sum += n; - self.count = self.count.saturating_add(1); - Ok(()) - } - Value::Error(e) if kind != Agg::Count => Err(*e), - _ => Ok(()), - } - } - - fn finish(&self, kind: Agg) -> Value { - match kind { - Agg::Sum => Value::number(self.sum), - Agg::Count => Value::number(self.count as f64), - Agg::Average if self.count == 0 => Value::Error(CellError::Div0), - Agg::Average => Value::number(self.sum / self.count as f64), - } - } -} - -// ---------------------------------------------------------------- operators - -fn map_unary(v: &Value, f: &dyn Fn(f64) -> Value, plus: bool, sys: gridcraft_core::DateSystem) -> Value { - match v { - Value::Array(a) => Value::from(Array { rows: a.rows, cols: a.cols, data: a.data.iter().map(|x| map_unary(x, f, plus, sys)).collect() }), - Value::Error(e) => Value::Error(*e), - Value::Text(_) if plus => v.clone(), - other => match other.to_number_in(sys) { - Ok(n) => f(n), - Err(e) => Value::Error(e), - }, - } -} - -/// Element-wise combination with Excel's broadcasting. -pub fn zip2(a: &Value, b: &Value, f: impl Fn(&Value, &Value) -> Value) -> Value { - match (a, b) { - (Value::Array(x), Value::Array(y)) => { - let rows = broadcast_dim(x.rows, y.rows); - let cols = broadcast_dim(x.cols, y.cols); - let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) }; - let mut data = Vec::with_capacity(n); - for r in 0..rows { - for c in 0..cols { - data.push(f(&x.get_broadcast(r, c), &y.get_broadcast(r, c))); - } - } - Value::from(Array { rows, cols, data }) - } - (Value::Array(x), s) => Value::from(Array { rows: x.rows, cols: x.cols, data: x.data.iter().map(|v| f(v, s)).collect() }), - (s, Value::Array(y)) => Value::from(Array { rows: y.rows, cols: y.cols, data: y.data.iter().map(|v| f(s, v)).collect() }), - (x, y) => f(x, y), - } -} - -/// Cell count of a `rows`×`cols` broadcast result, or `None` when it would exceed [`MAX_CELLS`]. -/// Each operand passes its own size cap, but their broadcast product can be far larger -/// (`SEQUENCE(1048576)*SEQUENCE(1,16384)` is 17 billion cells), so it is checked before allocating. -fn broadcast_len(rows: usize, cols: usize) -> Option { - rows.checked_mul(cols).filter(|&n| n as u64 <= MAX_CELLS) -} - -fn broadcast_dim(a: usize, b: usize) -> usize { - if a == 1 { - b - } else if b == 1 { - a - } else { - a.max(b) - } -} - -fn zip3(c: &Array, t: &Value, f: &Value, g: impl Fn(&Value, &Value, &Value) -> Value) -> Value { - let dims = |v: &Value| if let Value::Array(a) = v { (a.rows, a.cols) } else { (1, 1) }; - let (tr, tc) = dims(t); - let (fr, fc) = dims(f); - let rows = broadcast_dim(broadcast_dim(c.rows, tr), fr); - let cols = broadcast_dim(broadcast_dim(c.cols, tc), fc); - let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) }; - let get = |v: &Value, r: usize, col: usize| if let Value::Array(a) = v { a.get_broadcast(r, col) } else { v.clone() }; - let mut data = Vec::with_capacity(n); - for r in 0..rows { - for col in 0..cols { - data.push(g(&c.get_broadcast(r, col), &get(t, r, col), &get(f, r, col))); - } - } - Value::from(Array { rows, cols, data }) -} - -/// One cell of a LAMBDA helper's result, or `None` for an array of more than one value: arrays -/// can't nest, so the whole helper gives `#CALC!`, as in Excel. -fn single(v: Value) -> Option { - match v { - Value::Array(a) if a.rows * a.cols > 1 => None, - v => Some(v.scalar()), - } -} - -pub fn binary_values(op: BinOp, x: &Value, y: &Value, sys: gridcraft_core::DateSystem) -> Value { - zip2(x, y, |a, b| binary_scalar(op, a, b, sys)) -} - -fn binary_scalar(op: BinOp, a: &Value, b: &Value, sys: gridcraft_core::DateSystem) -> Value { - if let Value::Error(e) = a { - return Value::Error(*e); - } - if let Value::Error(e) = b { - return Value::Error(*e); - } - let num = |f: fn(f64, f64) -> Value| match (a.to_number_in(sys), b.to_number_in(sys)) { - (Ok(x), Ok(y)) => f(x, y), - (Err(e), _) | (_, Err(e)) => Value::Error(e), - }; - use std::cmp::Ordering::*; - match op { - BinOp::Add => num(|x, y| Value::number(x + y)), - BinOp::Sub => num(|x, y| Value::number(x - y)), - BinOp::Mul => num(|x, y| Value::number(x * y)), - BinOp::Div => num(|x, y| if y == 0.0 { Value::Error(CellError::Div0) } else { Value::number(x / y) }), - BinOp::Pow => num(|x, y| { - if x == 0.0 && y == 0.0 { - Value::Error(CellError::Num) - } else if x == 0.0 && y < 0.0 { - Value::Error(CellError::Div0) - } else { - Value::number(x.powf(y)) - } - }), - BinOp::Concat => match (a.to_text(), b.to_text()) { - (Ok(x), Ok(y)) => { - if x.len() + y.len() > 32767 * 4 { - Value::Error(CellError::Value) - } else { - Value::text(x + &y) - } - } - (Err(e), _) | (_, Err(e)) => Value::Error(e), - }, - BinOp::Eq => Value::Bool(compare(a, b) == Equal), - BinOp::Ne => Value::Bool(compare(a, b) != Equal), - BinOp::Lt => Value::Bool(compare(a, b) == Less), - BinOp::Gt => Value::Bool(compare(a, b) == Greater), - BinOp::Le => Value::Bool(compare(a, b) != Greater), - BinOp::Ge => Value::Bool(compare(a, b) != Less), - BinOp::Range | BinOp::Union | BinOp::Intersect => Value::Error(CellError::Value), - } -} - -/// SUMIF and AVERAGEIF with a sum range: Excel sizes the sum range like the criteria range. -fn resizes_sum_range(name: &str, args: usize) -> bool { - args == 3 && matches!(name, "SUMIF" | "AVERAGEIF") -} - -/// Height and width of a single-area reference or an array argument. -fn ev_shape(ev: &Ev) -> Option<(u32, u32)> { - match ev { - Ev::R(areas) => match areas.as_slice() { - [a] => Some((a.range.height(), a.range.width())), - _ => None, - }, - Ev::V(Value::Array(a)) => Some((u32::try_from(a.rows).unwrap_or(u32::MAX), u32::try_from(a.cols).unwrap_or(u32::MAX))), - Ev::V(_) => Some((1, 1)), - Ev::L(_) => None, - } -} - -/// The range SUMIF/AVERAGEIF add up: from the sum range's top-left cell, as many rows and columns -/// as the criteria range (`height` × `width`), cut off at the sheet's edge. -pub fn resized_sum_range(sum: RangeRef, height: u32, width: u32) -> RangeRef { - let row = sum.start.row.saturating_add(height.saturating_sub(1)).min(MAX_ROWS - 1); - let col = sum.start.col.saturating_add(width.saturating_sub(1)).min(MAX_COLS - 1); - RangeRef::new(sum.start, CellRef::new(row, col)) -} - -/// Collects the static references of an expression with their sheets resolved; `dynamic` is -/// set when the formula uses INDIRECT/OFFSET/names/tables whose targets can change. -pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec, bool) { - let mut out = Vec::new(); - let mut dynamic = false; - let push = |out: &mut Vec, r: &Reference, range: RangeRef| match &r.sheet { - SheetSel::Current => out.push(Area { sheet, range }), - SheetSel::Named(n) => { - if let Some(i) = wb.sheet_index(n) { - out.push(Area { sheet: i, range }); - } - } - SheetSel::Span(a, b) => { - if let (Some(i), Some(j)) = (wb.sheet_index(a), wb.sheet_index(b)) { - for s in i.min(j)..=i.max(j) { - out.push(Area { sheet: s, range }); - } - } - } - }; - e.walk(&mut |x| match x { - Expr::Ref(r) => push(&mut out, r, r.range()), - // The cells SUMIF/AVERAGEIF really read from the sum range (see `resized_sum_range`); when - // the criteria range's size isn't known statically, recalculate the formula every time. - Expr::Call(n, args) if resizes_sum_range(n, args.len()) => match (args.first(), args.get(2)) { - (Some(Expr::Ref(c)), Some(Expr::Ref(s))) => { - let (crit, sum) = (c.range(), s.range()); - push(&mut out, s, resized_sum_range(sum, crit.height(), crit.width())); - } - (_, Some(Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) | Expr::Error(_) | Expr::Array(_) | Expr::Missing)) => {} - _ => dynamic = true, - }, - Expr::Call(n, _) - if matches!(n.as_str(), "INDIRECT" | "OFFSET" | "NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" | "CELL" | "INFO") => - { - dynamic = true - } - Expr::Name(_) | Expr::Struct(_) => dynamic = true, - Expr::Call(n, _) if gridcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true, - // A range ending in a function's result (`A1:INDEX(B1:B9,n)`) covers the cells between - // the references inside it, not only those references. - Expr::Binary(BinOp::Range, a, b) if !matches!((&**a, &**b), (Expr::Ref(_), Expr::Ref(_))) => { - let mut inner = precedents(wb, sheet, a).0; - inner.extend(precedents(wb, sheet, b).0); - let mut boxes: Vec = Vec::new(); - for area in inner { - match boxes.iter_mut().find(|x| x.sheet == area.sheet) { - Some(x) => x.range = x.range.union(&area.range), - None => boxes.push(area), - } - } - out.extend(boxes); - } - _ => {} - }); - (out, dynamic) -} - -fn is_special(n: &str) -> bool { - matches!( - n, - "IF" | "IFS" - | "IFERROR" - | "IFNA" - | "CHOOSE" - | "SWITCH" - | "ROW" - | "COLUMN" - | "ROWS" - | "COLUMNS" - | "AREAS" - | "ISREF" - | "ISFORMULA" - | "FORMULATEXT" - | "SHEET" - | "SHEETS" - | "OFFSET" - | "INDIRECT" - | "INDEX" - | "CELL" - | "TRIMRANGE" - | "SUBTOTAL" - | "AGGREGATE" - | "TEXT" - | "LET" - | "LAMBDA" - | "ISOMITTED" - | "MAP" - | "REDUCE" - | "SCAN" - | "BYROW" - | "BYCOL" - | "MAKEARRAY" - ) -} - -/// Names of functions the evaluator implements itself (merged into the function list for UI). -pub const SPECIAL_FUNCTIONS: &[&str] = &[ - "IF", - "IFS", - "IFERROR", - "IFNA", - "CHOOSE", - "SWITCH", - "ROW", - "COLUMN", - "ROWS", - "COLUMNS", - "AREAS", - "ISREF", - "ISFORMULA", - "FORMULATEXT", - "SHEET", - "SHEETS", - "OFFSET", - "INDIRECT", - "INDEX", - "CELL", - "TRIMRANGE", - "SUBTOTAL", - "AGGREGATE", - "TEXT", - "LET", - "LAMBDA", - "ISOMITTED", - "MAP", - "REDUCE", - "SCAN", - "BYROW", - "BYCOL", - "MAKEARRAY", -]; - -pub fn is_known_function(n: &str) -> bool { - is_special(&n.to_ascii_uppercase()) || gridcraft_functions::lookup(n).is_some() -} - -#[allow(dead_code)] -fn unused(_: RefKind) {} - #[cfg(test)] mod fuel_tests { use super::*; diff --git a/crates/calc/src/eval/array.rs b/crates/calc/src/eval/array.rs new file mode 100644 index 000000000..b17a8c70f --- /dev/null +++ b/crates/calc/src/eval/array.rs @@ -0,0 +1,180 @@ +use super::*; + +/// The aggregates that can fold a reference without building an array. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum Agg { + Sum, + Count, + Average, +} + +impl Agg { + pub(super) fn of(name: &str) -> Option { + match name { + "SUM" => Some(Agg::Sum), + "COUNT" => Some(Agg::Count), + "AVERAGE" => Some(Agg::Average), + _ => None, + } + } +} + +/// What a folded aggregate carries from one value to the next. +#[derive(Default)] +pub(super) struct Acc { + pub(super) sum: f64, + pub(super) count: u64, +} + +impl Acc { + /// Takes a value found inside a reference: only numbers count. `Err` is an error value that + /// ends `SUM` and `AVERAGE`; `COUNT` skips errors. + pub(super) fn add_cell(&mut self, kind: Agg, v: &Value) -> Result<(), CellError> { + match v { + Value::Number(n) => { + self.sum += n; + self.count = self.count.saturating_add(1); + Ok(()) + } + Value::Error(e) if kind != Agg::Count => Err(*e), + _ => Ok(()), + } + } + + pub(super) fn finish(&self, kind: Agg) -> Value { + match kind { + Agg::Sum => Value::number(self.sum), + Agg::Count => Value::number(self.count as f64), + Agg::Average if self.count == 0 => Value::Error(CellError::Div0), + Agg::Average => Value::number(self.sum / self.count as f64), + } + } +} + +// ---------------------------------------------------------------- operators + +pub(super) fn map_unary(v: &Value, f: &dyn Fn(f64) -> Value, plus: bool, sys: gridcraft_core::DateSystem) -> Value { + match v { + Value::Array(a) => Value::from(Array { rows: a.rows, cols: a.cols, data: a.data.iter().map(|x| map_unary(x, f, plus, sys)).collect() }), + Value::Error(e) => Value::Error(*e), + Value::Text(_) if plus => v.clone(), + other => match other.to_number_in(sys) { + Ok(n) => f(n), + Err(e) => Value::Error(e), + }, + } +} + +/// Element-wise combination with Excel's broadcasting. +pub fn zip2(a: &Value, b: &Value, f: impl Fn(&Value, &Value) -> Value) -> Value { + match (a, b) { + (Value::Array(x), Value::Array(y)) => { + let rows = broadcast_dim(x.rows, y.rows); + let cols = broadcast_dim(x.cols, y.cols); + let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) }; + let mut data = Vec::with_capacity(n); + for r in 0..rows { + for c in 0..cols { + data.push(f(&x.get_broadcast(r, c), &y.get_broadcast(r, c))); + } + } + Value::from(Array { rows, cols, data }) + } + (Value::Array(x), s) => Value::from(Array { rows: x.rows, cols: x.cols, data: x.data.iter().map(|v| f(v, s)).collect() }), + (s, Value::Array(y)) => Value::from(Array { rows: y.rows, cols: y.cols, data: y.data.iter().map(|v| f(s, v)).collect() }), + (x, y) => f(x, y), + } +} + +/// Cell count of a `rows`×`cols` broadcast result, or `None` when it would exceed [`MAX_CELLS`]. +/// Each operand passes its own size cap, but their broadcast product can be far larger +/// (`SEQUENCE(1048576)*SEQUENCE(1,16384)` is 17 billion cells), so it is checked before allocating. +pub(super) fn broadcast_len(rows: usize, cols: usize) -> Option { + rows.checked_mul(cols).filter(|&n| n as u64 <= MAX_CELLS) +} + +fn broadcast_dim(a: usize, b: usize) -> usize { + if a == 1 { + b + } else if b == 1 { + a + } else { + a.max(b) + } +} + +pub(super) fn zip3(c: &Array, t: &Value, f: &Value, g: impl Fn(&Value, &Value, &Value) -> Value) -> Value { + let dims = |v: &Value| if let Value::Array(a) = v { (a.rows, a.cols) } else { (1, 1) }; + let (tr, tc) = dims(t); + let (fr, fc) = dims(f); + let rows = broadcast_dim(broadcast_dim(c.rows, tr), fr); + let cols = broadcast_dim(broadcast_dim(c.cols, tc), fc); + let Some(n) = broadcast_len(rows, cols) else { return Value::Error(CellError::Num) }; + let get = |v: &Value, r: usize, col: usize| if let Value::Array(a) = v { a.get_broadcast(r, col) } else { v.clone() }; + let mut data = Vec::with_capacity(n); + for r in 0..rows { + for col in 0..cols { + data.push(g(&c.get_broadcast(r, col), &get(t, r, col), &get(f, r, col))); + } + } + Value::from(Array { rows, cols, data }) +} + +/// One cell of a LAMBDA helper's result, or `None` for an array of more than one value: arrays +/// can't nest, so the whole helper gives `#CALC!`, as in Excel. +pub(super) fn single(v: Value) -> Option { + match v { + Value::Array(a) if a.rows * a.cols > 1 => None, + v => Some(v.scalar()), + } +} + +pub fn binary_values(op: BinOp, x: &Value, y: &Value, sys: gridcraft_core::DateSystem) -> Value { + zip2(x, y, |a, b| binary_scalar(op, a, b, sys)) +} + +fn binary_scalar(op: BinOp, a: &Value, b: &Value, sys: gridcraft_core::DateSystem) -> Value { + if let Value::Error(e) = a { + return Value::Error(*e); + } + if let Value::Error(e) = b { + return Value::Error(*e); + } + let num = |f: fn(f64, f64) -> Value| match (a.to_number_in(sys), b.to_number_in(sys)) { + (Ok(x), Ok(y)) => f(x, y), + (Err(e), _) | (_, Err(e)) => Value::Error(e), + }; + use std::cmp::Ordering::*; + match op { + BinOp::Add => num(|x, y| Value::number(x + y)), + BinOp::Sub => num(|x, y| Value::number(x - y)), + BinOp::Mul => num(|x, y| Value::number(x * y)), + BinOp::Div => num(|x, y| if y == 0.0 { Value::Error(CellError::Div0) } else { Value::number(x / y) }), + BinOp::Pow => num(|x, y| { + if x == 0.0 && y == 0.0 { + Value::Error(CellError::Num) + } else if x == 0.0 && y < 0.0 { + Value::Error(CellError::Div0) + } else { + Value::number(x.powf(y)) + } + }), + BinOp::Concat => match (a.to_text(), b.to_text()) { + (Ok(x), Ok(y)) => { + if x.len() + y.len() > 32767 * 4 { + Value::Error(CellError::Value) + } else { + Value::text(x + &y) + } + } + (Err(e), _) | (_, Err(e)) => Value::Error(e), + }, + BinOp::Eq => Value::Bool(compare(a, b) == Equal), + BinOp::Ne => Value::Bool(compare(a, b) != Equal), + BinOp::Lt => Value::Bool(compare(a, b) == Less), + BinOp::Gt => Value::Bool(compare(a, b) == Greater), + BinOp::Le => Value::Bool(compare(a, b) != Greater), + BinOp::Ge => Value::Bool(compare(a, b) != Less), + BinOp::Range | BinOp::Union | BinOp::Intersect => Value::Error(CellError::Value), + } +} diff --git a/crates/calc/src/eval/call.rs b/crates/calc/src/eval/call.rs new file mode 100644 index 000000000..9013ca882 --- /dev/null +++ b/crates/calc/src/eval/call.rs @@ -0,0 +1,424 @@ +use super::array::{Acc, Agg, zip2, zip3}; +use super::format::same_kind; +use super::special::{ev_shape, resized_sum_range, resizes_sum_range}; +use super::*; + +impl<'h> Evaluator<'h> { + pub(super) fn call(&mut self, name: &str, args: &[Expr]) -> Ev { + match name { + "IF" => { + if args.is_empty() || args.len() > 3 { + return err(CellError::Value); + } + let c = self.arg_val(args, 0).unwrap_or_default(); + if let Value::Array(arr) = &c { + // Array condition: element-wise. + let t = self.arg_val(args, 1).unwrap_or(Value::Bool(true)); + let f = if args.len() > 2 { self.arg_val(args, 2).unwrap_or(Value::Bool(false)) } else { Value::Bool(false) }; + return Ev::V(zip3(arr, &t, &f, |c, t, f| match c.to_bool() { + Ok(true) => t.clone(), + Ok(false) => f.clone(), + Err(e) => Value::Error(e), + })); + } + match c.to_bool() { + Ok(true) => match args.get(1) { + Some(Expr::Missing) => Ev::V(Value::Number(0.0)), + Some(e) => self.eval(e), + None => Ev::V(Value::Bool(true)), + }, + Ok(false) => match args.get(2) { + Some(Expr::Missing) => Ev::V(Value::Number(0.0)), + Some(e) => self.eval(e), + None => Ev::V(Value::Bool(false)), + }, + Err(e) => err(e), + } + } + "IFS" => { + if args.len() < 2 || !args.len().is_multiple_of(2) { + return err(CellError::Value); + } + for pair in args.chunks(2) { + let [c, v] = pair else { break }; + let cv = self.value(c); + match cv.scalar().to_bool() { + Ok(true) => return self.eval(v), + Ok(false) => {} + Err(e) => return err(e), + } + } + err(CellError::NA) + } + "IFERROR" | "IFNA" => { + if args.len() != 2 { + return err(CellError::Value); + } + let v = self.arg_ev(args, 0).unwrap_or(Ev::V(Value::Empty)); + let na_only = name == "IFNA"; + let catch = |e: CellError| if na_only { e == CellError::NA } else { true }; + match &v { + Ev::V(Value::Error(e)) if catch(*e) => self.eval(&args[1]), + Ev::V(Value::Array(a)) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => { + let alt = self.arg_val(args, 1).unwrap_or_default(); + let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect(); + Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value))) + } + Ev::R(_) => { + let val = self.deref(v.clone()); + match val { + Value::Error(e) if catch(e) => self.eval(&args[1]), + Value::Array(ref a) if a.data.iter().any(|x| x.as_error().is_some_and(catch)) => { + let alt = self.arg_val(args, 1).unwrap_or_default(); + let data = a.data.iter().map(|x| if x.as_error().is_some_and(catch) { alt.scalar() } else { x.clone() }).collect(); + Ev::V(Array::new(a.rows, a.cols, data).map(Value::from).unwrap_or(Value::Error(CellError::Value))) + } + _ => v, + } + } + _ => v, + } + } + "CHOOSE" => { + if args.len() < 2 { + return err(CellError::Value); + } + let index = self.value(&args[0]); + if let Value::Array(_) = index { + // An array of indexes picks element by element: `CHOOSE({1,2},A1:A3,B1:B3)` + // is a two-column array. The library CHOOSE lifts over its arguments. + let Some(spec) = gridcraft_functions::lookup(name) else { return err(CellError::Name) }; + let mut evaluated = vec![Arg { value: index, from_ref: false }]; + for a in &args[1..] { + let value = self.value(a); + evaluated.push(Arg { value, from_ref: false }); + } + let mut ctx = FnCtx { ev: self, criteria: None, lambda: None }; + return gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V); + } + let i = match index.to_number() { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + if i < 1 || i as usize >= args.len() { + return err(CellError::Value); + } + self.eval(&args[i as usize]) + } + "SWITCH" => { + if args.len() < 3 { + return err(CellError::Value); + } + let v = self.arg_val(args, 0).unwrap_or_default().scalar(); + if let Value::Error(e) = v { + return err(e); + } + let rest = &args[1..]; + let mut i = 0; + while i + 1 < rest.len() { + let cand = self.value(&rest[i]).scalar(); + if !cand.is_error() && compare(&v, &cand) == std::cmp::Ordering::Equal && same_kind(&v, &cand) { + return self.eval(&rest[i + 1]); + } + i += 2; + } + if rest.len() % 2 == 1 { rest.last().map_or(err(CellError::NA), |d| self.eval(d)) } else { err(CellError::NA) } + } + "ROW" | "COLUMN" => { + let is_row = name == "ROW"; + if args.is_empty() || matches!(args.first(), Some(Expr::Missing)) { + return Ev::V(Value::number(if is_row { self.at.row + 1 } else { self.at.col + 1 } as f64)); + } + match self.eval(&args[0]) { + Ev::R(a) => { + let Some(a) = a.first() else { return err(CellError::Ref) }; + let r = self.trim_lines(a); + if is_row { + let rows: Vec = (r.start.row..=r.end.row).map(|x| Value::number(x as f64 + 1.0)).collect(); + if rows.len() == 1 { Ev::V(rows.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::column(rows).into()) } + } else { + let cols: Vec = (r.start.col..=r.end.col).map(|x| Value::number(x as f64 + 1.0)).collect(); + if cols.len() == 1 { Ev::V(cols.into_iter().next().unwrap_or_default()) } else { Ev::V(Array::row(cols).into()) } + } + } + Ev::V(Value::Error(e)) => err(e), + _ => err(CellError::Value), + } + } + "ROWS" | "COLUMNS" => { + if args.len() != 1 { + return err(CellError::Value); + } + let is_rows = name == "ROWS"; + match self.eval(&args[0]) { + Ev::R(a) => match a.as_slice() { + [a] => Ev::V(Value::number(if is_rows { a.range.height() } else { a.range.width() } as f64)), + _ => err(CellError::Ref), + }, + Ev::V(Value::Array(arr)) => Ev::V(Value::number(if is_rows { arr.rows } else { arr.cols } as f64)), + Ev::V(Value::Error(e)) => err(e), + Ev::V(_) => Ev::V(Value::Number(1.0)), + Ev::L(_) => err(CellError::Value), + } + } + "AREAS" => match args.first().map(|a| self.eval(a)) { + Some(Ev::R(a)) => Ev::V(Value::number(a.len() as f64)), + Some(Ev::V(Value::Error(e))) => err(e), + _ => err(CellError::Value), + }, + "ISREF" => Ev::V(Value::Bool(matches!(args.first().map(|a| self.eval(a)), Some(Ev::R(_))))), + "ISFORMULA" | "FORMULATEXT" => { + let Some(Ev::R(a)) = args.first().map(|a| self.eval(a)) else { return err(CellError::NA) }; + let Some(a) = a.first() else { return err(CellError::NA) }; + let f = self.host.workbook().sheet(a.sheet).and_then(|s| s.cell(a.range.start)).and_then(|c| c.formula.clone()); + if name == "ISFORMULA" { + Ev::V(Value::Bool(f.is_some())) + } else { + match f { + Some(f) => Ev::V(Value::text(format!("={}", f.text))), + None => err(CellError::NA), + } + } + } + "SHEET" | "SHEETS" => { + let wb = self.host.workbook(); + if args.is_empty() { + return Ev::V(Value::number(if name == "SHEET" { self.sheet + 1 } else { wb.sheets.len() } as f64)); + } + match self.eval(&args[0]) { + Ev::R(a) => { + if name == "SHEET" { + a.first().map_or(err(CellError::Ref), |a| Ev::V(Value::number(a.sheet as f64 + 1.0))) + } else { + let mut s: Vec = a.iter().map(|x| x.sheet).collect(); + s.dedup(); + Ev::V(Value::number(s.len() as f64)) + } + } + Ev::V(Value::Text(t)) if name == "SHEET" => { + let wb = self.host.workbook(); + wb.sheet_index(&t).map_or(err(CellError::NA), |i| Ev::V(Value::number(i as f64 + 1.0))) + } + _ => err(CellError::NA), + } + } + "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), + "TEXT" => { + if args.len() != 2 { + return err(CellError::Value); + } + let v = self.arg_val(args, 0).unwrap_or_default(); + let f = self.arg_val(args, 1).unwrap_or_default(); + let sys = self.host.workbook().date_system; + let one = |v: &Value, f: &Value| -> Value { + if let Value::Error(e) = v { + return Value::Error(*e); + } + let code = match f.to_text() { + Ok(c) => c, + Err(e) => return Value::Error(e), + }; + match gridcraft_numfmt::text_function(v, &code, sys) { + Ok(s) => Value::text(s), + Err(e) => Value::Error(e), + } + }; + Ev::V(zip2(&v, &f, one)) + } + "LET" => self.let_(args), + "LAMBDA" => { + if args.is_empty() { + return err(CellError::Value); + } + let mut params = Vec::new(); + for p in &args[..args.len() - 1] { + match p { + Expr::Name(n) => params.push(n.trim_start_matches('[').trim_end_matches(']').to_string()), + _ => return err(CellError::Value), + } + } + let Some(body) = args.last() else { return err(CellError::Value) }; + Ev::L(Arc::new(Lambda { params, body: body.clone(), env: self.env.clone() })) + } + "ISOMITTED" => { + let r = matches!(args.first(), Some(Expr::Name(n)) if self.env.iter().rev().find(|(k, _)| gridcraft_formula::same_name(k, n)).is_some_and(|(_, v)| matches!(v, Ev::V(Value::Empty)))); + 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) + } + _ => self.builtin(name, args), + } + } + + pub(super) fn builtin(&mut self, name: &str, args: &[Expr]) -> Ev { + let Some(spec) = gridcraft_functions::lookup(name) else { + // A defined name holding a LAMBDA, or a LET-bound lambda. + return match self.name(name) { + Ev::L(l) => self.apply_lambda(&l, args), + Ev::V(Value::Error(x)) => err(x), + _ => err(CellError::Name), + }; + }; + if args.len() < spec.min_args || args.len() > spec.max_args { + return err(CellError::Value); + } + if let Some(kind) = Agg::of(spec.name) { + return self.aggregate(kind, args); + } + let mut evaluated = Vec::with_capacity(args.len()); + // SUMIF/AVERAGEIF: the size of the criteria range (argument 1), which the sum range takes. + let mut criteria_shape = None; + // Database functions: the criteria range (argument 2), whose formulas are computed criteria. + let mut criteria = None; + // GROUPBY/PIVOTBY: the LAMBDA given as their function. + let mut lambda = None; + let lambda_at = match name { + "GROUPBY" => Some(2), + "PIVOTBY" => Some(3), + _ => None, + }; + for (i, a) in args.iter().enumerate() { + let mut ev = self.eval(a); + if lambda_at == Some(i) + && let Ev::L(l) = &ev + { + lambda = Some(l.clone()); + ev = Ev::V(Value::Empty); + } + if spec.category == gridcraft_functions::Category::Database + && i == 2 + && let Ev::R(areas) = &ev + && let [area] = areas.as_slice() + { + criteria = Some(*area); + } + if resizes_sum_range(name, args.len()) { + if i == 0 { + criteria_shape = ev_shape(&ev); + } else if i == 2 + && let (Some((h, w)), Ev::R(areas)) = (criteria_shape, &ev) + && let [area] = areas.as_slice() + { + ev = Ev::R(vec![Area { sheet: area.sheet, range: resized_sum_range(area.range, h, w) }]); + } + } + let from_ref = matches!(ev, Ev::R(_)); + let value = match ev { + Ev::R(areas) if areas.len() > 1 => { + // Multi-area references (unions): flatten into one row for aggregates. + let mut vals = Vec::new(); + for ar in &areas { + match self.area_value(ar) { + Value::Array(a) => vals.extend(a.data.iter().cloned()), + v => vals.push(v), + } + } + Value::from(Array::row(vals)) + } + ev => self.deref(ev), + }; + evaluated.push(Arg { value, from_ref }); + } + let mut ctx = FnCtx { ev: self, criteria, lambda }; + gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V) + } + + /// Performs aggregation operations without copying its ranges: blocks are folded over the stored + /// cells, so no array is built and there is no `MAX_CELLS` limit. Arguments are evaluated and read one + /// after another, as the array path does, so volatile formulas (like `RAND`) see the same order. + /// Only `SUM`, `COUNT` and `AVG` are supported currently. + fn aggregate(&mut self, kind: Agg, args: &[Expr]) -> Ev { + let mut acc = Acc::default(); + let mut first_error = None; + for a in args { + let ev = self.eval(a); + let steps = match ev { + Ev::R(areas) => areas.iter().map(|area| self.aggregate_area(kind, area, &mut acc)).collect(), + ev => { + let value = self.deref(ev); + vec![self.aggregate_value(kind, value, false, &mut acc)] + } + }; + if first_error.is_none() { + first_error = steps.into_iter().find_map(Result::err); + } + } + first_error.unwrap_or_else(|| Ev::V(acc.finish(kind))) + } + + /// Adds one area to `acc`; `Err` is the result the function ends with. + fn aggregate_area(&mut self, kind: Agg, a: &Area, acc: &mut Acc) -> Result<(), Ev> { + if !a.range.is_single() { + let mut error = None; + let walked = self.host.visit_range(a.sheet, a.range, &mut |v| match acc.add_cell(kind, v) { + Ok(()) => true, + Err(e) => { + error = Some(e); + false + } + }); + if walked { + return error.map_or(Ok(()), |e| Err(err(e))); + } + } + let value = self.area_value(a); + self.aggregate_value(kind, value, true, acc) + } + + /// Adds one argument's value by calling `SUM` and `COUNT` on it, so coercion and error rules + /// stay in one place (`AVERAGE` of an argument is its `SUM` over its `COUNT`). + fn aggregate_value(&mut self, kind: Agg, value: Value, from_ref: bool, acc: &mut Acc) -> Result<(), Ev> { + let (Some(sum), Some(count)) = (gridcraft_functions::lookup("SUM"), gridcraft_functions::lookup("COUNT")) else { + return Err(err(CellError::Name)); + }; + let args = [Arg { value, from_ref }]; + if kind != Agg::Count { + match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) { + Value::Number(n) => acc.sum += n, + other => return Err(Ev::V(other)), + } + } + if kind != Agg::Sum { + match gridcraft_functions::call(count, &args, &mut FnCtx { ev: self, criteria: None, lambda: None }) { + Value::Number(n) => acc.count = acc.count.saturating_add(n as u64), + other => return Err(Ev::V(other)), + } + } + Ok(()) + } + + /// 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. + pub(super) fn function_by_name(&self, e: &Expr) -> Option { + let Expr::Name(n) = e else { return None }; + if self.env.iter().any(|(k, _)| gridcraft_formula::same_name(k, n)) || self.host.workbook().name(n, self.sheet).is_some() { + return None; + } + let u = n.to_ascii_uppercase(); + gridcraft_functions::lookup(&u).map(|_| u) + } +} diff --git a/crates/calc/src/eval/format.rs b/crates/calc/src/eval/format.rs new file mode 100644 index 000000000..a4c52b492 --- /dev/null +++ b/crates/calc/src/eval/format.rs @@ -0,0 +1,74 @@ +use super::*; + +/// CELL("format") code for a number format. +pub(super) fn cell_format_code(code: &str) -> String { + let c = code.to_ascii_lowercase(); + if c == "general" { + return "G".into(); + } + if c.contains('%') { + return format!("P{}", decimals(&c)); + } + if c.contains('e') && c.contains('0') { + return format!("S{}", decimals(&c)); + } + if c.contains('$') { + return format!("C{}", decimals(&c)); + } + if c.contains('y') || c.contains('d') { + return "D1".into(); + } + if c.contains('h') { + return "D9".into(); + } + if c.contains(',') { + return format!(",{}", decimals(&c)); + } + format!("F{}", decimals(&c)) +} + +fn decimals(c: &str) -> usize { + c.split_once('.').map_or(0, |(_, d)| d.chars().take_while(|ch| *ch == '0').count()) +} + +pub(super) fn same_kind(a: &Value, b: &Value) -> bool { + std::mem::discriminant(a) == std::mem::discriminant(b) || (a.is_empty() || b.is_empty()) +} + +/// Converts an R1C1 reference text to A1 (for INDIRECT(…, FALSE)). `Ok(None)` means the text is +/// not R1C1; `Err` means it is R1C1 but points past the sheet. +pub fn r1c1_to_a1(s: &str, at: CellRef) -> Result, CellError> { + let (sheet, body) = match s.rsplit_once('!') { + Some((sh, b)) => (format!("{sh}!"), b), + None => (String::new(), s), + }; + let conv = |part: &str| -> Result, CellError> { + let u = part.to_ascii_uppercase(); + let Some((rpart, cpart)) = u.strip_prefix('R').and_then(|rest| rest.split_once('C')) else { return Ok(None) }; + // `None`: not R1C1. `Some(None)`: R1C1, but past the sheet. + let num = |p: &str, base: u32, max: u32| -> Option> { + let (v, absolute) = if p.is_empty() { + (Some(i64::from(base)), false) + } else if let Some(inner) = p.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { + (i64::from(base).checked_add(inner.parse().ok()?), false) + } else { + (p.parse::().ok()?.checked_sub(1).map(i64::from), true) + }; + Some(v.filter(|v| (0..i64::from(max)).contains(v)).map(|v| (v as u32, absolute))) + }; + let (Some(r), Some(c)) = (num(rpart, at.row, MAX_ROWS), num(cpart, at.col, MAX_COLS)) else { return Ok(None) }; + let (Some((r, ra)), Some((c, ca))) = (r, c) else { return Err(CellError::Ref) }; + Ok(Some(format!("{}{}{}{}", if ca { "$" } else { "" }, gridcraft_core::col_to_letters(c), if ra { "$" } else { "" }, r + 1))) + }; + let out = match body.split_once(':') { + Some((a, b)) => match (conv(a)?, conv(b)?) { + (Some(a), Some(b)) => format!("{a}:{b}"), + _ => return Ok(None), + }, + None => match conv(body)? { + Some(x) => x, + None => return Ok(None), + }, + }; + Ok(Some(format!("{sheet}{out}"))) +} diff --git a/crates/calc/src/eval/lambda.rs b/crates/calc/src/eval/lambda.rs new file mode 100644 index 000000000..45f99b488 --- /dev/null +++ b/crates/calc/src/eval/lambda.rs @@ -0,0 +1,198 @@ +use super::array::{broadcast_len, single}; +use super::*; + +impl<'h> Evaluator<'h> { + pub fn apply_lambda(&mut self, l: &Lambda, args: &[Expr]) -> Ev { + // Too many or too few argument positions is #VALUE!. An explicitly empty position, as in + // `(1,)`, is `Expr::Missing` and still counts as a position (it binds as omitted). `f()` + // parses to no arguments, which for a one-parameter lambda can also be read as one empty + // position, so that call still binds the parameter as omitted. + let positions_ok = args.len() == l.params.len() || (args.is_empty() && l.params.len() == 1); + if !positions_ok { + return err(CellError::Value); + } + let mut bound = Vec::with_capacity(l.params.len()); + for (i, p) in l.params.iter().enumerate() { + let v = match args.get(i) { + Some(Expr::Missing) | None => Ev::V(Value::Empty), + Some(e) => self.eval(e), + }; + bound.push((p.clone(), v)); + } + self.call_lambda_values(l, bound) + } + + pub(super) fn call_lambda_values(&mut self, l: &Lambda, bound: Vec<(String, Ev)>) -> Ev { + if self.calls >= MAX_LAMBDA_CALLS { + return err(CellError::Num); + } + // #2382: one charge per invocation, so a cell's total work stays bounded however the + // lambdas nest. Foregrounding the charge here means every path into a body (direct + // `(2,3)` application, `MAP`/`REDUCE`/`BYROW` helpers, D-function criteria) meters. + self.fuel = match self.fuel.checked_sub(1) { + Some(f) => f, + None => return err(CellError::Calc), + }; + self.calls += 1; + let saved = std::mem::replace(&mut self.env, l.env.clone()); + self.env.extend(bound); + let r = self.eval(&l.body); + self.env = saved; + self.calls -= 1; + r + } + + pub(super) fn let_(&mut self, args: &[Expr]) -> Ev { + if args.len() < 3 || args.len().is_multiple_of(2) { + return err(CellError::Value); + } + let base = self.env.len(); + for pair in args[..args.len() - 1].chunks(2) { + let [Expr::Name(n), v] = pair else { + self.env.truncate(base); + return err(CellError::Value); + }; + let val = self.eval(v); + self.env.push((n.clone(), val)); + } + let r = match args.last() { + Some(body) => self.eval(body), + None => err(CellError::Value), + }; + self.env.truncate(base); + r + } + + /// 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, + } + } + + pub(super) fn higher_order(&mut self, name: &str, args: &[Expr]) -> Ev { + let as_array = |v: Value| -> Arc { + match v { + Value::Array(a) => a, + other => Arc::new(Array::scalar(other)), + } + }; + match name { + "MAP" => { + if args.len() < 2 { + 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); + let Some(n) = broadcast_len(rows, cols) else { return err(CellError::Num) }; + // The LAMBDA takes one parameter per array. + if l.params.len() != arrays.len() { + return err(CellError::Value); + } + let mut out = Vec::with_capacity(n); + for r in 0..rows { + for c in 0..cols { + let bound = l.params.iter().zip(arrays.iter()).map(|(p, a)| (p.clone(), Ev::V(a.get_broadcast(r, c)))).collect(); + let v = self.call_lambda_values(&l, bound); + let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; + out.push(one); + } + } + Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) + } + "REDUCE" | "SCAN" => { + if args.len() != 3 { + return err(CellError::Value); + } + let mut acc = self.value(&args[0]); + let arr = as_array(self.value(&args[1])); + let Some(l) = self.lambda_arg(args.get(2), 2) else { return err(CellError::Value) }; + if l.params.len() != 2 { + return err(CellError::Value); + } + let mut scan = Vec::with_capacity(arr.data.len()); + for v in arr.data.iter() { + let bound = vec![(l.params[0].clone(), Ev::V(acc.clone())), (l.params[1].clone(), Ev::V(v.clone()))]; + let r = self.call_lambda_values(&l, bound); + acc = self.deref(r); + if name == "SCAN" { + let Some(one) = single(acc.clone()) else { return err(CellError::Calc) }; + scan.push(one); + } + } + if name == "SCAN" { + Ev::V(Array::new(arr.rows, arr.cols, scan).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) + } else { + Ev::V(acc) + } + } + "BYROW" | "BYCOL" => { + if args.len() != 2 { + return err(CellError::Value); + } + let arr = as_array(self.value(&args[0])); + 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) }; + if l.params.len() != 1 { + return err(CellError::Value); + } + let by_row = name == "BYROW"; + let n = if by_row { arr.rows } else { arr.cols }; + let mut out = Vec::with_capacity(n); + for i in 0..n { + let slice: Vec = if by_row { + (0..arr.cols).map(|c| arr.get(i, c).cloned().unwrap_or_default()).collect() + } else { + (0..arr.rows).map(|r| arr.get(r, i).cloned().unwrap_or_default()).collect() + }; + let a = if by_row { Array::row(slice) } else { Array::column(slice) }; + let r = self.call_lambda_values(&l, vec![(p.clone(), Ev::V(a.into()))]); + let Some(one) = single(self.deref(r)) else { return err(CellError::Calc) }; + out.push(one); + } + let a = if by_row { Array::column(out) } else { Array::row(out) }; + Ev::V(a.into()) + } + "MAKEARRAY" => { + if args.len() != 3 { + return err(CellError::Value); + } + let (rows, cols) = match (self.arg_num(args, 0, 0.0), self.arg_num(args, 1, 0.0)) { + (Ok(r), Ok(c)) => (r.trunc(), c.trunc()), + (Err(e), _) | (_, Err(e)) => return err(e), + }; + 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), 2) else { return err(CellError::Value) }; + if l.params.len() != 2 { + return err(CellError::Value); + } + let (rows, cols) = (rows as usize, cols as usize); + let mut out = Vec::with_capacity(rows * cols); + for r in 0..rows { + for c in 0..cols { + let bound = vec![ + (l.params[0].clone(), Ev::V(Value::number(r as f64 + 1.0))), + (l.params[1].clone(), Ev::V(Value::number(c as f64 + 1.0))), + ]; + let v = self.call_lambda_values(&l, bound); + let Some(one) = single(self.deref(v)) else { return err(CellError::Calc) }; + out.push(one); + } + } + Ev::V(Array::new(rows, cols, out).map(Value::from).unwrap_or(Value::Error(CellError::Calc))) + } + _ => err(CellError::Name), + } + } +} diff --git a/crates/calc/src/eval/reference_fns.rs b/crates/calc/src/eval/reference_fns.rs new file mode 100644 index 000000000..a21797c23 --- /dev/null +++ b/crates/calc/src/eval/reference_fns.rs @@ -0,0 +1,407 @@ +use super::format::{cell_format_code, r1c1_to_a1}; +use super::*; + +impl<'h> Evaluator<'h> { + /// Whole rows/columns trimmed for ROW()/COLUMN() arrays. + pub(super) fn trim_lines(&mut self, a: &Area) -> RangeRef { + if a.range.is_full_cols() || a.range.is_full_rows() { self.trim(a) } else { a.range } + } + + pub(super) fn offset(&mut self, args: &[Expr]) -> Ev { + self.host.note_volatile(); + if args.len() < 3 || args.len() > 5 { + return err(CellError::Value); + } + let base = match self.eval(&args[0]) { + Ev::R(a) if a.len() == 1 => a[0], + Ev::V(Value::Error(e)) => return err(e), + _ => return err(CellError::Value), + }; + let rows = match self.arg_num(args, 1, 0.0) { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + let cols = match self.arg_num(args, 2, 0.0) { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + let h = match self.arg_num(args, 3, base.range.height() as f64) { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + let w = match self.arg_num(args, 4, base.range.width() as f64) { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + if h == 0 || w == 0 { + return err(CellError::Ref); + } + let r0 = (base.range.start.row as i64).saturating_add(rows); + let c0 = (base.range.start.col as i64).saturating_add(cols); + let (r0, r1) = if h > 0 { (r0, r0.saturating_add(h - 1)) } else { (r0.saturating_add(h + 1), r0) }; + let (c0, c1) = if w > 0 { (c0, c0.saturating_add(w - 1)) } else { (c0.saturating_add(w + 1), c0) }; + if r0 < 0 || c0 < 0 || r1 >= MAX_ROWS as i64 || c1 >= MAX_COLS as i64 { + return err(CellError::Ref); + } + Ev::R(vec![Area { sheet: base.sheet, range: RangeRef::new(CellRef::new(r0 as u32, c0 as u32), CellRef::new(r1 as u32, c1 as u32)) }]) + } + + pub(super) fn indirect(&mut self, args: &[Expr]) -> Ev { + self.host.note_volatile(); + if args.is_empty() || args.len() > 2 { + return err(CellError::Value); + } + let text = match self.arg_val(args, 0).map(|v| v.scalar().to_text()) { + Some(Ok(t)) => t, + Some(Err(e)) => return err(e), + None => return err(CellError::Value), + }; + let a1 = match args.get(1) { + None | Some(Expr::Missing) => true, + Some(e) => { + let v = self.value(e); + v.scalar().to_bool().unwrap_or(true) + } + }; + // With a1 = FALSE, text that isn't R1C1 can still be a name, but never an A1 reference. + let (text, a1_ok) = if a1 { + (text, true) + } else { + match r1c1_to_a1(&text, self.at) { + Ok(Some(a1_text)) => (a1_text, true), + Ok(None) => (text, false), + Err(e) => return err(e), + } + }; + match gridcraft_formula::parse(&text) { + Ok(Expr::Ref(_)) if !a1_ok => err(CellError::Ref), + Ok(e @ (Expr::Ref(_) | Expr::Name(_) | Expr::Struct(_))) => match self.eval(&e) { + r @ Ev::R(_) => r, + _ => err(CellError::Ref), + }, + _ => err(CellError::Ref), + } + } + + pub(super) fn index(&mut self, args: &[Expr]) -> Ev { + if args.is_empty() || args.len() > 4 { + return err(CellError::Value); + } + let src = self.eval(&args[0]); + let mut arg = |i: usize| match args.get(i) { + None | Some(Expr::Missing) => None, + Some(e) => Some(self.value(e)), + }; + let (rows, cols) = (arg(1), arg(2)); + let area_n = match self.arg_num(args, 3, 1.0) { + Ok(n) => n.trunc() as i64, + Err(e) => return err(e), + }; + let two = args.len() == 2; + let num = |v: &Option| v.as_ref().map_or(Ok(0.0), |v| v.to_number()).map(|n| n.trunc() as i64); + // Arrays of positions pick a value for each: INDEX(A1:C10,{1;2},{1,3}) is 2×2. + if let (Some(Value::Array(_)), _) | (_, Some(Value::Array(_))) = (&rows, &cols) { + let as_array = |v: &Option| match v { + Some(Value::Array(a)) => Arc::clone(a), + Some(v) => Arc::new(Array::scalar(v.clone())), + None => Arc::new(Array::scalar(Value::Number(0.0))), + }; + let (ra, ca) = (as_array(&rows), as_array(&cols)); + let (h, w) = (ra.rows.max(ca.rows), ra.cols.max(ca.cols)); + if (h as u64).saturating_mul(w as u64) > MAX_CELLS { + return err(CellError::Num); + } + let mut out = Vec::with_capacity(h * w); + for r in 0..h { + for c in 0..w { + let pos = (num(&Some(ra.get_broadcast(r, c))), num(&Some(ca.get_broadcast(r, c)))); + let v = match pos { + (Ok(row), Ok(col)) => { + let one = self.index_one(src.clone(), row, col, area_n, two); + match self.deref(one) { + Value::Array(a) if a.rows * a.cols > 1 => Value::Error(CellError::Calc), + v => v.scalar(), + } + } + (Err(e), _) | (_, Err(e)) => Value::Error(e), + }; + out.push(v); + } + } + return Ev::V(Array::new(h, w, out).map(Value::from).unwrap_or(Value::Error(CellError::Value))); + } + let (row, col) = match (num(&rows), num(&cols)) { + (Ok(r), Ok(c)) => (r, c), + (Err(e), _) | (_, Err(e)) => return err(e), + }; + self.index_one(src, row, col, area_n, two) + } + + /// INDEX for one row and column position (0 = the whole column or row). + fn index_one(&mut self, src: Ev, row: i64, col: i64, area_n: i64, two_args: bool) -> Ev { + if row < 0 || col < 0 { + return err(CellError::Value); + } + match src { + Ev::R(areas) => { + let area = usize::try_from((area_n as u64).saturating_sub(1)).ok(); + let Some(a) = area.and_then(|i| areas.get(i)).copied() else { return err(CellError::Ref) }; + let r = a.range; + // A single row or column with one index picks along it. An area with several rows + // and columns needs both: `INDEX(A1:C3,2)` is #REF! in Excel (an empty third + // argument, `INDEX(A1:C3,2,)`, still means the whole row). Arrays differ: + // `INDEX({1,2;3,4},2)` is the row. + if two_args && r.height() > 1 && r.width() > 1 { + return err(CellError::Ref); + } + let (row, col) = if two_args && r.height() == 1 { (1, row) } else { (row, col) }; + if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 { + return err(CellError::Ref); + } + let (r0, r1) = if row == 0 { (r.start.row, r.end.row) } else { (r.start.row + row as u32 - 1, r.start.row + row as u32 - 1) }; + let (c0, c1) = if col == 0 { (r.start.col, r.end.col) } else { (r.start.col + col as u32 - 1, r.start.col + col as u32 - 1) }; + Ev::R(vec![Area { sheet: a.sheet, range: RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1)) }]) + } + Ev::V(Value::Array(arr)) => { + let (row, col) = if two_args && arr.rows == 1 { (1, row) } else { (row, col) }; + if row as usize > arr.rows || col as usize > arr.cols { + return err(CellError::Ref); + } + match (row, col) { + (0, 0) => Ev::V(Value::Array(arr)), + (0, c) => Ev::V(Array::column((0..arr.rows).map(|r| arr.get(r, c as usize - 1).cloned().unwrap_or_default()).collect()).into()), + (r, 0) => Ev::V(Array::row((0..arr.cols).map(|c| arr.get(r as usize - 1, c).cloned().unwrap_or_default()).collect()).into()), + (r, c) => Ev::V(arr.get(r as usize - 1, c as usize - 1).cloned().unwrap_or(Value::Error(CellError::Ref))), + } + } + Ev::V(Value::Error(e)) => err(e), + Ev::V(v) => { + if row <= 1 && col <= 1 { + Ev::V(v) + } else { + err(CellError::Ref) + } + } + Ev::L(_) => err(CellError::Value), + } + } + + /// 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. + pub(super) 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, criteria: None, lambda: None }; + 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) + } + + pub(super) 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(), + Some(Err(e)) => return err(e), + None => return err(CellError::Value), + }; + let area = match args.get(1).map(|a| self.eval(a)) { + Some(Ev::R(a)) => a.first().copied(), + None => Some(Area { sheet: self.sheet, range: RangeRef::cell(self.at) }), + _ => return err(CellError::Value), + }; + let Some(a) = area else { return err(CellError::Ref) }; + let c = a.range.start; + let wb = self.host.workbook(); + let sheet = wb.sheet(a.sheet); + match kind.as_str() { + "address" => Ev::V(Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1))), + "row" => Ev::V(Value::number(c.row as f64 + 1.0)), + "col" => Ev::V(Value::number(c.col as f64 + 1.0)), + "contents" => Ev::V(self.host.cell_value(a.sheet, c)), + "type" => { + let v = self.host.cell_value(a.sheet, c); + Ev::V(Value::text(match v { + Value::Empty => "b", + Value::Text(_) => "l", + _ => "v", + })) + } + "width" => Ev::V(Value::number(sheet.map_or(8.43, |s| gridcraft_model::points_to_chars(s.col_width(c.col) as f64)).round())), + // Excel's `/[]`, which the classic sheet-name formula splits on + // the `]`. Empty until the workbook has a file of its own, as in Excel (#1489). + "filename" => Ev::V(Value::text(match &wb.source_path { + Some(file) => { + let name = file.file_name().unwrap_or_default().to_string_lossy(); + let on = sheet.map_or("", |s| s.name.as_str()); + file.parent().unwrap_or(std::path::Path::new("")).join(format!("[{name}]{on}")).to_string_lossy().into_owned() + } + None => String::new(), + })), + "sheetname" => Ev::V(Value::text(sheet.map(|s| s.name.clone()).unwrap_or_default())), + "protect" => Ev::V(Value::number(if sheet.is_some_and(|s| wb.styles.get(s.style_id(c)).protection.locked) { 1.0 } else { 0.0 })), + "format" => { + let code = sheet.map(|s| wb.styles.get(s.style_id(c)).num_fmt.as_str().to_string()).unwrap_or_default(); + Ev::V(Value::text(cell_format_code(&code))) + } + "color" | "parentheses" => Ev::V(Value::Number(0.0)), + "prefix" => Ev::V(Value::text("")), + _ => err(CellError::Value), + } + } + + pub(super) fn subtotal(&mut self, name: &str, args: &[Expr]) -> Ev { + // SUBTOTAL needs function_num and ref1; AGGREGATE needs function_num, options and ref1. + let min_args = if name == "AGGREGATE" { 3 } else { 2 }; + if args.len() < min_args { + return err(CellError::Value); + } + let code = match self.arg_num(args, 0, 0.0) { + Ok(n) => n.trunc() as u32, + Err(e) => return err(e), + }; + let (func, options, rest) = if name == "AGGREGATE" { + let opt = match self.arg_num(args, 1, 0.0) { + Ok(n) => n.trunc() as u32, + Err(e) => return err(e), + }; + (code, opt, &args[2..]) + } else { + let (f, ignore_hidden) = if code > 100 { (code - 100, true) } else { (code, false) }; + (f, if ignore_hidden { 5 } else { 0 }, &args[1..]) + }; + if !(1..=19).contains(&func) || (name == "SUBTOTAL" && func > 11) { + return err(CellError::Value); + } + let skip_hidden = matches!(options, 1 | 3 | 5 | 7); + // SUBTOTAL leaves out rows a filter hides whatever the function number (Microsoft's + // SUBTOTAL documentation); 101-111 also leave out rows hidden by hand. + let skip_filtered = name == "SUBTOTAL"; + let skip_errors = matches!(options, 2 | 3 | 6 | 7); + // SUBTOTAL always leaves out nested SUBTOTAL/AGGREGATE cells (101-111 too); AGGREGATE does so for options 0-3. + let skip_nested = name == "SUBTOTAL" || options <= 3; + let mut values = Vec::new(); + let mut k = None; + let refs = if name == "AGGREGATE" && func >= 14 { &rest[..1.min(rest.len())] } else { rest }; + if name == "AGGREGATE" && func >= 14 { + k = rest.get(1).map(|e| self.value(e)); + } + // Every cell visited (even one skipped as hidden, nested or an error) and every value + // counts against one budget for the whole call. All arguments are evaluated (references + // stay lazy) and charged before any cell is read: a value stored in XFD1048576 makes + // `A:XFD` trim to the whole sheet, ~17 billion cells (#2175). + enum Input { + Cells(usize, RangeRef), + Values(Value), + } + let mut inputs = Vec::new(); + let mut budget = MAX_CELLS; + for e in refs { + match self.eval(e) { + Ev::R(areas) => { + for a in areas { + let range = self.trim(&a); + budget = match budget.checked_sub(range.count()) { + Some(left) => left, + None => return err(CellError::Num), + }; + inputs.push(Input::Cells(a.sheet, range)); + } + } + Ev::V(v) => { + let n = match &v { + Value::Array(a) => u64::try_from(a.data.len()).unwrap_or(u64::MAX), + _ => 1, + }; + budget = match budget.checked_sub(n) { + Some(left) => left, + None => return err(CellError::Num), + }; + inputs.push(Input::Values(v)); + } + Ev::L(_) => return err(CellError::Value), + } + } + for input in inputs { + match input { + Input::Cells(sheet, range) => { + for c in range.iter() { + let (hidden, nested) = { + let wb = self.host.workbook(); + let s = wb.sheet(sheet); + let hidden = + s.is_some_and(|s| if skip_hidden { s.is_row_hidden(c.row) } else { skip_filtered && s.is_row_filtered(c.row) }); + let nested = skip_nested + && s.and_then(|s| s.cell(c)).and_then(|cell| cell.formula.as_ref()).is_some_and(|f| { + let u = f.text.to_ascii_uppercase(); + u.contains("SUBTOTAL(") || u.contains("AGGREGATE(") + }); + (hidden, nested) + }; + if hidden || nested { + continue; + } + let v = self.host.cell_value(sheet, c); + if skip_errors && v.is_error() { + continue; + } + values.push(v); + } + } + Input::Values(Value::Array(a)) => values.extend(a.data.iter().filter(|v| !(skip_errors && v.is_error())).cloned()), + Input::Values(v) => values.push(v), + } + } + if let Some(k) = k { + return Ev::V(gridcraft_functions::aggregate_values_k(func, &values, &k)); + } + Ev::V(gridcraft_functions::aggregate_values(func, &values)) + } +} diff --git a/crates/calc/src/eval/special.rs b/crates/calc/src/eval/special.rs new file mode 100644 index 000000000..f68badbfc --- /dev/null +++ b/crates/calc/src/eval/special.rs @@ -0,0 +1,167 @@ +use super::*; + +/// SUMIF and AVERAGEIF with a sum range: Excel sizes the sum range like the criteria range. +pub(super) fn resizes_sum_range(name: &str, args: usize) -> bool { + args == 3 && matches!(name, "SUMIF" | "AVERAGEIF") +} + +/// Height and width of a single-area reference or an array argument. +pub(super) fn ev_shape(ev: &Ev) -> Option<(u32, u32)> { + match ev { + Ev::R(areas) => match areas.as_slice() { + [a] => Some((a.range.height(), a.range.width())), + _ => None, + }, + Ev::V(Value::Array(a)) => Some((u32::try_from(a.rows).unwrap_or(u32::MAX), u32::try_from(a.cols).unwrap_or(u32::MAX))), + Ev::V(_) => Some((1, 1)), + Ev::L(_) => None, + } +} + +/// The range SUMIF/AVERAGEIF add up: from the sum range's top-left cell, as many rows and columns +/// as the criteria range (`height` × `width`), cut off at the sheet's edge. +pub fn resized_sum_range(sum: RangeRef, height: u32, width: u32) -> RangeRef { + let row = sum.start.row.saturating_add(height.saturating_sub(1)).min(MAX_ROWS - 1); + let col = sum.start.col.saturating_add(width.saturating_sub(1)).min(MAX_COLS - 1); + RangeRef::new(sum.start, CellRef::new(row, col)) +} + +/// Collects the static references of an expression with their sheets resolved; `dynamic` is +/// set when the formula uses INDIRECT/OFFSET/names/tables whose targets can change. +pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec, bool) { + let mut out = Vec::new(); + let mut dynamic = false; + let push = |out: &mut Vec, r: &Reference, range: RangeRef| match &r.sheet { + SheetSel::Current => out.push(Area { sheet, range }), + SheetSel::Named(n) => { + if let Some(i) = wb.sheet_index(n) { + out.push(Area { sheet: i, range }); + } + } + SheetSel::Span(a, b) => { + if let (Some(i), Some(j)) = (wb.sheet_index(a), wb.sheet_index(b)) { + for s in i.min(j)..=i.max(j) { + out.push(Area { sheet: s, range }); + } + } + } + }; + e.walk(&mut |x| match x { + Expr::Ref(r) => push(&mut out, r, r.range()), + // The cells SUMIF/AVERAGEIF really read from the sum range (see `resized_sum_range`); when + // the criteria range's size isn't known statically, recalculate the formula every time. + Expr::Call(n, args) if resizes_sum_range(n, args.len()) => match (args.first(), args.get(2)) { + (Some(Expr::Ref(c)), Some(Expr::Ref(s))) => { + let (crit, sum) = (c.range(), s.range()); + push(&mut out, s, resized_sum_range(sum, crit.height(), crit.width())); + } + (_, Some(Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) | Expr::Error(_) | Expr::Array(_) | Expr::Missing)) => {} + _ => dynamic = true, + }, + Expr::Call(n, _) + if matches!(n.as_str(), "INDIRECT" | "OFFSET" | "NOW" | "TODAY" | "RAND" | "RANDBETWEEN" | "RANDARRAY" | "CELL" | "INFO") => + { + dynamic = true + } + Expr::Name(_) | Expr::Struct(_) => dynamic = true, + Expr::Call(n, _) if gridcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true, + // A range ending in a function's result (`A1:INDEX(B1:B9,n)`) covers the cells between + // the references inside it, not only those references. + Expr::Binary(BinOp::Range, a, b) if !matches!((&**a, &**b), (Expr::Ref(_), Expr::Ref(_))) => { + let mut inner = precedents(wb, sheet, a).0; + inner.extend(precedents(wb, sheet, b).0); + let mut boxes: Vec = Vec::new(); + for area in inner { + match boxes.iter_mut().find(|x| x.sheet == area.sheet) { + Some(x) => x.range = x.range.union(&area.range), + None => boxes.push(area), + } + } + out.extend(boxes); + } + _ => {} + }); + (out, dynamic) +} + +fn is_special(n: &str) -> bool { + matches!( + n, + "IF" | "IFS" + | "IFERROR" + | "IFNA" + | "CHOOSE" + | "SWITCH" + | "ROW" + | "COLUMN" + | "ROWS" + | "COLUMNS" + | "AREAS" + | "ISREF" + | "ISFORMULA" + | "FORMULATEXT" + | "SHEET" + | "SHEETS" + | "OFFSET" + | "INDIRECT" + | "INDEX" + | "CELL" + | "TRIMRANGE" + | "SUBTOTAL" + | "AGGREGATE" + | "TEXT" + | "LET" + | "LAMBDA" + | "ISOMITTED" + | "MAP" + | "REDUCE" + | "SCAN" + | "BYROW" + | "BYCOL" + | "MAKEARRAY" + ) +} + +/// Names of functions the evaluator implements itself (merged into the function list for UI). +pub const SPECIAL_FUNCTIONS: &[&str] = &[ + "IF", + "IFS", + "IFERROR", + "IFNA", + "CHOOSE", + "SWITCH", + "ROW", + "COLUMN", + "ROWS", + "COLUMNS", + "AREAS", + "ISREF", + "ISFORMULA", + "FORMULATEXT", + "SHEET", + "SHEETS", + "OFFSET", + "INDIRECT", + "INDEX", + "CELL", + "TRIMRANGE", + "SUBTOTAL", + "AGGREGATE", + "TEXT", + "LET", + "LAMBDA", + "ISOMITTED", + "MAP", + "REDUCE", + "SCAN", + "BYROW", + "BYCOL", + "MAKEARRAY", +]; + +pub fn is_known_function(n: &str) -> bool { + is_special(&n.to_ascii_uppercase()) || gridcraft_functions::lookup(n).is_some() +} + +#[allow(dead_code)] +fn unused(_: RefKind) {}