use super::format::{cell_format_code, r1c1_to_a1}; use super::{Area, Arg, Array, CellError, Ev, Evaluator, Expr, FnCtx, MAX_CELLS, Value, err}; use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef}; use std::sync::Arc; 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)) } }