Skip to main content

visi_core/core/engine/sheet/
mod.rs

1mod edit;
2mod functions;
3
4pub use edit::get_word_boundaries_from_str;
5
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet, VecDeque};
8use web_time::Instant;
9
10use super::cell::{
11    CellRef, CellType, Dependency, EngineError, EvalError, TextCellRef, generate_unique_id,
12};
13use super::column::DataColumn;
14use super::result_data::ResultData;
15/// Context for evaluating expressions, containing references to other sheets
16#[derive(Default)]
17pub struct Context<'a> {
18    /// Map of sheet names to sheet references for cross-sheet lookups
19    pub sheets: HashMap<String, &'a Sheet>,
20    /// Every pivot table in the workbook, so `GETPIVOTDATA` can resolve a
21    /// rendered pivot's destination cell back to its definition. Pivot
22    /// tables are workbook-level (like `Context.sheets`' cross-sheet
23    /// lookups), not sheet-scoped, so this lives here rather than on
24    /// `Sheet` itself.
25    pub pivot_tables: &'a [crate::core::pivot::PivotTable],
26    /// Sheet names in true workbook order, so `SHEET()` can report a real
27    /// ordinal. `sheets` is an unordered `HashMap`, which is why this is
28    /// tracked separately rather than derived from it -- true order only
29    /// exists one layer up, in `visi`'s `WorkbookManager::sheets` (a
30    /// `Vec`), which populates this when building the context.
31    pub sheet_order: Vec<String>,
32}
33
34impl<'a> Context<'a> {
35    /// Create a new empty context
36    pub fn new() -> Self {
37        Self {
38            sheets: HashMap::new(),
39            pivot_tables: &[],
40            sheet_order: Vec::new(),
41        }
42    }
43
44    /// Add a sheet to the context for lookup during evaluation
45    pub fn add_table(&mut self, name: String, sheet: &'a Sheet) {
46        self.sheets.insert(name, sheet);
47    }
48}
49
50/// A chain of LET name/value bindings in scope while evaluating a single
51/// formula. This is a linked list (not a cloned `HashMap`) because LET
52/// binds names one at a time -- each value expression, and the final
53/// calculation, must see all *earlier* bindings from the same LET (and any
54/// outer LET it's nested inside), and a name can shadow an outer binding of
55/// the same spelling. `evaluate_let` builds this chain by recursing one
56/// pair at a time rather than mutating a shared map.
57enum LetScope<'a> {
58    Empty,
59    Bound {
60        name: &'a str,
61        value: &'a ResultData,
62        parent: &'a LetScope<'a>,
63    },
64}
65
66impl<'a> LetScope<'a> {
67    fn get(&self, name: &str) -> Option<&ResultData> {
68        match self {
69            LetScope::Empty => None,
70            LetScope::Bound {
71                name: n,
72                value,
73                parent,
74            } => {
75                if n.eq_ignore_ascii_case(name) {
76                    Some(value)
77                } else {
78                    parent.get(name)
79                }
80            }
81        }
82    }
83}
84
85/// Which way a fill or selection extends from its anchor cell.
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum Direction {
88    /// No direction; the operation is a no-op.
89    None,
90    /// Toward row 0.
91    Up,
92    /// Toward the last row.
93    Down,
94    /// Toward column 0.
95    Left,
96    /// Toward the last column.
97    Right,
98}
99
100/// One worksheet: a grid of cells, the formulas over them, and the dependency
101/// graph that keeps them up to date.
102///
103/// # Coordinates
104///
105/// Everything here is **0-based `(row, col)`**. A1 notation exists only at the
106/// parser and CLI boundaries -- see [`parse_a1_coordinates`] and
107/// [`col_idx_to_letters`] to convert.
108///
109/// # Naming trap
110///
111/// A `Sheet` is informally called a "table" in places (a new one is named
112/// `table_1`, and `Context::add_table` registers one). That is *not* an
113/// [`ExcelTable`], which is a ListObject -- a named rectangular range *on* a
114/// sheet -- and lives in [`Sheet::tables`].
115///
116/// # Storage
117///
118/// Storage is column-oriented: each [`DataColumn`] keeps the raw user text,
119/// the computed values and the compiled formulas in three parallel vectors
120/// that must stay the same length. The row and column insert/delete paths
121/// maintain that invariant by hand, so a new one has to do the same.
122///
123/// # Recalculation
124///
125/// [`Sheet::commit`] recomputes the dirty cells and propagates through
126/// [`Dependency::Local`] and [`Dependency::LocalColumn`] edges only.
127/// Cross-sheet edges are `WorkbookManager::evaluate`'s job, and evaluating a
128/// formula with a remote reference requires a [`Context`] -- without one it
129/// errors.
130///
131/// [`parse_a1_coordinates`]: crate::core::parse_a1_coordinates
132/// [`col_idx_to_letters`]: crate::core::col_idx_to_letters
133/// [`ExcelTable`]: crate::core::table::ExcelTable
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Sheet {
136    /// Workbook-unique identifier. Formulas compile references against this
137    /// rather than the name, which is what makes a rename non-destructive.
138    #[serde(default = "generate_unique_id")]
139    pub id: u64,
140    /// Display name, as it appears in a cross-sheet reference.
141    pub name: String,
142    /// The cells, one entry per column. Row `r` of column `c` is
143    /// `columns[c]`'s entry `r`.
144    ///
145    /// Every column has the same number of rows -- [`Sheet::row_count`] reads
146    /// only the first and assumes the rest match -- so the `Vec` itself is
147    /// crate-private. Read them through [`Sheet::columns`].
148    pub(crate) columns: Vec<DataColumn>,
149    /// [AI-Agent] Excel/OpenXML row heights in point units, aligned with sheet rows.
150    #[serde(default)]
151    pub(crate) row_heights: Vec<Option<f64>>,
152    /// Excel Tables (ListObjects) defined on this sheet.
153    #[serde(default)]
154    pub tables: Vec<crate::core::table::ExcelTable>,
155    /// Forward edges: which cells must be recomputed when a dependency
156    /// changes. Rebuilt from the formulas, so not serialized.
157    #[serde(skip, default)]
158    pub dependencies: HashMap<Dependency, HashSet<CellRef>>,
159    /// Reverse edges: what each cell currently reads, so its old edges can be
160    /// dropped when its formula changes. Rebuilt, so not serialized.
161    #[serde(skip, default)]
162    pub dependencies_rev: HashMap<CellRef, HashSet<Dependency>>,
163    /// Edits made since the last commit, for callers that want to observe or
164    /// replay them.
165    #[serde(skip)]
166    pub uncommitted_actions: Vec<crate::core::SheetAction>,
167    /// Regional locale for date and number parsing.
168    #[serde(default)]
169    pub locale: crate::core::locale::Locale,
170}
171
172/// Arguments for [`Sheet::new`]. [`Default`] gives a 10x5 sheet with a
173/// generated id and the name `table_1`.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SheetInit {
176    /// Identifier to use; `None` generates a fresh one.
177    #[serde(default)]
178    pub id: Option<u64>,
179    /// Name to use; `None` means `table_1`.
180    pub name: Option<String>,
181    /// Rows to allocate.
182    pub rows: usize,
183    /// Columns to allocate.
184    pub cols: usize,
185}
186
187impl Default for SheetInit {
188    fn default() -> Self {
189        Self {
190            id: None,
191            name: None,
192            rows: 10,
193            cols: 5,
194        }
195    }
196}
197
198/// How a blank cell is treated by the strict numeric flatteners.
199#[derive(Clone, Copy, PartialEq, Eq)]
200enum BlankPolicy {
201    /// Counts as 0 (MULTINOMIAL).
202    Zero,
203    /// Dropped entirely, shifting later elements (SERIESSUM).
204    Skip,
205    /// #VALUE!, like text (LINEST/TREND/GROWTH/LOGEST/MMULT).
206    Reject,
207}
208
209impl Sheet {
210    /// Creates a sheet of `args.rows` x `args.cols` empty cells, every one of
211    /// them queued as a pending edit so the first [`Sheet::commit`] sees them.
212    pub fn new(args: SheetInit) -> Sheet {
213        let SheetInit {
214            id,
215            name,
216            rows,
217            cols,
218        } = args;
219        let sheet_id = id.unwrap_or_else(generate_unique_id);
220        let sheet_name = name.unwrap_or_else(|| "table_1".to_string());
221
222        let mut columns = Vec::with_capacity(cols);
223        for _ in 0..cols {
224            columns.push(DataColumn::new(rows));
225        }
226
227        let mut uncommitted_actions = Vec::new();
228        for c in 0..cols {
229            for r in 0..rows {
230                uncommitted_actions.push(crate::core::SheetAction::SetCellSrc {
231                    sheet_name: sheet_name.clone(),
232                    col: c,
233                    row: r,
234                    src: String::new(),
235                });
236            }
237        }
238
239        Self {
240            id: sheet_id,
241            name: sheet_name,
242            columns,
243            row_heights: vec![None; rows],
244            tables: Vec::new(),
245            dependencies: HashMap::new(),
246            dependencies_rev: HashMap::new(),
247            uncommitted_actions,
248            locale: crate::core::locale::Locale::default(),
249        }
250    }
251
252    /// Rebuilds what serialization drops.
253    ///
254    /// Only the raw source text is persisted, so this resizes the value and
255    /// compiled-formula vectors back to match it -- restoring the
256    /// same-length invariant -- and marks everything dirty. Call it after
257    /// deserializing, before [`Sheet::commit`].
258    pub fn setup_after_deserialization(&mut self) {
259        for col in &mut self.columns {
260            col.rebuild_after_load();
261        }
262        let row_count = self.row_count();
263        self.row_heights.resize(row_count, None);
264        self.mark_all_dirty();
265    }
266
267    /// Every sheet a formula on this one could refer to -- this sheet first,
268    /// then the rest of `context` -- as the name-to-id lookup table that
269    /// `compile_formula` resolves references against.
270    pub(crate) fn get_all_sheets_for_compilation(&self, context: Option<&Context>) -> Vec<Sheet> {
271        let mut list = vec![self.clone()];
272        let mut seen = std::collections::HashSet::new();
273        seen.insert(self.id);
274        if let Some(ctx) = context {
275            for sheet in ctx.sheets.values() {
276                if !seen.contains(&sheet.id) {
277                    seen.insert(sheet.id);
278                    list.push((*sheet).clone());
279                }
280            }
281        }
282        list
283    }
284
285    /// Queues every cell for recomputation on the next [`Sheet::commit`].
286    ///
287    /// This is how cross-sheet staleness is handled: `WorkbookManager` cannot
288    /// tell which cells a remote edit reached, so it marks whole sheets.
289    pub fn mark_all_dirty(&mut self) {
290        for col in &mut self.columns {
291            col.dirty_indices.clear();
292            col.dirty_indices.extend(0..col.src.len());
293        }
294    }
295
296    /// Commit all changed src items with a context for sheet lookups
297    pub fn commit(&mut self, context: Option<&Context>) -> Result<HashSet<CellRef>, EngineError> {
298        let mut queue: VecDeque<CellRef> = VecDeque::new();
299        let mut queue_set: HashSet<CellRef> = HashSet::new();
300        let mut updated_cells: HashSet<CellRef> = HashSet::new();
301
302        for (col_idx, col_data) in self.columns.iter_mut().enumerate() {
303            for row_idx in &col_data.dirty_indices {
304                let cell = CellRef::new(*row_idx, col_idx);
305                queue.push_back(cell);
306                queue_set.insert(cell);
307                updated_cells.insert(cell);
308            }
309            col_data.dirty_indices.clear();
310        }
311
312        let initial_queue_len = queue.len();
313        if initial_queue_len == 0 {
314            return Ok(updated_cells);
315        }
316
317        let start_commit = Instant::now();
318        log::info!(
319            "Sheet '{}' commit starting for {} dirty cells",
320            self.name,
321            initial_queue_len
322        );
323        let max_ops = 10000.max(initial_queue_len * 3);
324        let mut ops = 0;
325
326        let mut sheets_for_compilation = self.get_all_sheets_for_compilation(context);
327        let mut last_log_time = Instant::now();
328
329        while let Some(cell_ref) = queue.pop_front() {
330            queue_set.remove(&cell_ref);
331            ops += 1;
332            if ops > max_ops {
333                println!("Circular dependency or too many updates detected");
334                break;
335            }
336
337            if ops % 50000 == 0 {
338                log::info!(
339                    "Sheet '{}' commit progress: {}/{} cells processed ({:.2?})",
340                    self.name,
341                    ops,
342                    initial_queue_len,
343                    last_log_time.elapsed()
344                );
345                last_log_time = Instant::now();
346            }
347
348            let cell_type_hint = self
349                .columns
350                .get(cell_ref.col)
351                .and_then(|c| c.cell_types.get(cell_ref.row).copied())
352                .unwrap_or(CellType::Auto);
353
354            let mut detected_num_format: Option<String> = None;
355            let (result, new_deps, compiled_to_cache, final_cell_type) = {
356                let src = self.get_src_str_ref(&cell_ref).unwrap_or("");
357                if cell_type_hint == CellType::String {
358                    let val = if src.starts_with('"') && src.ends_with('"') && src.len() >= 2 {
359                        src[1..src.len() - 1].to_string()
360                    } else {
361                        src.to_string()
362                    };
363                    (ResultData::String(val), vec![], None, CellType::String)
364                } else if !src.starts_with('=') && cell_type_hint != CellType::Formula {
365                    let (res, c_type) = if let Some(stripped) = src.strip_prefix('\'') {
366                        (ResultData::String(stripped.to_string()), CellType::String)
367                    } else if src.is_empty() {
368                        (ResultData::None, CellType::Empty)
369                    } else if src.starts_with('"') && src.ends_with('"') && src.len() >= 2 {
370                        (
371                            ResultData::String(src[1..src.len() - 1].to_string()),
372                            CellType::String,
373                        )
374                    } else if let Ok(i) = src.trim().parse::<i64>() {
375                        (ResultData::Integer(i), CellType::Number)
376                    } else if let Ok(f) = src.trim().parse::<f64>()
377                        && f.is_finite()
378                    {
379                        (ResultData::Float(f), CellType::Number)
380                    } else if crate::core::engine::result_data::is_excel_error_code(src) {
381                        (ResultData::Error(src.to_uppercase()), CellType::Error)
382                    } else if src.eq_ignore_ascii_case("true") {
383                        (ResultData::Boolean(true), CellType::Boolean)
384                    } else if src.eq_ignore_ascii_case("false") {
385                        (ResultData::Boolean(false), CellType::Boolean)
386                    } else if let Some((date, format)) = crate::core::date::parse_date_with_locale(
387                        src.trim_matches(' '),
388                        &self.locale,
389                    ) {
390                        detected_num_format = Some(format.to_format_code());
391                        (
392                            ResultData::Float(crate::core::date::date_to_excel_serial(date)),
393                            CellType::Number,
394                        )
395                    } else if let Some(f) = crate::core::date_fn::parse_time_fraction(src) {
396                        (ResultData::Float(f), CellType::Number)
397                    } else {
398                        (ResultData::String(src.to_string()), CellType::String)
399                    };
400                    (res, vec![], None, c_type)
401                } else {
402                    let compiled =
403                        crate::core::parser::compile_formula(src, &sheets_for_compilation);
404                    let eval_src =
405                        crate::core::parser::serialize_formula(&compiled, &sheets_for_compilation);
406                    let (res, deps) = match self.eval_with_row(
407                        &eval_src,
408                        context,
409                        Some(cell_ref.row),
410                        Some(cell_ref.col),
411                    ) {
412                        Ok(r) => r,
413                        Err(e) => (ResultData::Error(e.to_string()), vec![]),
414                    };
415                    let final_res = if let ResultData::None = res {
416                        ResultData::Float(0.0)
417                    } else {
418                        res
419                    };
420                    (final_res, deps, Some(compiled), CellType::Formula)
421                }
422            };
423
424            if let Some(src_str) = self.get_src_str_ref(&cell_ref)
425                && let Some(stripped) = src_str.strip_prefix('\'')
426            {
427                let stripped_str = stripped.to_string();
428                if let Some(col) = self.columns.get_mut(cell_ref.col)
429                    && cell_ref.row < col.src.len()
430                {
431                    col.src[cell_ref.row] = stripped_str;
432                }
433            }
434
435            if let Some(col) = self.columns.get_mut(cell_ref.col)
436                && cell_ref.row < col.compiled_src.len()
437            {
438                col.compiled_src[cell_ref.row] = compiled_to_cache.unwrap_or_default();
439            }
440
441            if let Some(old_deps) = self.dependencies_rev.remove(&cell_ref) {
442                for provider in old_deps {
443                    if let Some(dependents) = self.dependencies.get_mut(&provider) {
444                        dependents.remove(&cell_ref);
445                    }
446                }
447            }
448
449            if !new_deps.is_empty() {
450                let mut new_deps_set = HashSet::new();
451                for provider in new_deps {
452                    new_deps_set.insert(provider.clone());
453                    self.dependencies
454                        .entry(provider)
455                        .or_default()
456                        .insert(cell_ref);
457                }
458                self.dependencies_rev.insert(cell_ref, new_deps_set);
459            }
460
461            let inherited = if detected_num_format.is_some()
462                || !matches!(result, ResultData::Float(_) | ResultData::Integer(_))
463            {
464                None
465            } else {
466                self.get_src_str_ref(&cell_ref)
467                    .and_then(|src| src.strip_prefix('='))
468                    .and_then(|body| crate::core::parser::parse_excel_formula(body).ok())
469                    .and_then(|ast| self.inherited_date_format(&ast))
470            };
471            if let Some(code) = detected_num_format.or(inherited) {
472                let existing = self
473                    .get_cell_style(cell_ref.row, cell_ref.col)
474                    .and_then(|s| s.num_format.clone());
475                if existing.is_none() {
476                    self.update_cell_style(cell_ref.row, cell_ref.col, |style| {
477                        style.num_format = Some(code);
478                    });
479                }
480            }
481
482            if let Some(col) = self.columns.get_mut(cell_ref.col)
483                && cell_ref.row < col.data.len()
484            {
485                col.cell_types[cell_ref.row] = final_cell_type;
486                col.data.set(cell_ref.row, result.clone());
487                updated_cells.insert(cell_ref);
488            }
489            if let Some(comp_sheet) = sheets_for_compilation
490                .iter_mut()
491                .find(|s| s.name == self.name)
492                && let Some(col) = comp_sheet.columns.get_mut(cell_ref.col)
493                && cell_ref.row < col.data.len()
494            {
495                col.cell_types[cell_ref.row] = final_cell_type;
496                col.data.set(cell_ref.row, result);
497            }
498
499            let local_dep_key = Dependency::Local(cell_ref);
500            if let Some(dependents) = self.dependencies.get(&local_dep_key) {
501                for dependent in dependents {
502                    if !queue_set.contains(dependent) {
503                        queue.push_back(*dependent);
504                        queue_set.insert(*dependent);
505                    }
506                }
507            }
508
509            let local_col_dep_key = Dependency::LocalColumn(cell_ref.col);
510            if let Some(dependents) = self.dependencies.get(&local_col_dep_key) {
511                for dependent in dependents {
512                    if !queue_set.contains(dependent) {
513                        queue.push_back(*dependent);
514                        queue_set.insert(*dependent);
515                    }
516                }
517            }
518        }
519        if initial_queue_len > 0 {
520            log::info!(
521                "Sheet '{}' commit finished. Processed {} cell updates. Total time: {:.2?}",
522                self.name,
523                ops,
524                start_commit.elapsed()
525            );
526        }
527        Ok(updated_cells)
528    }
529
530    /// Evaluates cell source text without storing it, as
531    /// [`Sheet::eval`] does, but from the point of view of `(row, col)`.
532    ///
533    /// The position is what makes relative constructs work -- a structured
534    /// reference like `[@Amount]` means "this row", so it needs to know which
535    /// row is asking. Pass `None` for both when there is no anchor.
536    ///
537    /// # Errors
538    ///
539    /// Returns an [`EngineError`] if the formula cannot be parsed. An *Excel*
540    /// error is not a Rust error: `=1/0` succeeds, returning
541    /// `ResultData::Error("#DIV/0!")`.
542    pub fn eval_with_row(
543        &self,
544        input: &str,
545        context: Option<&Context>,
546        row: Option<usize>,
547        col: Option<usize>,
548    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
549        if input.is_empty() {
550            return Ok((ResultData::None, vec![]));
551        }
552        if let Some(formula) = input.strip_prefix('=') {
553            self.eval_excel(formula, context, row, col)
554        } else {
555            if let Ok(i) = input.parse::<i64>() {
556                Ok((ResultData::Integer(i), vec![]))
557            } else if let Ok(f) = input.parse::<f64>() {
558                Ok((ResultData::Float(f), vec![]))
559            } else if let Ok(b) = input.parse::<bool>() {
560                Ok((ResultData::Boolean(b), vec![]))
561            } else {
562                Ok((ResultData::String(input.to_string()), vec![]))
563            }
564        }
565    }
566
567    /// Evaluates cell source text against this sheet without storing it,
568    /// returning the value and the references it read.
569    ///
570    /// Text with a leading `=` is a formula; anything else is parsed as a
571    /// literal. `context` supplies the other sheets, and is required for a
572    /// cross-sheet reference to resolve.
573    ///
574    /// # Errors
575    ///
576    /// Returns an [`EngineError`] if the formula cannot be parsed. An *Excel*
577    /// error is not a Rust error: `=1/0` succeeds, returning
578    /// `ResultData::Error("#DIV/0!")`.
579    pub fn eval(
580        &self,
581        input: &str,
582        context: Option<&Context>,
583    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
584        self.eval_with_row(input, context, None, None)
585    }
586
587    fn eval_excel(
588        &self,
589        code: &str,
590        context: Option<&Context>,
591        row: Option<usize>,
592        col: Option<usize>,
593    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
594        let ast = crate::core::parser::parse_excel_formula(code)
595            .map_err(|e| EngineError::EvalError(EvalError::UnknownFunction(e)))?;
596
597        let mut deps = Vec::new();
598        let result = match self.evaluate_ast(&ast, context, row, col, &mut deps, &LetScope::Empty) {
599            Ok(r) => r,
600            Err(EngineError::EvalError(EvalError::UnknownFunction(err_str)))
601                if err_str.starts_with('#') =>
602            {
603                ResultData::Error(err_str)
604            }
605            Err(e) => return Err(e),
606        };
607        Ok((result, deps))
608    }
609
610    fn evaluate_ast(
611        &self,
612        ast: &crate::core::parser::Expr,
613        context: Option<&Context>,
614        row: Option<usize>,
615        col: Option<usize>,
616        deps: &mut Vec<Dependency>,
617        scope: &LetScope<'_>,
618    ) -> Result<ResultData, EngineError> {
619        use crate::core::SheetSection;
620        use crate::core::parser::Expr;
621        use crate::core::parser::Op;
622
623        match ast {
624            Expr::Number(n) => Ok(ResultData::Float(*n)),
625            Expr::String(s) => Ok(ResultData::String(s.clone())),
626            Expr::Boolean(b) => Ok(ResultData::Boolean(*b)),
627            Expr::Error(code) => Ok(ResultData::Error(code.to_string())),
628            Expr::Identifier(name) => match scope.get(name) {
629                Some(val) => Ok(val.clone()),
630                None => Ok(ResultData::Error("#NAME?".to_string())),
631            },
632            Expr::StructuredRef {
633                sheet,
634                column,
635                is_this_row,
636                section,
637            } => {
638                let ref_name = match sheet {
639                    Some(name) => name.clone(),
640                    None => self.name.clone(),
641                };
642
643                let mut found: Option<(&Sheet, &crate::core::table::ExcelTable)> =
644                    self.find_table(&ref_name).map(|t| (self, t));
645                if found.is_none()
646                    && let Some(ctx) = context
647                {
648                    for s in ctx.sheets.values() {
649                        if let Some(t) = s.find_table(&ref_name) {
650                            found = Some((s, t));
651                            break;
652                        }
653                    }
654                }
655
656                if let Some((table_sheet, excel_table)) = found {
657                    let is_self = table_sheet.name == self.name;
658                    let sheet_name = table_sheet.name.clone();
659
660                    let col_indices: Vec<(usize, usize)> = if let Some(col_name) = column {
661                        let local = excel_table.local_column_index(col_name).ok_or_else(|| {
662                            EngineError::EvalError(EvalError::UnknownFunction(format!(
663                                "Column not found: {}",
664                                col_name
665                            )))
666                        })?;
667                        vec![(local, excel_table.start_col + local)]
668                    } else {
669                        (0..excel_table.columns.len())
670                            .map(|local| (local, excel_table.start_col + local))
671                            .collect()
672                    };
673                    let is_whole_table = column.is_none();
674
675                    match section {
676                        SheetSection::Headers => {
677                            let names: Vec<ResultData> = col_indices
678                                .iter()
679                                .map(|&(local, _)| {
680                                    ResultData::String(
681                                        excel_table.columns.get(local).cloned().unwrap_or_default(),
682                                    )
683                                })
684                                .collect();
685                            if is_whole_table {
686                                Ok(ResultData::List(names))
687                            } else {
688                                Ok(names.into_iter().next().unwrap_or(ResultData::None))
689                            }
690                        }
691                        SheetSection::Totals => {
692                            if let Some(totals_row) = excel_table.totals_row() {
693                                let mut results = Vec::new();
694                                for &(_, col_idx) in &col_indices {
695                                    let cell_ref = CellRef::new(totals_row, col_idx);
696                                    if is_self {
697                                        deps.push(Dependency::Local(cell_ref));
698                                    } else {
699                                        deps.push(Dependency::Remote {
700                                            sheet: sheet_name.clone(),
701                                            cell: cell_ref,
702                                        });
703                                    }
704                                    results.push(table_sheet.get_result_data(&cell_ref));
705                                }
706                                if is_whole_table {
707                                    Ok(ResultData::List(results))
708                                } else {
709                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
710                                }
711                            } else {
712                                Ok(ResultData::None)
713                            }
714                        }
715                        SheetSection::Data | SheetSection::All => {
716                            if *is_this_row {
717                                let r = row.ok_or_else(|| {
718                                    EngineError::EvalError(EvalError::UnknownFunction(
719                                        "This row reference cannot be evaluated without row context"
720                                            .to_string(),
721                                    ))
722                                })?;
723                                let mut results = Vec::new();
724                                for &(_, col_idx) in &col_indices {
725                                    let cell_ref = CellRef::new(r, col_idx);
726                                    if is_self {
727                                        deps.push(Dependency::Local(cell_ref));
728                                    } else {
729                                        deps.push(Dependency::Remote {
730                                            sheet: sheet_name.clone(),
731                                            cell: cell_ref,
732                                        });
733                                    }
734                                    results.push(table_sheet.get_result_data(&cell_ref));
735                                }
736                                if is_whole_table {
737                                    Ok(ResultData::List(results))
738                                } else {
739                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
740                                }
741                            } else {
742                                let mut results = Vec::new();
743                                for &(_, col_idx) in &col_indices {
744                                    for r in
745                                        excel_table.data_start_row()..=excel_table.data_end_row()
746                                    {
747                                        let cell_ref = CellRef::new(r, col_idx);
748                                        if is_self {
749                                            deps.push(Dependency::Local(cell_ref));
750                                        } else {
751                                            deps.push(Dependency::Remote {
752                                                sheet: sheet_name.clone(),
753                                                cell: cell_ref,
754                                            });
755                                        }
756                                        results.push(table_sheet.get_result_data(&cell_ref));
757                                    }
758                                }
759                                Ok(ResultData::List(results))
760                            }
761                        }
762                    }
763                } else {
764                    let sheet_name = ref_name;
765                    let is_self = sheet_name == self.name;
766
767                    let target_sheet = if is_self {
768                        self
769                    } else if let Some(ctx) = context {
770                        if let Some(sheet) = ctx.sheets.get(&sheet_name) {
771                            sheet
772                        } else {
773                            return Err(EngineError::EvalError(EvalError::UnknownFunction(
774                                format!("Sheet not found: {}", sheet_name),
775                            )));
776                        }
777                    } else {
778                        return Err(EngineError::EvalError(EvalError::UnknownFunction(format!(
779                            "No context to resolve sheet reference: {}",
780                            sheet_name
781                        ))));
782                    };
783
784                    let col_indices: Vec<usize> = if let Some(col_name) = column {
785                        let pos = target_sheet
786                            .columns
787                            .iter()
788                            .position(|c| c.name == *col_name)
789                            .ok_or_else(|| {
790                                EngineError::EvalError(EvalError::UnknownFunction(format!(
791                                    "Column not found: {}",
792                                    col_name
793                                )))
794                            })?;
795                        vec![pos]
796                    } else {
797                        (0..target_sheet.columns.len()).collect()
798                    };
799                    let is_whole_table = column.is_none();
800
801                    match section {
802                        SheetSection::Headers => {
803                            let names: Vec<ResultData> = col_indices
804                                .iter()
805                                .map(|&idx| {
806                                    ResultData::String(
807                                        target_sheet
808                                            .columns
809                                            .get(idx)
810                                            .map(|c| c.name.clone())
811                                            .unwrap_or_default(),
812                                    )
813                                })
814                                .collect();
815                            if is_whole_table {
816                                Ok(ResultData::List(names))
817                            } else {
818                                Ok(names.into_iter().next().unwrap_or(ResultData::None))
819                            }
820                        }
821                        SheetSection::Totals => Ok(ResultData::None),
822                        SheetSection::Data | SheetSection::All => {
823                            if *is_this_row {
824                                let r = row.ok_or_else(|| {
825                                    EngineError::EvalError(EvalError::UnknownFunction(
826                                        "This row reference cannot be evaluated without row context"
827                                            .to_string(),
828                                    ))
829                                })?;
830                                let mut results = Vec::new();
831                                for &col_idx in &col_indices {
832                                    let cell_ref = CellRef::new(r, col_idx);
833                                    if is_self {
834                                        deps.push(Dependency::Local(cell_ref));
835                                    } else {
836                                        deps.push(Dependency::Remote {
837                                            sheet: sheet_name.clone(),
838                                            cell: cell_ref,
839                                        });
840                                    }
841                                    results.push(target_sheet.get_result_data(&cell_ref));
842                                }
843                                if is_whole_table {
844                                    Ok(ResultData::List(results))
845                                } else {
846                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
847                                }
848                            } else {
849                                let mut results = Vec::new();
850                                for &col_idx in &col_indices {
851                                    if is_self {
852                                        deps.push(Dependency::LocalColumn(col_idx));
853                                    } else {
854                                        deps.push(Dependency::RemoteColumn {
855                                            sheet: sheet_name.clone(),
856                                            col: col_idx,
857                                        });
858                                    }
859                                    for r in 0..target_sheet.row_count() {
860                                        let cell_ref = CellRef::new(r, col_idx);
861                                        results.push(target_sheet.get_result_data(&cell_ref));
862                                    }
863                                }
864                                Ok(ResultData::List(results))
865                            }
866                        }
867                    }
868                }
869            }
870            Expr::CellRef {
871                sheet,
872                row: r_val,
873                col,
874                ..
875            } => {
876                let cell_ref = CellRef::new(*r_val, *col);
877                let is_self = match sheet {
878                    Some(name) => name == &self.name,
879                    None => true,
880                };
881
882                if is_self {
883                    deps.push(Dependency::Local(cell_ref));
884                    Ok(self.get_result_data(&cell_ref))
885                } else {
886                    let name = sheet.as_ref().unwrap().clone();
887                    deps.push(Dependency::Remote {
888                        sheet: name.clone(),
889                        cell: cell_ref,
890                    });
891
892                    if let Some(ctx) = context {
893                        if let Some(t) = ctx.sheets.get(&name) {
894                            Ok(t.get_result_data(&cell_ref))
895                        } else {
896                            Err(EngineError::EvalError(EvalError::UnknownFunction(format!(
897                                "Sheet not found: {}",
898                                name
899                            ))))
900                        }
901                    } else {
902                        Err(EngineError::EvalError(EvalError::UnknownFunction(
903                            "No context to resolve sheet reference".to_string(),
904                        )))
905                    }
906                }
907            }
908            Expr::RangeRef {
909                sheet,
910                start_row,
911                start_col,
912                end_row,
913                end_col,
914                ..
915            } => {
916                let is_self = match sheet {
917                    Some(name) => name == &self.name,
918                    None => true,
919                };
920
921                let target_sheet = if is_self {
922                    Some(self)
923                } else {
924                    context.and_then(|ctx| ctx.sheets.get(sheet.as_ref().unwrap()).copied())
925                };
926
927                let actual_end_row = if *end_row == usize::MAX {
928                    target_sheet
929                        .map(|t| t.row_count().saturating_sub(1))
930                        .unwrap_or(0)
931                } else {
932                    *end_row
933                };
934                let actual_end_col = if *end_col == usize::MAX {
935                    target_sheet
936                        .map(|t| t.col_count().saturating_sub(1))
937                        .unwrap_or(0)
938                } else {
939                    *end_col
940                };
941
942                let is_col_range = *end_row == usize::MAX;
943
944                let mut seen_col_deps: HashSet<usize> = HashSet::new();
945
946                let mut results = Vec::new();
947                for r in *start_row..=actual_end_row {
948                    for c in *start_col..=actual_end_col {
949                        let cell_ref = CellRef::new(r, c);
950                        if is_self {
951                            if is_col_range {
952                                if seen_col_deps.insert(c) {
953                                    let col_dep = Dependency::LocalColumn(c);
954                                    if !deps.contains(&col_dep) {
955                                        deps.push(col_dep);
956                                    }
957                                }
958                            } else {
959                                deps.push(Dependency::Local(cell_ref));
960                            }
961                            if row == Some(r) && col == Some(c) {
962                                results.push(ResultData::None);
963                            } else {
964                                results.push(self.get_result_data(&cell_ref));
965                            }
966                        } else {
967                            let name = sheet.as_ref().unwrap().clone();
968                            if is_col_range {
969                                if seen_col_deps.insert(c) {
970                                    let col_dep = Dependency::RemoteColumn {
971                                        sheet: name.clone(),
972                                        col: c,
973                                    };
974                                    if !deps.contains(&col_dep) {
975                                        deps.push(col_dep);
976                                    }
977                                }
978                            } else {
979                                deps.push(Dependency::Remote {
980                                    sheet: name.clone(),
981                                    cell: cell_ref,
982                                });
983                            }
984                            if let Some(ctx) = context {
985                                if let Some(t) = ctx.sheets.get(&name) {
986                                    results.push(t.get_result_data(&cell_ref));
987                                } else {
988                                    return Err(EngineError::EvalError(
989                                        EvalError::UnknownFunction(format!(
990                                            "Sheet not found: {}",
991                                            name
992                                        )),
993                                    ));
994                                }
995                            } else {
996                                return Err(EngineError::EvalError(EvalError::UnknownFunction(
997                                    "No context to resolve sheet reference".to_string(),
998                                )));
999                            }
1000                        }
1001                    }
1002                }
1003                Ok(ResultData::List(results))
1004            }
1005            Expr::List(list) => {
1006                let mut results = Vec::new();
1007                for item in list {
1008                    results.push(self.evaluate_ast(item, context, row, col, deps, scope)?);
1009                }
1010                Ok(ResultData::List(results))
1011            }
1012            Expr::Slice { expr, start, end } => {
1013                let target_val = self.evaluate_ast(expr, context, row, col, deps, scope)?;
1014                if let ResultData::Error(_) = &target_val {
1015                    return Ok(target_val);
1016                }
1017                if let ResultData::List(list) = target_val {
1018                    let len = list.len() as isize;
1019                    let start_idx = if let Some(start_expr) = start {
1020                        let s_val =
1021                            self.evaluate_ast(start_expr, context, row, col, deps, scope)?;
1022                        if let ResultData::Error(_) = &s_val {
1023                            return Ok(s_val);
1024                        }
1025                        let s = self.to_f64(&s_val).unwrap_or(0.0) as isize;
1026                        if s < 0 {
1027                            (len + s).max(0) as usize
1028                        } else {
1029                            s.min(len) as usize
1030                        }
1031                    } else {
1032                        0
1033                    };
1034
1035                    let end_idx = if let Some(end_expr) = end {
1036                        let e_val = self.evaluate_ast(end_expr, context, row, col, deps, scope)?;
1037                        if let ResultData::Error(_) = &e_val {
1038                            return Ok(e_val);
1039                        }
1040                        let e = self.to_f64(&e_val).unwrap_or(len as f64) as isize;
1041                        if e < 0 {
1042                            (len + e).max(0) as usize
1043                        } else {
1044                            e.min(len) as usize
1045                        }
1046                    } else {
1047                        len as usize
1048                    };
1049
1050                    let sliced = if start_idx < end_idx && start_idx < list.len() {
1051                        list[start_idx..end_idx.min(list.len())].to_vec()
1052                    } else {
1053                        Vec::new()
1054                    };
1055                    Ok(ResultData::List(sliced))
1056                } else {
1057                    Ok(ResultData::None)
1058                }
1059            }
1060            Expr::UnaryOp { op, expr } => {
1061                let val = self.evaluate_ast(expr, context, row, col, deps, scope)?;
1062                match op {
1063                    Op::Sub => match val {
1064                        ResultData::Float(f) => Ok(ResultData::Float(-f)),
1065                        ResultData::Integer(i) => Ok(ResultData::Integer(-i)),
1066                        _ => Err(EngineError::EvalError(EvalError::UnknownFunction(
1067                            "Unary minus expects number".to_string(),
1068                        ))),
1069                    },
1070                    _ => Ok(val),
1071                }
1072            }
1073            Expr::BinaryOp { op, left, right } => {
1074                let l_val = self.evaluate_ast(left, context, row, col, deps, scope)?;
1075
1076                match op {
1077                    Op::Eq | Op::Ne | Op::Lt | Op::Gt | Op::Le | Op::Ge => {
1078                        if let ResultData::Error(_) = &l_val {
1079                            return Ok(l_val);
1080                        }
1081                        let r_val = self.evaluate_ast(right, context, row, col, deps, scope)?;
1082                        if let ResultData::Error(_) = &r_val {
1083                            return Ok(r_val);
1084                        }
1085                        let ord = Self::compare_excel_values(&l_val, &r_val);
1086                        let b = match op {
1087                            Op::Eq => ord.is_eq(),
1088                            Op::Ne => !ord.is_eq(),
1089                            Op::Lt => ord.is_lt(),
1090                            Op::Gt => ord.is_gt(),
1091                            Op::Le => ord.is_le(),
1092                            Op::Ge => ord.is_ge(),
1093                            _ => unreachable!(),
1094                        };
1095                        Ok(ResultData::Boolean(b))
1096                    }
1097                    _ => {
1098                        if let ResultData::Error(_) = &l_val {
1099                            return Ok(l_val);
1100                        }
1101                        let lf = match self.to_f64(&l_val) {
1102                            Some(f) => f,
1103                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
1104                        };
1105                        let r_val = self.evaluate_ast(right, context, row, col, deps, scope)?;
1106                        if let ResultData::Error(_) = &r_val {
1107                            return Ok(r_val);
1108                        }
1109                        let rf = match self.to_f64(&r_val) {
1110                            Some(f) => f,
1111                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
1112                        };
1113                        match op {
1114                            Op::Add => Ok(ResultData::Float(lf + rf)),
1115                            Op::Sub => Ok(ResultData::Float(lf - rf)),
1116                            Op::Mul => Ok(ResultData::Float(lf * rf)),
1117                            Op::Div => {
1118                                if rf == 0.0 {
1119                                    return Ok(ResultData::Error("#DIV/0!".to_string()));
1120                                }
1121                                Ok(ResultData::Float(lf / rf))
1122                            }
1123                            Op::Exp => {
1124                                if lf == 0.0 && rf == 0.0 {
1125                                    return Ok(ResultData::Error("#NUM!".to_string()));
1126                                }
1127                                if lf == 0.0 && rf < 0.0 {
1128                                    return Ok(ResultData::Error("#DIV/0!".to_string()));
1129                                }
1130                                if lf < 0.0 {
1131                                    if rf.fract() != 0.0 || rf.abs() > 1e6 {
1132                                        return Ok(ResultData::Error("#NUM!".to_string()));
1133                                    }
1134                                    let res = lf.powi(rf as i32);
1135                                    if res.is_nan() || res.is_infinite() {
1136                                        return Ok(ResultData::Error("#NUM!".to_string()));
1137                                    }
1138                                    return Ok(ResultData::Float(res));
1139                                }
1140                                let res = lf.powf(rf);
1141                                if res.is_nan() || res.is_infinite() {
1142                                    return Ok(ResultData::Error("#NUM!".to_string()));
1143                                }
1144                                Ok(ResultData::Float(res))
1145                            }
1146                            _ => unreachable!(),
1147                        }
1148                    }
1149                }
1150            }
1151            Expr::FunctionCall { name, args } => {
1152                self.evaluate_function(name, args, context, row, col, deps, scope)
1153            }
1154        }
1155    }
1156
1157    fn excel_type_rank(val: &ResultData) -> u8 {
1158        match val {
1159            ResultData::None => 0,
1160            ResultData::Integer(_) | ResultData::Float(_) => 1,
1161            ResultData::String(_) => 2,
1162            ResultData::Boolean(_) => 3,
1163            _ => 4,
1164        }
1165    }
1166
1167    fn compare_excel_values(l: &ResultData, r: &ResultData) -> std::cmp::Ordering {
1168        match (l, r) {
1169            (ResultData::None, ResultData::None) => return std::cmp::Ordering::Equal,
1170            (ResultData::None, ResultData::Integer(b)) => {
1171                return 0.0
1172                    .partial_cmp(&(*b as f64))
1173                    .unwrap_or(std::cmp::Ordering::Equal);
1174            }
1175            (ResultData::None, ResultData::Float(b)) => {
1176                return 0.0.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
1177            }
1178            (ResultData::Integer(a), ResultData::None) => {
1179                return (*a as f64)
1180                    .partial_cmp(&0.0)
1181                    .unwrap_or(std::cmp::Ordering::Equal);
1182            }
1183            (ResultData::Float(a), ResultData::None) => {
1184                return a.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal);
1185            }
1186            (ResultData::None, ResultData::String(b)) => {
1187                return "".cmp(b.to_lowercase().as_str());
1188            }
1189            (ResultData::String(a), ResultData::None) => {
1190                return a.to_lowercase().as_str().cmp("");
1191            }
1192            (ResultData::None, ResultData::Boolean(b)) => {
1193                return false.cmp(b);
1194            }
1195            (ResultData::Boolean(a), ResultData::None) => {
1196                return a.cmp(&false);
1197            }
1198            _ => {}
1199        }
1200
1201        let rank_l = Self::excel_type_rank(l);
1202        let rank_r = Self::excel_type_rank(r);
1203        if rank_l != rank_r {
1204            return rank_l.cmp(&rank_r);
1205        }
1206        match (l, r) {
1207            (ResultData::Integer(a), ResultData::Integer(b)) => a.cmp(b),
1208            (ResultData::Float(a), ResultData::Float(b)) => {
1209                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
1210            }
1211            (ResultData::Integer(a), ResultData::Float(b)) => (*a as f64)
1212                .partial_cmp(b)
1213                .unwrap_or(std::cmp::Ordering::Equal),
1214            (ResultData::Float(a), ResultData::Integer(b)) => a
1215                .partial_cmp(&(*b as f64))
1216                .unwrap_or(std::cmp::Ordering::Equal),
1217            (ResultData::Boolean(a), ResultData::Boolean(b)) => a.cmp(b),
1218            (ResultData::String(a), ResultData::String(b)) => Self::compare_excel_strings(a, b),
1219            _ => std::cmp::Ordering::Equal,
1220        }
1221    }
1222
1223    /// `SORT`/`SORTBY`-specific comparator: Microsoft documents that both
1224    /// functions always place blank cells last, regardless of ascending
1225    /// vs. descending order -- unlike `compare_excel_values`'s general
1226    /// blank-coerces-to-0/""/false rule (correct for comparison operators,
1227    /// MATCH, etc.), which would otherwise rank a blank ahead of every
1228    /// negative number once descending order reverses the comparison.
1229    /// E.g. `SORT({-215.8,,-100,-240.97,-88},1,-1)` puts the blank last.
1230    fn sort_compare_blanks_last(
1231        l: &ResultData,
1232        r: &ResultData,
1233        sort_order: f64,
1234    ) -> std::cmp::Ordering {
1235        match (matches!(l, ResultData::None), matches!(r, ResultData::None)) {
1236            (true, true) => std::cmp::Ordering::Equal,
1237            (true, false) => std::cmp::Ordering::Greater,
1238            (false, true) => std::cmp::Ordering::Less,
1239            (false, false) => {
1240                let ord = Self::compare_excel_values(l, r);
1241                if sort_order < 0.0 { ord.reverse() } else { ord }
1242            }
1243        }
1244    }
1245
1246    fn is_excel_number_str(s: &str) -> bool {
1247        let s = s.trim();
1248        if s.is_empty() {
1249            return false;
1250        }
1251        let bytes = s.as_bytes();
1252        let first = bytes[0];
1253        if first == b'e' || first == b'E' {
1254            return false;
1255        }
1256        if (first == b'+' || first == b'-') && bytes.len() > 1 {
1257            let second = bytes[1];
1258            if second == b'e' || second == b'E' {
1259                return false;
1260            }
1261        }
1262        true
1263    }
1264
1265    fn compare_excel_strings(a: &str, b: &str) -> std::cmp::Ordering {
1266        let char_weight = |ch: char| -> u32 {
1267            match ch {
1268                ' ' => 0,
1269                '_' => 1,
1270                '-' => 2,
1271                ',' => 3,
1272                ';' => 4,
1273                ':' => 5,
1274                '!' => 6,
1275                '?' => 7,
1276                '.' => 8,
1277                '\'' => 9,
1278                '"' => 10,
1279                '(' => 11,
1280                ')' => 12,
1281                '[' => 13,
1282                ']' => 14,
1283                '{' => 15,
1284                '}' => 16,
1285                '@' => 17,
1286                '*' => 18,
1287                '/' => 19,
1288                '\\' => 20,
1289                '&' => 21,
1290                '#' => 22,
1291                '%' => 23,
1292                '`' => 24,
1293                '^' => 25,
1294                '+' => 26,
1295                '<' => 27,
1296                '=' => 28,
1297                '>' => 29,
1298                '|' => 30,
1299                '~' => 31,
1300                '$' => 32,
1301                '0'..='9' => 33 + (ch as u32 - '0' as u32),
1302                'A'..='Z' => 43 + (ch as u32 - 'A' as u32),
1303                'a'..='z' => 43 + (ch as u32 - 'a' as u32),
1304                _ => ch
1305                    .to_lowercase()
1306                    .next()
1307                    .map(|c| c as u32 + 200)
1308                    .unwrap_or(ch as u32 + 200),
1309            }
1310        };
1311
1312        for (ca, cb) in a.chars().zip(b.chars()) {
1313            let wa = char_weight(ca);
1314            let wb = char_weight(cb);
1315            if wa != wb {
1316                return wa.cmp(&wb);
1317            }
1318        }
1319        a.len().cmp(&b.len())
1320    }
1321
1322    /// Snaps a float to its 15-significant-digit rounding when the two are
1323    /// within floating-point noise of each other, so accumulated error does
1324    /// not leak into a result Excel would show as exact.
1325    ///
1326    /// Left alone if the rounding moves the value by more than that, and for
1327    /// zero and non-finite values.
1328    pub(crate) fn clean_float(val: f64) -> f64 {
1329        if val == 0.0 || !val.is_finite() {
1330            return val;
1331        }
1332        let abs_val = val.abs();
1333        let exp = abs_val.log10().floor() as i32;
1334        let factor = 10.0f64.powi(15 - 1 - exp);
1335        if factor.is_finite() && factor != 0.0 {
1336            let rounded = (val * factor).round() / factor;
1337            if (val - rounded).abs() <= 1e-14 * abs_val {
1338                return rounded;
1339            }
1340        }
1341        val
1342    }
1343
1344    /// Coerces a value to a number the way an Excel arithmetic operator does:
1345    /// a blank is 0, a boolean is 0 or 1, and text is converted if it reads as
1346    /// a number or a date (a date becoming its serial).
1347    ///
1348    /// `None` for text that is not numeric and for every other value,
1349    /// including errors -- callers turn that into `#VALUE!`.
1350    ///
1351    /// Not every function coerces this way; the stricter families reject text
1352    /// and booleans outright.
1353    pub(crate) fn to_f64(&self, val: &ResultData) -> Option<f64> {
1354        match val {
1355            ResultData::None => Some(0.0),
1356            ResultData::Float(f) => Some(*f),
1357            ResultData::Integer(i) => Some(*i as f64),
1358            ResultData::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
1359            ResultData::String(s) => {
1360                let s_trim = s.trim();
1361                if Self::is_excel_number_str(s_trim) {
1362                    if let Ok(f) = s_trim.parse::<f64>() {
1363                        return Some(f);
1364                    }
1365                    if let Some((date, _)) =
1366                        crate::core::date::parse_date_with_locale(s_trim, &self.locale)
1367                    {
1368                        return Some(crate::core::date::date_to_excel_serial(date));
1369                    }
1370                    None
1371                } else if let Some((date, _)) =
1372                    crate::core::date::parse_date_with_locale(s_trim, &self.locale)
1373                {
1374                    Some(crate::core::date::date_to_excel_serial(date))
1375                } else {
1376                    None
1377                }
1378            }
1379            _ => None,
1380        }
1381    }
1382
1383    fn to_f64_arg(&self, arg_opt: Option<&ResultData>, fn_name: &str) -> Result<f64, EngineError> {
1384        let val = arg_opt.ok_or_else(|| {
1385            EngineError::EvalError(EvalError::UnknownFunction(format!(
1386                "{} requires argument",
1387                fn_name
1388            )))
1389        })?;
1390        if let ResultData::Error(e) = val {
1391            return Err(EngineError::EvalError(EvalError::UnknownFunction(
1392                e.clone(),
1393            )));
1394        }
1395        self.to_f64(val).ok_or_else(|| {
1396            EngineError::EvalError(EvalError::UnknownFunction("#VALUE!".to_string()))
1397        })
1398    }
1399
1400    fn find_error_in_args(args: &[ResultData]) -> Option<ResultData> {
1401        for arg in args {
1402            match arg {
1403                ResultData::Error(_) => return Some(arg.clone()),
1404                ResultData::List(list) => {
1405                    if let Some(err) = Self::find_error_in_args(list) {
1406                        return Some(err);
1407                    }
1408                }
1409                _ => {}
1410            }
1411        }
1412        None
1413    }
1414
1415    fn check_arg_errors(&self, args: &[ResultData], is_direct: &[bool]) -> Option<ResultData> {
1416        for (i, arg) in args.iter().enumerate() {
1417            match arg {
1418                ResultData::Error(_) => return Some(arg.clone()),
1419                ResultData::List(list) => {
1420                    if let Some(err) = self.check_arg_errors(list, &[]) {
1421                        return Some(err);
1422                    }
1423                }
1424                ResultData::String(_)
1425                    if is_direct.get(i).copied().unwrap_or(false) && self.to_f64(arg).is_none() =>
1426                {
1427                    return Some(ResultData::Error("#VALUE!".to_string()));
1428                }
1429                _ => {}
1430            }
1431        }
1432        None
1433    }
1434
1435    fn sum_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
1436        match arg {
1437            ResultData::Float(f) => *f,
1438            ResultData::Integer(i) => *i as f64,
1439            ResultData::Boolean(b) => {
1440                if is_direct {
1441                    if *b { 1.0 } else { 0.0 }
1442                } else {
1443                    0.0
1444                }
1445            }
1446            ResultData::String(_) => {
1447                if is_direct {
1448                    self.to_f64(arg).unwrap_or(0.0)
1449                } else {
1450                    0.0
1451                }
1452            }
1453            ResultData::List(list) => {
1454                let mut sum = 0.0;
1455                for item in list {
1456                    sum += self.sum_helper(item, false);
1457                }
1458                sum
1459            }
1460            _ => 0.0,
1461        }
1462    }
1463
1464    /// Flattens a single argument (which may be a range/array `List`) into
1465    /// an ordered `Vec<f64>` for the financial functions that take a
1466    /// cashflow series (`NPV`, `IRR`, `MIRR`, `XNPV`, `XIRR`, `FVSCHEDULE`).
1467    /// Mirrors `sum_helper`'s convention: booleans/text only count when
1468    /// passed directly (not through a range).
1469    fn flatten_finance_numbers(&self, arg: &ResultData, is_direct: bool) -> Vec<f64> {
1470        match arg {
1471            ResultData::Float(f) => vec![*f],
1472            ResultData::Integer(i) => vec![*i as f64],
1473            ResultData::Boolean(b) => {
1474                if is_direct {
1475                    vec![if *b { 1.0 } else { 0.0 }]
1476                } else {
1477                    vec![]
1478                }
1479            }
1480            ResultData::String(_) => {
1481                if is_direct {
1482                    self.to_f64(arg).into_iter().collect()
1483                } else {
1484                    vec![]
1485                }
1486            }
1487            ResultData::List(list) => list
1488                .iter()
1489                .flat_map(|v| self.flatten_finance_numbers(v, false))
1490                .collect(),
1491            _ => vec![],
1492        }
1493    }
1494
1495    fn flatten_stat_numbers(&self, arg: &ResultData, is_direct: bool) -> Vec<f64> {
1496        match arg {
1497            ResultData::Float(f) => vec![*f],
1498            ResultData::Integer(i) => vec![*i as f64],
1499            ResultData::Boolean(b) => {
1500                if is_direct {
1501                    vec![if *b { 1.0 } else { 0.0 }]
1502                } else {
1503                    vec![]
1504                }
1505            }
1506            ResultData::String(_) => {
1507                if is_direct {
1508                    self.to_f64(arg).into_iter().collect()
1509                } else {
1510                    vec![]
1511                }
1512            }
1513            ResultData::List(list) => list
1514                .iter()
1515                .flat_map(|v| self.flatten_stat_numbers(v, false))
1516                .collect(),
1517            _ => vec![],
1518        }
1519    }
1520
1521    /// Flattens one argument positionally: `Some(n)` for a numeric cell,
1522    /// `None` for anything real Excel excludes from a paired statistical
1523    /// calculation (text, boolean, blank). Unlike flatten_stat_numbers,
1524    /// excluded cells still occupy a slot, so two ranges of the same
1525    /// shape always produce vectors of the same length and element `i` of
1526    /// one still lines up with element `i` of the other.
1527    fn flatten_positional(
1528        &self,
1529        arg: &ResultData,
1530        out: &mut Vec<Option<f64>>,
1531        first_err: &mut Option<String>,
1532    ) {
1533        match arg {
1534            ResultData::List(items) => {
1535                for item in items {
1536                    self.flatten_positional(item, out, first_err);
1537                }
1538            }
1539            ResultData::Float(f) => out.push(Some(*f)),
1540            ResultData::Integer(i) => out.push(Some(*i as f64)),
1541            ResultData::Error(e) => {
1542                if first_err.is_none() {
1543                    *first_err = Some(e.clone());
1544                }
1545                out.push(None);
1546            }
1547            _ => out.push(None),
1548        }
1549    }
1550
1551    fn positional_numbers(
1552        &self,
1553        arg: Option<&ResultData>,
1554        first_err: &mut Option<String>,
1555    ) -> Vec<Option<f64>> {
1556        let mut out = Vec::new();
1557        if let Some(a) = arg {
1558            self.flatten_positional(a, &mut out, first_err);
1559        }
1560        out
1561    }
1562
1563    /// Excel's paired statistical functions (CORREL/PEARSON/COVAR/
1564    /// COVARIANCE.P/COVARIANCE.S/SLOPE/INTERCEPT/RSQ/STEYX/FORECAST/
1565    /// TREND/LINEST/GROWTH/LOGEST/T.TEST/SUMX2PY2/SUMXMY2/SUMX2MY2/PROB)
1566    /// compare the two ranges' *raw* element counts first -- a mismatch
1567    /// is #N/A regardless of content -- and then drop every (x, y) pair
1568    /// where either side is non-numeric, keeping what survives aligned.
1569    ///
1570    /// Verified directly against real Excel: `COVAR(A1:A4, B1:B4)` with
1571    /// one text cell in B returns exactly the value of the 3-element
1572    /// ranges with that whole pair physically removed, and the same holds
1573    /// for SLOPE/INTERCEPT/RSQ/PEARSON/STEYX/FORECAST/T.TEST/SUMX*.
1574    /// Booleans and blanks are excluded the same way text is.
1575    ///
1576    /// This is deliberately *not* the same as flattening each side
1577    /// independently (what flatten_stat_numbers does): dropping a
1578    /// non-numeric from only one side shifts every later element against
1579    /// its partner, silently correlating the wrong values together.
1580    /// F.TEST/FTEST is the exception that genuinely does want independent
1581    /// per-array flattening -- it compares two samples' variances and
1582    /// doesn't require equal sizes at all (confirmed against real Excel:
1583    /// `FTEST(4-cell-with-text, ...)` equals `FTEST(full-4-cell, ...)`
1584    /// against the 3-cell survivor, i.e. each side shrinks on its own).
1585    fn pair_and_filter(
1586        xs_raw: Vec<Option<f64>>,
1587        ys_raw: Vec<Option<f64>>,
1588    ) -> Result<(Vec<f64>, Vec<f64>), String> {
1589        if xs_raw.len() != ys_raw.len() {
1590            return Err("#N/A".to_string());
1591        }
1592        let mut xs = Vec::with_capacity(xs_raw.len());
1593        let mut ys = Vec::with_capacity(ys_raw.len());
1594        for (x, y) in xs_raw.into_iter().zip(ys_raw) {
1595            if let (Some(x), Some(y)) = (x, y) {
1596                xs.push(x);
1597                ys.push(y);
1598            }
1599        }
1600        Ok((xs, ys))
1601    }
1602
1603    /// pair_and_filter over two argument slots.
1604    fn paired_args(
1605        &self,
1606        x_arg: Option<&ResultData>,
1607        y_arg: Option<&ResultData>,
1608    ) -> Result<(Vec<f64>, Vec<f64>), String> {
1609        for arg in [x_arg, y_arg].into_iter().flatten() {
1610            let scalar = match arg {
1611                ResultData::List(items) if items.len() == 1 => &items[0],
1612                other => other,
1613            };
1614            if let ResultData::Error(e) = scalar {
1615                return Err(e.clone());
1616            }
1617            if Self::is_empty_scalar_operand(arg) {
1618                return Err("#VALUE!".to_string());
1619            }
1620        }
1621        let mut first_err = None;
1622        let xs_raw = self.positional_numbers(x_arg, &mut first_err);
1623        let ys_raw = self.positional_numbers(y_arg, &mut first_err);
1624        if xs_raw.len() != ys_raw.len() {
1625            return Err("#N/A".to_string());
1626        }
1627        if let Some(e) = first_err {
1628            return Err(e);
1629        }
1630        Self::pair_and_filter(xs_raw, ys_raw)
1631    }
1632
1633    /// Like flatten_stat_numbers, but errors instead of silently dropping
1634    /// a cell real Excel won't accept. Excel's array/matrix-argument
1635    /// functions don't ignore text the way SUM/AVERAGE-style aggregates
1636    /// do -- one bad cell makes the whole call #VALUE!.
1637    ///
1638    /// `blanks` selects between the three blank-handling behaviours real
1639    /// Excel actually exhibits here, each established by probing it
1640    /// directly:
1641    ///  - `BlankPolicy::Zero` (MULTINOMIAL): a blank counts as 0 and the
1642    ///    call still succeeds -- `MULTINOMIAL(3, <blank>)` is 1, the
1643    ///    blank participating as a zero.
1644    ///  - `BlankPolicy::Skip` (GCD/LCM/SERIESSUM): a blank is dropped
1645    ///    outright rather than zero-filled. `LCM(1, <blank>)` is 1 (as if
1646    ///    `LCM(1)`), not `LCM(1, 0)` = 0. For SERIESSUM this also shifts
1647    ///    every later coefficient down a power:
1648    ///    `SERIESSUM(0.5, 0, 2, {4, 6, <blank>, 8})` is 6.0 -- exactly the
1649    ///    3-coefficient answer -- not the 5.625 a zero in that slot gives.
1650    ///  - `BlankPolicy::Reject` (LINEST/TREND/GROWTH/LOGEST/MMULT): blanks
1651    ///    are #VALUE! too, same as text and booleans.
1652    ///
1653    /// `coerce_text` selects separately whether a numeric-looking string
1654    /// is accepted (converted the same way `to_f64` would) or rejected
1655    /// outright as #VALUE! -- this does *not* track the blank policy,
1656    /// since GCD/LCM (`Skip`) coerce text (`GCD("12", 8)` = 4) while
1657    /// SERIESSUM (also `Skip`) does not (`SERIESSUM(1.49, 1, 2,
1658    /// {<blank>, "2", 27, -35})` is #VALUE! in real Excel, not the number
1659    /// the coerced "2" would give -- fuzz/fuzz_excel.py seed 107768).
1660    /// Booleans are always rejected regardless of either policy -- `GCD(TRUE,
1661    /// 8)` is #VALUE! -- which is why this can't just fall through to
1662    /// `to_f64`, the lenient coercion used for scalar arguments.
1663    fn flatten_strict_inner(
1664        &self,
1665        arg: &ResultData,
1666        blanks: BlankPolicy,
1667        coerce_text: bool,
1668        out: &mut Vec<f64>,
1669    ) -> Result<(), String> {
1670        match arg {
1671            ResultData::List(items) => {
1672                for item in items {
1673                    self.flatten_strict_inner(item, blanks, coerce_text, out)?;
1674                }
1675                Ok(())
1676            }
1677            ResultData::Error(e) => Err(e.clone()),
1678            ResultData::Float(f) => {
1679                out.push(*f);
1680                Ok(())
1681            }
1682            ResultData::Integer(i) => {
1683                out.push(*i as f64);
1684                Ok(())
1685            }
1686            ResultData::None => match blanks {
1687                BlankPolicy::Zero => {
1688                    out.push(0.0);
1689                    Ok(())
1690                }
1691                BlankPolicy::Skip => Ok(()),
1692                BlankPolicy::Reject => Err("#VALUE!".to_string()),
1693            },
1694            ResultData::String(_) if coerce_text => match self.to_f64(arg) {
1695                Some(f) => {
1696                    out.push(f);
1697                    Ok(())
1698                }
1699                None => Err("#VALUE!".to_string()),
1700            },
1701            _ => Err("#VALUE!".to_string()),
1702        }
1703    }
1704
1705    fn flatten_strict_numbers(&self, arg: &ResultData) -> Result<Vec<f64>, String> {
1706        let mut out = Vec::new();
1707        self.flatten_strict_inner(arg, BlankPolicy::Zero, true, &mut out)?;
1708        Ok(out)
1709    }
1710
1711    /// flatten_strict_numbers with blanks dropped rather than zero-filled,
1712    /// for GCD/LCM (which also coerce numeric text, like MULTINOMIAL).
1713    fn flatten_skipping_blanks(&self, arg: Option<&ResultData>) -> Result<Vec<f64>, String> {
1714        let mut out = Vec::new();
1715        if let Some(a) = arg {
1716            self.flatten_strict_inner(a, BlankPolicy::Skip, true, &mut out)?;
1717        }
1718        Ok(out)
1719    }
1720
1721    /// Like `flatten_skipping_blanks`, but a numeric-looking string is
1722    /// #VALUE! rather than coerced -- SERIESSUM's coefficients, unlike
1723    /// GCD/LCM's operands, don't accept text at all (measured:
1724    /// `SERIESSUM(1.49, 1, 2, {<blank>, "2", 27, -35})` is #VALUE! in real
1725    /// Excel, not the value the coerced "2" would give -- see
1726    /// `flatten_strict_inner`'s doc comment).
1727    fn flatten_skipping_blanks_no_text_coercion(
1728        &self,
1729        arg: Option<&ResultData>,
1730    ) -> Result<Vec<f64>, String> {
1731        let mut out = Vec::new();
1732        if let Some(a) = arg {
1733            self.flatten_strict_inner(a, BlankPolicy::Skip, false, &mut out)?;
1734        }
1735        Ok(out)
1736    }
1737
1738    /// flatten_strict_numbers with the stricter "a blank is also #VALUE!"
1739    /// rule the regression-array and matrix functions use.
1740    fn flatten_numbers_only(&self, arg: &ResultData) -> Result<Vec<f64>, String> {
1741        let mut out = Vec::new();
1742        self.flatten_strict_inner(arg, BlankPolicy::Reject, false, &mut out)?;
1743        Ok(out)
1744    }
1745
1746    /// The value of one cell of a SUMIF/AVERAGEIF/MAXIFS/MINIFS-style
1747    /// *aggregate* range. Only a real number counts: Excel silently skips
1748    /// text and booleans in the range being summed/averaged/compared
1749    /// (confirmed directly -- `SUMIF` over a range holding
1750    /// `{100, TRUE, 200, "txt", 300}` is 600, and MAXIFS over the same
1751    /// range is 300, not the boolean coerced to 1). Using the lenient
1752    /// `to_f64` here instead folded `TRUE` in as a 1, which both shifted
1753    /// sums/averages and could win a MAX/MIN outright.
1754    fn aggregate_range_number(val: &ResultData) -> Option<f64> {
1755        match val {
1756            ResultData::Float(f) => Some(*f),
1757            ResultData::Integer(i) => Some(*i as f64),
1758            _ => None,
1759        }
1760    }
1761
1762    fn flatten_numbers_only_arg(&self, arg: Option<&ResultData>) -> Result<Vec<f64>, String> {
1763        match arg {
1764            Some(a) => self.flatten_numbers_only(a),
1765            None => Ok(vec![]),
1766        }
1767    }
1768
1769    /// `flatten_stat_numbers` across an argument list, applying Excel's rule
1770    /// for text supplied *directly* as an argument: it is coerced if it
1771    /// looks numeric, and is `#VALUE!` if it does not. Text reached through
1772    /// a reference is skipped instead, which is what `flatten_stat_numbers`
1773    /// already does on its own.
1774    ///
1775    /// The split matters because silently skipping uncoercible direct text
1776    /// turns a wrong formula into a plausible number: `DEVSQ("abc",3,4,5)`
1777    /// answered 2 (the spread of the remaining three) where Excel answers
1778    /// `#VALUE!`. Verified against real Excel for SUM, AVERAGE, DEVSQ,
1779    /// STDEV, VAR, MEDIAN, MAX, MIN, PRODUCT, SUMSQ, GEOMEAN, AVEDEV, SKEW
1780    /// and KURT. COUNT is the deliberate exception -- it never errors, it
1781    /// just doesn't count what it can't read -- and does not call this.
1782    fn flatten_args_stat_numbers(
1783        &self,
1784        args: &[ResultData],
1785        is_direct: &[bool],
1786    ) -> Result<Vec<f64>, String> {
1787        let mut out = Vec::new();
1788        for (i, arg) in args.iter().enumerate() {
1789            let direct = is_direct.get(i).copied().unwrap_or(false);
1790            if direct && matches!(arg, ResultData::String(_)) && self.to_f64(arg).is_none() {
1791                return Err("#VALUE!".to_string());
1792            }
1793            out.extend(self.flatten_stat_numbers(arg, direct));
1794        }
1795        Ok(out)
1796    }
1797
1798    /// Flatten arguments for the `*A` statistical family (AVERAGEA, MAXA,
1799    /// MINA, STDEVA, STDEVPA, VARA, VARPA), which count text and booleans
1800    /// rather than skipping them.
1801    ///
1802    /// Text is where the family gets interesting, and the rule depends on
1803    /// *how* the text arrived. Inside a reference it counts as 0, which is
1804    /// the documented behaviour everyone knows. Passed directly as an
1805    /// argument it is coerced instead, and a value that will not coerce is
1806    /// an error rather than a zero. Against real Excel, with A1 holding the
1807    /// text "12":
1808    ///
1809    /// ```text
1810    /// AVERAGEA(A1, 3)     = 1.5        text in a reference counts as 0
1811    /// AVERAGEA("12", 3)   = 7.5        direct text is coerced
1812    /// AVERAGEA("abc", 3)  = #VALUE!    ... and must coerce
1813    /// ```
1814    fn flatten_stat_numbers_a(
1815        &self,
1816        arg: &ResultData,
1817        is_direct: bool,
1818    ) -> Result<Vec<f64>, String> {
1819        Ok(match arg {
1820            ResultData::Float(f) => vec![*f],
1821            ResultData::Integer(i) => vec![*i as f64],
1822            ResultData::Boolean(b) => vec![if *b { 1.0 } else { 0.0 }],
1823            ResultData::String(_) => {
1824                if is_direct {
1825                    match self.to_f64(arg) {
1826                        Some(f) => vec![f],
1827                        None => return Err("#VALUE!".to_string()),
1828                    }
1829                } else {
1830                    vec![0.0]
1831                }
1832            }
1833            ResultData::Error(e) => return Err(e.clone()),
1834            ResultData::List(list) => {
1835                let mut out = Vec::new();
1836                for v in list {
1837                    out.extend(self.flatten_stat_numbers_a(v, false)?);
1838                }
1839                out
1840            }
1841            ResultData::None => vec![],
1842            _ => vec![0.0],
1843        })
1844    }
1845
1846    /// `flatten_stat_numbers_a` over a whole argument list, using the
1847    /// caller's per-argument direct/reference classification.
1848    fn flatten_args_stat_numbers_a(
1849        &self,
1850        args: &[ResultData],
1851        is_direct: &[bool],
1852    ) -> Result<Vec<f64>, String> {
1853        let mut out = Vec::new();
1854        for (i, arg) in args.iter().enumerate() {
1855            out.extend(
1856                self.flatten_stat_numbers_a(arg, is_direct.get(i).copied().unwrap_or(false))?,
1857            );
1858        }
1859        Ok(out)
1860    }
1861
1862    fn extract_matrix(&self, arg: &ResultData) -> Vec<Vec<f64>> {
1863        match arg {
1864            ResultData::List(list) => {
1865                let mut rows = Vec::new();
1866                for item in list {
1867                    match item {
1868                        ResultData::List(sub_list) => {
1869                            let row: Vec<f64> =
1870                                sub_list.iter().flat_map(|v| self.to_f64(v)).collect();
1871                            if !row.is_empty() {
1872                                rows.push(row);
1873                            }
1874                        }
1875                        _ => {
1876                            if let Some(f) = self.to_f64(item) {
1877                                rows.push(vec![f]);
1878                            }
1879                        }
1880                    }
1881                }
1882                rows
1883            }
1884            _ => vec![],
1885        }
1886    }
1887
1888    /// Reshapes a range argument's flat evaluated list back into a 2D
1889    /// row-major matrix using the *reference's* own width.
1890    ///
1891    /// A plain rectangular range like `F1:G2` evaluates to a flat
1892    /// `List` of 4 scalars with no nesting, so extract_matrix (which can
1893    /// only treat a nested `List` as a row) turned it into a 4x1 column
1894    /// instead of a 2x2 square -- and every matrix function then reported
1895    /// #VALUE! on a perfectly valid square range. MMULT already
1896    /// reconstructed its operands' shapes from the argument expression
1897    /// this way; this shares that logic with MDETERM/MINVERSE.
1898    fn matrix_from_arg(
1899        &self,
1900        expr: &crate::core::parser::Expr,
1901        value: &ResultData,
1902    ) -> Vec<Vec<f64>> {
1903        if let ResultData::List(items) = value
1904            && items.iter().any(|i| matches!(i, ResultData::List(_)))
1905        {
1906            return self.extract_matrix(value);
1907        }
1908        fn plain(v: &ResultData) -> Option<f64> {
1909            match v {
1910                ResultData::Float(f) => Some(*f),
1911                ResultData::Integer(i) => Some(*i as f64),
1912                _ => None,
1913            }
1914        }
1915        let items: Vec<&ResultData> = match value {
1916            ResultData::List(items) => items.iter().collect(),
1917            other => vec![other],
1918        };
1919        if items.iter().any(|v| plain(v).is_none()) {
1920            return Vec::new();
1921        }
1922        let flat: Vec<f64> = items.iter().filter_map(|v| plain(v)).collect();
1923        let cols = match Self::range_bounds(expr) {
1924            Some((_, _, start_col, _, end_col)) => end_col.saturating_sub(start_col) + 1,
1925            None => flat.len().max(1),
1926        };
1927        if cols == 0 || !flat.len().is_multiple_of(cols) {
1928            return self.extract_matrix(value);
1929        }
1930        flat.chunks(cols).map(|c| c.to_vec()).collect()
1931    }
1932
1933    /// An optional numeric argument. An *absent* argument falls back to
1934    /// `default`, but one that is present and non-numeric is #VALUE! --
1935    /// the `.and_then(to_f64).unwrap_or(default)` shape used in places
1936    /// conflates the two, so e.g. `LOG(3.14, "E")` quietly computed
1937    /// base-10 instead of erroring.
1938    /// `#DIV/0!` when either operand of a paired sum contains no numeric
1939    /// value at all.
1940    ///
1941    /// This is *not* the same as "no pair survived exclusion", which is
1942    /// simply 0. Real Excel, with a column [53, TRUE] against a row
1943    /// [TRUE, -10]: every pair is dropped (each holds a boolean), yet the
1944    /// answer is 0 rather than an error, because each range does hold a
1945    /// number. Swap in a range that is entirely text or entirely booleans
1946    /// and it becomes #DIV/0!.
1947    ///
1948    /// Fitted against eleven real-Excel cases spanning text, booleans and
1949    /// mixtures, at one, two and three elements per range.
1950    fn paired_sum_has_no_numbers(&self, arg: Option<&ResultData>) -> bool {
1951        let mut ignored = None;
1952        let slots = self.positional_numbers(arg, &mut ignored);
1953        slots.iter().all(|v| v.is_none())
1954    }
1955
1956    /// True when an argument is a *single-cell* operand that is empty.
1957    ///
1958    /// Excel treats that as a missing operand and answers #VALUE!, rather
1959    /// than as a one-element array of nothing. The distinction is
1960    /// specifically about a single cell: `SUMPRODUCT(<one blank cell>)` is
1961    /// #VALUE! while `SUMPRODUCT(<two blank cells>)` is 0, and
1962    /// `SUMPRODUCT(-50, <blank>)` is #VALUE! too. Same for MULTINOMIAL and
1963    /// the paired statistical functions.
1964    ///
1965    /// A one-cell range evaluates to a one-element `List` rather than a
1966    /// bare scalar, so both spellings have to be unwrapped. Note this is
1967    /// about blankness only -- a one-cell operand holding text or a
1968    /// boolean behaves differently again.
1969    fn is_empty_scalar_operand(arg: &ResultData) -> bool {
1970        let scalar = match arg {
1971            ResultData::List(items) if items.len() == 1 => &items[0],
1972            other => other,
1973        };
1974        matches!(scalar, ResultData::None)
1975    }
1976
1977    /// True when the first argument is a boolean and the function is one
1978    /// of the few that refuse them.
1979    ///
1980    /// Excel's numeric coercion is not uniform here. SQRT, FACT, SIGN,
1981    /// INT, EXP, ROMAN and most of their neighbours take TRUE as 1
1982    /// without complaint, but ERF, ERFC, FACTDOUBLE and SQRTPI all answer
1983    /// #VALUE! -- verified one function at a time against real Excel,
1984    /// because the split does not follow from anything about the
1985    /// functions themselves.
1986    fn first_arg_is_boolean(args: &[ResultData]) -> bool {
1987        matches!(args.first(), Some(ResultData::Boolean(_)))
1988    }
1989
1990    fn opt_f64_arg(&self, args: &[ResultData], i: usize, default: f64) -> Result<f64, EngineError> {
1991        match args.get(i) {
1992            None => Ok(default),
1993            Some(ResultData::None) => Ok(0.0),
1994            Some(ResultData::Error(e)) => Err(EngineError::EvalError(EvalError::UnknownFunction(
1995                e.clone(),
1996            ))),
1997            Some(v) => self.to_f64(v).ok_or_else(|| {
1998                EngineError::EvalError(EvalError::UnknownFunction("#VALUE!".to_string()))
1999            }),
2000        }
2001    }
2002
2003    fn opt_f64(&self, args: &[ResultData], i: usize, default: f64) -> f64 {
2004        args.get(i).and_then(|v| self.to_f64(v)).unwrap_or(default)
2005    }
2006
2007    fn average_helper(&self, arg: &ResultData, is_direct: bool) -> (f64, usize) {
2008        match arg {
2009            ResultData::Float(f) => (*f, 1),
2010            ResultData::Integer(i) => (*i as f64, 1),
2011            ResultData::Boolean(b) => {
2012                if is_direct {
2013                    (if *b { 1.0 } else { 0.0 }, 1)
2014                } else {
2015                    (0.0, 0)
2016                }
2017            }
2018            ResultData::String(_) => {
2019                if is_direct {
2020                    if let Some(f) = self.to_f64(arg) {
2021                        (f, 1)
2022                    } else {
2023                        (0.0, 0)
2024                    }
2025                } else {
2026                    (0.0, 0)
2027                }
2028            }
2029            ResultData::List(list) => {
2030                let mut sum = 0.0;
2031                let mut count = 0;
2032                for item in list {
2033                    let (s, c) = self.average_helper(item, false);
2034                    sum += s;
2035                    count += c;
2036                }
2037                (sum, count)
2038            }
2039            _ => (0.0, 0),
2040        }
2041    }
2042
2043    fn count_helper(&self, arg: &ResultData) -> usize {
2044        match arg {
2045            ResultData::Float(_) | ResultData::Integer(_) => 1,
2046            ResultData::List(list) => {
2047                let mut count = 0;
2048                for item in list {
2049                    count += self.count_helper(item);
2050                }
2051                count
2052            }
2053            _ => 0,
2054        }
2055    }
2056
2057    fn min_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
2058        match arg {
2059            ResultData::Float(f) => *f,
2060            ResultData::Integer(i) => *i as f64,
2061            ResultData::Boolean(b) => {
2062                if is_direct {
2063                    if *b { 1.0 } else { 0.0 }
2064                } else {
2065                    f64::INFINITY
2066                }
2067            }
2068            ResultData::String(_) => {
2069                if is_direct {
2070                    self.to_f64(arg).unwrap_or(f64::INFINITY)
2071                } else {
2072                    f64::INFINITY
2073                }
2074            }
2075            ResultData::List(list) => {
2076                let mut min_val = f64::INFINITY;
2077                for item in list {
2078                    min_val = min_val.min(self.min_helper(item, false));
2079                }
2080                min_val
2081            }
2082            _ => f64::INFINITY,
2083        }
2084    }
2085
2086    fn max_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
2087        match arg {
2088            ResultData::Float(f) => *f,
2089            ResultData::Integer(i) => *i as f64,
2090            ResultData::Boolean(b) => {
2091                if is_direct {
2092                    if *b { 1.0 } else { 0.0 }
2093                } else {
2094                    f64::NEG_INFINITY
2095                }
2096            }
2097            ResultData::String(_) => {
2098                if is_direct {
2099                    self.to_f64(arg).unwrap_or(f64::NEG_INFINITY)
2100                } else {
2101                    f64::NEG_INFINITY
2102                }
2103            }
2104            ResultData::List(list) => {
2105                let mut max_val = f64::NEG_INFINITY;
2106                for item in list {
2107                    max_val = max_val.max(self.max_helper(item, false));
2108                }
2109                max_val
2110            }
2111            _ => f64::NEG_INFINITY,
2112        }
2113    }
2114
2115    fn concat_helper(&self, arg: &ResultData, out: &mut String) {
2116        match arg {
2117            ResultData::List(list) => {
2118                for item in list {
2119                    self.concat_helper(item, out);
2120                }
2121            }
2122            other => {
2123                out.push_str(&other.to_string());
2124            }
2125        }
2126    }
2127
2128    fn counta_helper(&self, arg: &ResultData) -> usize {
2129        match arg {
2130            ResultData::None => 0,
2131            ResultData::List(list) => {
2132                let mut count = 0;
2133                for item in list {
2134                    count += self.counta_helper(item);
2135                }
2136                count
2137            }
2138            _ => 1,
2139        }
2140    }
2141
2142    fn product_helper(&self, arg: &ResultData, is_direct: bool) -> (f64, bool) {
2143        match arg {
2144            ResultData::Float(f) => (*f, true),
2145            ResultData::Integer(i) => (*i as f64, true),
2146            ResultData::Boolean(b) => {
2147                if is_direct {
2148                    (if *b { 1.0 } else { 0.0 }, true)
2149                } else {
2150                    (1.0, false)
2151                }
2152            }
2153            ResultData::String(_) => {
2154                if is_direct {
2155                    if let Some(f) = self.to_f64(arg) {
2156                        (f, true)
2157                    } else {
2158                        (1.0, false)
2159                    }
2160                } else {
2161                    (1.0, false)
2162                }
2163            }
2164            ResultData::List(list) => {
2165                let mut prod = 1.0;
2166                let mut has_nums = false;
2167                for item in list {
2168                    let (p, h) = self.product_helper(item, false);
2169                    if h {
2170                        prod *= p;
2171                        has_nums = true;
2172                    }
2173                }
2174                (prod, has_nums)
2175            }
2176            _ => (1.0, false),
2177        }
2178    }
2179
2180    fn to_bool_opt(&self, val: &ResultData) -> Option<bool> {
2181        match val {
2182            ResultData::Boolean(b) => Some(*b),
2183            ResultData::Integer(i) => Some(*i != 0),
2184            ResultData::Float(f) => Some(*f != 0.0),
2185            ResultData::String(s) => {
2186                let s_trim = s.trim();
2187                if s_trim.eq_ignore_ascii_case("true") {
2188                    Some(true)
2189                } else if s_trim.eq_ignore_ascii_case("false") {
2190                    Some(false)
2191                } else if let Ok(f) = s_trim.parse::<f64>() {
2192                    Some(f != 0.0)
2193                } else {
2194                    None
2195                }
2196            }
2197            ResultData::None => Some(false),
2198            _ => None,
2199        }
2200    }
2201
2202    fn to_bool(&self, val: &ResultData) -> bool {
2203        self.to_bool_opt(val).unwrap_or(false)
2204    }
2205
2206    /// Strict "is this a genuine number" check for range-value aggregation
2207    /// (DCOUNT/DSUM/DAVERAGE/... and friends), as opposed to `to_f64`'s
2208    /// scalar-arithmetic coercion (which maps blank -> 0 and booleans ->
2209    /// 1/0). Confirmed against real Excel via the differential fuzzer that
2210    /// blank and boolean database cells must be excluded here the same
2211    /// way SUM/COUNT/AVERAGE ignore them within a range argument -- using
2212    /// `to_f64` instead let a blank row zero out DPRODUCT entirely and
2213    /// skewed DCOUNT/DSUM/DAVERAGE by counting/summing blanks and
2214    /// TRUE/FALSE as 0/1.
2215    fn range_numeric(val: &ResultData) -> Option<f64> {
2216        match val {
2217            ResultData::Integer(i) => Some(*i as f64),
2218            ResultData::Float(f) => Some(*f),
2219            _ => None,
2220        }
2221    }
2222
2223    /// Exact-match ("match_type 0" / "range_lookup FALSE") comparison for
2224    /// MATCH/VLOOKUP/HLOOKUP/XLOOKUP.
2225    ///
2226    /// A *blank* lookup value is coerced to 0 (Excel's usual empty-cell
2227    /// coercion) and a blank cell in the searched range never matches
2228    /// anything. Comparing the two blanks as equal strings instead --
2229    /// which is what a plain `to_string()` comparison does, since both
2230    /// render as "" -- made `MATCH(A1, A1:A4, 0)` over a blank A1 report
2231    /// a hit at position 1 where real Excel reports #N/A.
2232    fn exact_lookup_matches(lookup: &ResultData, candidate: &ResultData) -> bool {
2233        if matches!(candidate, ResultData::None) {
2234            return false;
2235        }
2236        let lookup_key = match lookup {
2237            ResultData::None => "0".to_string(),
2238            other => other.to_string(),
2239        };
2240        candidate.to_string() == lookup_key
2241    }
2242
2243    fn wildcard_criteria_matches(pattern: &str, text: &str) -> bool {
2244        fn rec(pat: &[char], txt: &[char]) -> bool {
2245            if pat.is_empty() {
2246                return txt.is_empty();
2247            }
2248            match pat[0] {
2249                '*' => rec(&pat[1..], txt) || (!txt.is_empty() && rec(pat, &txt[1..])),
2250                '?' => !txt.is_empty() && rec(&pat[1..], &txt[1..]),
2251                '~' if pat.len() > 1 && matches!(pat[1], '*' | '?' | '~') => {
2252                    !txt.is_empty() && pat[1] == txt[0] && rec(&pat[2..], &txt[1..])
2253                }
2254                ch => !txt.is_empty() && ch == txt[0] && rec(&pat[1..], &txt[1..]),
2255            }
2256        }
2257
2258        let pat = pattern.to_lowercase().chars().collect::<Vec<_>>();
2259        let txt = text.to_lowercase().chars().collect::<Vec<_>>();
2260        rec(&pat, &txt)
2261    }
2262
2263    fn criteria_text_eq(val: &ResultData, pattern: &str) -> bool {
2264        let text = val.to_string();
2265        if pattern.contains('*') || pattern.contains('?') {
2266            matches!(val, ResultData::String(_)) && Self::wildcard_criteria_matches(pattern, &text)
2267        } else {
2268            text.to_lowercase() == pattern.to_lowercase()
2269        }
2270    }
2271
2272    fn match_criteria(&self, val: &ResultData, criteria: &ResultData) -> bool {
2273        let crit_str = criteria.to_string();
2274        if let Some(rest) = crit_str.strip_prefix(">=") {
2275            let val_f = match Self::range_numeric(val) {
2276                Some(f) => f,
2277                None => return false,
2278            };
2279            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2280            val_f >= crit_f
2281        } else if let Some(rest) = crit_str.strip_prefix('>') {
2282            let val_f = match Self::range_numeric(val) {
2283                Some(f) => f,
2284                None => return false,
2285            };
2286            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2287            val_f > crit_f
2288        } else if let Some(rest) = crit_str.strip_prefix("<>") {
2289            let remainder = rest.trim();
2290            !Self::criteria_text_eq(val, remainder)
2291        } else if let Some(rest) = crit_str.strip_prefix("<=") {
2292            let val_f = match Self::range_numeric(val) {
2293                Some(f) => f,
2294                None => return false,
2295            };
2296            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2297            val_f <= crit_f
2298        } else if let Some(rest) = crit_str.strip_prefix('<') {
2299            let val_f = match Self::range_numeric(val) {
2300                Some(f) => f,
2301                None => return false,
2302            };
2303            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2304            val_f < crit_f
2305        } else if let Some(rest) = crit_str.strip_prefix('=') {
2306            let remainder = rest.trim();
2307            Self::criteria_text_eq(val, remainder)
2308        } else {
2309            Self::criteria_text_eq(val, &crit_str)
2310        }
2311    }
2312
2313    /// Resolves an argument `Expr` to its raw `(sheet, start_row, start_col,
2314    /// end_row, end_col)` range bounds, for functions (like the database
2315    /// `D*` family below) that need genuine 2D shape and can't work off the
2316    /// pre-flattened `ResultData::List` every other argument already went
2317    /// through in `evaluated_args`.
2318    fn range_bounds(
2319        expr: &crate::core::parser::Expr,
2320    ) -> Option<(Option<String>, usize, usize, usize, usize)> {
2321        use crate::core::parser::Expr;
2322        match expr {
2323            Expr::RangeRef {
2324                sheet,
2325                start_row,
2326                start_col,
2327                end_row,
2328                end_col,
2329                ..
2330            } => Some((sheet.clone(), *start_row, *start_col, *end_row, *end_col)),
2331            Expr::CellRef {
2332                sheet, row, col, ..
2333            } => Some((sheet.clone(), *row, *col, *row, *col)),
2334            _ => None,
2335        }
2336    }
2337
2338    /// Reads a range's cells into a row-major grid, resolving a whole-column
2339    /// range's `end_row` sentinel and cross-sheet references via `context`.
2340    /// Materializing into an owned `Vec<Vec<ResultData>>` (rather than
2341    /// keeping a live `&Sheet` around) sidesteps the local-vs-remote
2342    /// lifetime split for the rest of the database-function logic, and
2343    /// database/criteria ranges are small enough that this is cheap.
2344    fn materialize_range(
2345        &self,
2346        sheet_opt: &Option<String>,
2347        start_row: usize,
2348        start_col: usize,
2349        end_row: usize,
2350        end_col: usize,
2351        context: Option<&Context>,
2352    ) -> Option<Vec<Vec<ResultData>>> {
2353        let is_self = match sheet_opt {
2354            Some(name) => name == &self.name,
2355            None => true,
2356        };
2357        let source: &Sheet = if is_self {
2358            self
2359        } else {
2360            context?.sheets.get(sheet_opt.as_ref()?)?
2361        };
2362        let actual_end_row = if end_row == usize::MAX {
2363            source.row_count().saturating_sub(1)
2364        } else {
2365            end_row
2366        };
2367        let actual_end_col = if end_col == usize::MAX {
2368            source.col_count().saturating_sub(1)
2369        } else {
2370            end_col
2371        };
2372        if actual_end_row < start_row || actual_end_col < start_col {
2373            return Some(Vec::new());
2374        }
2375        let mut grid = Vec::with_capacity(actual_end_row - start_row + 1);
2376        for r in start_row..=actual_end_row {
2377            let mut row = Vec::with_capacity(actual_end_col - start_col + 1);
2378            for c in start_col..=actual_end_col {
2379                row.push(source.get_result_data(&CellRef::new(r, c)));
2380            }
2381            grid.push(row);
2382        }
2383        Some(grid)
2384    }
2385
2386    /// Shared implementation for the 12 database `D*` functions
2387    /// (DAVERAGE/DCOUNT/DCOUNTA/DGET/DMAX/DMIN/DPRODUCT/DSTDEV/DSTDEVP/
2388    /// DSUM/DVAR/DVARP): each reduces to "match database rows against the
2389    /// criteria table, then aggregate one field column of the matches" --
2390    /// they differ only in which aggregation runs at the end.
2391    ///
2392    /// `database`/`criteria` are read from the raw `args` AST nodes (not
2393    /// `evaluated_args`) specifically to recover real row/column bounds;
2394    /// `field` (name or 1-based index) still comes from `evaluated_args`
2395    /// since it's a scalar. Criteria semantics match Excel's: multiple
2396    /// criteria *rows* are OR'd together, multiple non-blank cells within
2397    /// one criteria row are AND'd, and a blank criteria cell imposes no
2398    /// constraint on that field.
2399    fn evaluate_database_function(
2400        &self,
2401        func_name: &str,
2402        args: &[crate::core::parser::Expr],
2403        evaluated_args: &[ResultData],
2404        context: Option<&Context>,
2405    ) -> Result<ResultData, EngineError> {
2406        if args.len() < 3 || evaluated_args.len() < 3 {
2407            return Ok(ResultData::Error("#VALUE!".to_string()));
2408        }
2409        let (db_sheet, db_sr, db_sc, db_er, db_ec) = match Self::range_bounds(&args[0]) {
2410            Some(v) => v,
2411            None => return Ok(ResultData::Error("#VALUE!".to_string())),
2412        };
2413        let (crit_sheet, crit_sr, crit_sc, crit_er, crit_ec) = match Self::range_bounds(&args[2]) {
2414            Some(v) => v,
2415            None => return Ok(ResultData::Error("#VALUE!".to_string())),
2416        };
2417        let db = match self.materialize_range(&db_sheet, db_sr, db_sc, db_er, db_ec, context) {
2418            Some(g) => g,
2419            None => return Ok(ResultData::Error("#REF!".to_string())),
2420        };
2421        let crit = match self.materialize_range(
2422            &crit_sheet,
2423            crit_sr,
2424            crit_sc,
2425            crit_er,
2426            crit_ec,
2427            context,
2428        ) {
2429            Some(g) => g,
2430            None => return Ok(ResultData::Error("#REF!".to_string())),
2431        };
2432        if db.len() < 2 || crit.len() < 2 {
2433            return Ok(ResultData::Error("#VALUE!".to_string()));
2434        }
2435
2436        let db_headers: Vec<String> = db[0].iter().map(|v| v.to_string()).collect();
2437        let field_idx: usize = match &evaluated_args[1] {
2438            ResultData::String(s) => {
2439                match db_headers.iter().position(|h| h.eq_ignore_ascii_case(s)) {
2440                    Some(idx) => idx,
2441                    None => return Ok(ResultData::Error("#VALUE!".to_string())),
2442                }
2443            }
2444            other => match self.to_f64(other) {
2445                Some(n) if n >= 1.0 && (n as usize) <= db_headers.len() => n as usize - 1,
2446                _ => return Ok(ResultData::Error("#VALUE!".to_string())),
2447            },
2448        };
2449
2450        let crit_headers: Vec<String> = crit[0].iter().map(|v| v.to_string()).collect();
2451        let crit_to_db: Vec<Option<usize>> = crit_headers
2452            .iter()
2453            .map(|h| db_headers.iter().position(|dh| dh.eq_ignore_ascii_case(h)))
2454            .collect();
2455
2456        let mut matched: Vec<ResultData> = Vec::new();
2457        for row in db.iter().skip(1) {
2458            let row_matches_any_criteria_row = crit.iter().skip(1).any(|crit_row| {
2459                crit_row.iter().enumerate().all(|(ci, cell)| {
2460                    if matches!(cell, ResultData::None) {
2461                        return true;
2462                    }
2463                    match crit_to_db.get(ci).copied().flatten() {
2464                        Some(db_col) => self.match_criteria(&row[db_col], cell),
2465                        None => false,
2466                    }
2467                })
2468            });
2469            if row_matches_any_criteria_row {
2470                matched.push(row[field_idx].clone());
2471            }
2472        }
2473
2474        match func_name {
2475            "DGET" => match matched.len() {
2476                0 => Ok(ResultData::Error("#VALUE!".to_string())),
2477                1 => Ok(matched.into_iter().next().unwrap()),
2478                _ => Ok(ResultData::Error("#NUM!".to_string())),
2479            },
2480            "DCOUNT" => Ok(ResultData::Float(
2481                matched
2482                    .iter()
2483                    .filter(|v| Self::range_numeric(v).is_some())
2484                    .count() as f64,
2485            )),
2486            "DCOUNTA" => Ok(ResultData::Float(
2487                matched.iter().map(|v| self.counta_helper(v)).sum::<usize>() as f64,
2488            )),
2489            _ => {
2490                let nums: Vec<f64> = matched.iter().filter_map(Self::range_numeric).collect();
2491                match func_name {
2492                    "DSUM" => Ok(ResultData::Float(nums.iter().sum())),
2493                    "DPRODUCT" => Ok(ResultData::Float(if nums.is_empty() {
2494                        0.0
2495                    } else {
2496                        nums.iter().product()
2497                    })),
2498                    "DMAX" => {
2499                        let m = nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
2500                        Ok(ResultData::Float(if m.is_finite() { m } else { 0.0 }))
2501                    }
2502                    "DMIN" => {
2503                        let m = nums.iter().cloned().fold(f64::INFINITY, f64::min);
2504                        Ok(ResultData::Float(if m.is_finite() { m } else { 0.0 }))
2505                    }
2506                    "DAVERAGE" => {
2507                        if nums.is_empty() {
2508                            Ok(ResultData::Error("#DIV/0!".to_string()))
2509                        } else {
2510                            Ok(ResultData::Float(
2511                                nums.iter().sum::<f64>() / nums.len() as f64,
2512                            ))
2513                        }
2514                    }
2515                    "DSTDEV" => match crate::core::stats::stdev_s(&nums) {
2516                        Ok(v) => Ok(ResultData::Float(v)),
2517                        Err(e) => Ok(ResultData::Error(e)),
2518                    },
2519                    "DSTDEVP" => match crate::core::stats::stdev_p(&nums) {
2520                        Ok(v) => Ok(ResultData::Float(v)),
2521                        Err(e) => Ok(ResultData::Error(e)),
2522                    },
2523                    "DVAR" => match crate::core::stats::var_s(&nums) {
2524                        Ok(v) => Ok(ResultData::Float(v)),
2525                        Err(e) => Ok(ResultData::Error(e)),
2526                    },
2527                    "DVARP" => match crate::core::stats::var_p(&nums) {
2528                        Ok(v) => Ok(ResultData::Float(v)),
2529                        Err(e) => Ok(ResultData::Error(e)),
2530                    },
2531                    _ => unreachable!(),
2532                }
2533            }
2534        }
2535    }
2536
2537    fn proper(&self, s: &str) -> String {
2538        let mut c_chars = Vec::new();
2539        let mut capitalize_next = true;
2540        for c in s.chars() {
2541            if c.is_alphabetic() {
2542                if capitalize_next {
2543                    c_chars.extend(c.to_uppercase());
2544                } else {
2545                    c_chars.extend(c.to_lowercase());
2546                }
2547                capitalize_next = false;
2548            } else {
2549                c_chars.push(c);
2550                capitalize_next = true;
2551            }
2552        }
2553        c_chars.into_iter().collect()
2554    }
2555
2556    fn get_ymd_hms(&self) -> ((i32, u32, u32), (u32, u32, u32)) {
2557        let now = web_time::SystemTime::now()
2558            .duration_since(web_time::SystemTime::UNIX_EPOCH)
2559            .unwrap_or_default()
2560            .as_secs();
2561        let secs_in_day = 86400;
2562        let days_since_epoch = (now / secs_in_day) as i32;
2563        let seconds_of_day = (now % secs_in_day) as u32;
2564
2565        let hour = seconds_of_day / 3600;
2566        let minute = (seconds_of_day % 3600) / 60;
2567        let second = seconds_of_day % 60;
2568
2569        let era = (if days_since_epoch >= -719468 {
2570            days_since_epoch + 719468
2571        } else {
2572            days_since_epoch + 719468 - 146096
2573        }) / 146097;
2574        let doe = (days_since_epoch + 719468 - era * 146097) as u32;
2575        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2576        let y = (yoe as i32) + era * 400;
2577        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2578        let mp = (5 * doy + 2) / 153;
2579        let d = doy - (153 * mp + 2) / 5 + 1;
2580        let m = if mp < 10 { mp + 3 } else { mp - 9 };
2581        let year = if m <= 2 { y + 1 } else { y };
2582
2583        ((year, m, d), (hour, minute, second))
2584    }
2585
2586    /// Evaluates Excel's LET(name1, value1, [name2, value2, ...],
2587    /// calculation). Binds each name/value pair in order -- value2 (and
2588    /// later pairs, and the final calculation) can reference name1, per
2589    /// Excel's LET semantics -- by recursing one pair at a time so each
2590    /// level's scope chain only needs to borrow the *previous* level's
2591    /// binding rather than mutate a shared map (see `LetScope`).
2592    fn evaluate_let(
2593        &self,
2594        args: &[crate::core::parser::Expr],
2595        context: Option<&Context>,
2596        row: Option<usize>,
2597        col: Option<usize>,
2598        deps: &mut Vec<Dependency>,
2599        scope: &LetScope<'_>,
2600    ) -> Result<ResultData, EngineError> {
2601        use crate::core::parser::Expr;
2602
2603        if args.is_empty() || args.len().is_multiple_of(2) {
2604            return Ok(ResultData::Error("#VALUE!".to_string()));
2605        }
2606        if args.len() == 1 {
2607            return self.evaluate_ast(&args[0], context, row, col, deps, scope);
2608        }
2609
2610        let name = match &args[0] {
2611            Expr::Identifier(n) => n.as_str(),
2612            _ => return Ok(ResultData::Error("#VALUE!".to_string())),
2613        };
2614        let remaining_pairs = args.len() / 2 - 1;
2615        let is_duplicate = args[2..]
2616            .iter()
2617            .step_by(2)
2618            .take(remaining_pairs)
2619            .any(|a| matches!(a, Expr::Identifier(n2) if n2.eq_ignore_ascii_case(name)));
2620        if is_duplicate {
2621            return Ok(ResultData::Error("#VALUE!".to_string()));
2622        }
2623
2624        let value = self.evaluate_ast(&args[1], context, row, col, deps, scope)?;
2625        let inner_scope = LetScope::Bound {
2626            name,
2627            value: &value,
2628            parent: scope,
2629        };
2630        self.evaluate_let(&args[2..], context, row, col, deps, &inner_scope)
2631    }
2632
2633    /// Recognizes `expr` as a `LAMBDA(param1, [param2, ...], body)` call
2634    /// and, if so, returns its declared parameter names alongside the
2635    /// (still-unevaluated) body expression. Used by every function below
2636    /// that takes a lambda argument: the lambda is never evaluated as an
2637    /// ordinary function call (there's no value a bare LAMBDA could
2638    /// produce on its own -- see the `#CALC!` case in `evaluate_function`)
2639    /// -- callers instead inspect its raw AST here and invoke the body
2640    /// themselves, once per element, via `invoke_lambda`.
2641    fn extract_lambda(
2642        expr: &crate::core::parser::Expr,
2643    ) -> Option<(Vec<&str>, &crate::core::parser::Expr)> {
2644        use crate::core::parser::Expr;
2645        let Expr::FunctionCall { name, args } = expr else {
2646            return None;
2647        };
2648        if !name.eq_ignore_ascii_case("LAMBDA") || args.is_empty() {
2649            return None;
2650        }
2651        let (body, params) = args.split_last().unwrap();
2652        let param_names: Vec<&str> = params
2653            .iter()
2654            .filter_map(|p| match p {
2655                Expr::Identifier(n) => Some(n.as_str()),
2656                _ => None,
2657            })
2658            .collect();
2659        if param_names.len() != params.len() {
2660            return None;
2661        }
2662        Some((param_names, body))
2663    }
2664
2665    /// Evaluates a lambda's body with each of `params` bound (via
2666    /// `LetScope`) to the corresponding entry of `values`, which must be
2667    /// the same length. `values` is borrowed rather than consumed so
2668    /// callers can reuse per-element storage across many invocations
2669    /// (e.g. MAP calling this once per array element).
2670    #[allow(clippy::too_many_arguments)]
2671    fn invoke_lambda<'v>(
2672        &self,
2673        params: &[&str],
2674        values: &'v [ResultData],
2675        body: &crate::core::parser::Expr,
2676        context: Option<&Context>,
2677        row: Option<usize>,
2678        col: Option<usize>,
2679        deps: &mut Vec<Dependency>,
2680        scope: &LetScope<'v>,
2681    ) -> Result<ResultData, EngineError> {
2682        match (params.split_first(), values.split_first()) {
2683            (Some((&pname, prest)), Some((vfirst, vrest))) => {
2684                let inner_scope = LetScope::Bound {
2685                    name: pname,
2686                    value: vfirst,
2687                    parent: scope,
2688                };
2689                self.invoke_lambda(prest, vrest, body, context, row, col, deps, &inner_scope)
2690            }
2691            _ => self.evaluate_ast(body, context, row, col, deps, scope),
2692        }
2693    }
2694
2695    /// Flattens `expr` (evaluated) into a `Vec<ResultData>`, treating a
2696    /// scalar as a single-element array -- shared by MAP/REDUCE/SCAN,
2697    /// which all iterate an "array" argument that might just be one cell.
2698    fn eval_as_array(
2699        &self,
2700        expr: &crate::core::parser::Expr,
2701        context: Option<&Context>,
2702        row: Option<usize>,
2703        col: Option<usize>,
2704        deps: &mut Vec<Dependency>,
2705        scope: &LetScope<'_>,
2706    ) -> Result<Vec<ResultData>, EngineError> {
2707        Ok(
2708            match self.evaluate_ast(expr, context, row, col, deps, scope)? {
2709                ResultData::List(items) => Self::flatten_row_major(items).0,
2710                other => vec![other],
2711            },
2712        )
2713    }
2714
2715    /// `SEQUENCE`/`MUNIT` (unlike every array-*reshaping* function added
2716    /// this session) return their 2D result as a genuinely nested
2717    /// `List(List(row_values), ...)`, one inner list per row, rather than
2718    /// a flat row-major list -- that's the only place in this engine a
2719    /// `ResultData::List` still carries real shape. Detect that shape
2720    /// here and flatten it so downstream consumers (`array_shape`,
2721    /// `INDEX`, reshape functions) don't need to special-case it; a list
2722    /// that isn't uniformly nested (the flat convention) passes through
2723    /// unchanged, with `None` signaling "no shape recovered here".
2724    fn flatten_row_major(items: Vec<ResultData>) -> (Vec<ResultData>, Option<usize>) {
2725        if !items.is_empty() && items.iter().all(|v| matches!(v, ResultData::List(_))) {
2726            let cols = match &items[0] {
2727                ResultData::List(inner) => inner.len().max(1),
2728                _ => 1,
2729            };
2730            let flat = items
2731                .into_iter()
2732                .flat_map(|v| match v {
2733                    ResultData::List(inner) => inner,
2734                    other => vec![other],
2735                })
2736                .collect();
2737            (flat, Some(cols))
2738        } else {
2739            (items, None)
2740        }
2741    }
2742
2743    /// Infers `(flat_values, num_cols)` for an array-like argument: real
2744    /// column count from a `RangeRef`/`CellRef` AST node when available,
2745    /// otherwise treats the flattened result as a single row -- the same
2746    /// convention `INDEX`'s 3-arg form already uses (see its `num_cols`
2747    /// match on `args[0]`), since a computed/nested array result (e.g. the
2748    /// output of another array function) carries no shape of its own in
2749    /// this engine's flat-`ResultData::List` representation.
2750    fn array_shape(
2751        &self,
2752        expr: &crate::core::parser::Expr,
2753        context: Option<&Context>,
2754        row: Option<usize>,
2755        col: Option<usize>,
2756        deps: &mut Vec<Dependency>,
2757        scope: &LetScope<'_>,
2758    ) -> Result<(Vec<ResultData>, usize), EngineError> {
2759        use crate::core::parser::Expr;
2760        let items = match self.evaluate_ast(expr, context, row, col, deps, scope)? {
2761            ResultData::List(items) => items,
2762            other => vec![other],
2763        };
2764        let (flat, nested_cols) = Self::flatten_row_major(items);
2765        if let Some(cols) = nested_cols {
2766            return Ok((flat, cols));
2767        }
2768        let num_cols = match expr {
2769            Expr::RangeRef {
2770                start_col, end_col, ..
2771            } => (end_col - start_col + 1).max(1),
2772            Expr::CellRef { .. } => 1,
2773            Expr::FunctionCall { name, args } => self
2774                .function_call_cols(name, args, context, row, col, deps, scope)
2775                .unwrap_or_else(|| flat.len().max(1)),
2776            _ => flat.len().max(1),
2777        };
2778        Ok((flat, num_cols))
2779    }
2780
2781    /// Recovers the column count an array-reshaping function call's result
2782    /// would have, purely from its argument expressions -- needed because
2783    /// this engine's flat `ResultData::List` carries no shape of its own,
2784    /// so nesting one of these calls inside another (e.g.
2785    /// `INDEX(EXPAND(A1:B2,3,3,0),3,3)`) requires recovering the 2D shape.
2786    /// Returns `None` for anything not in this known set, so callers fall
2787    /// back to the single-row assumption.
2788    #[allow(clippy::too_many_arguments)]
2789    fn function_call_cols(
2790        &self,
2791        name: &str,
2792        args: &[crate::core::parser::Expr],
2793        context: Option<&Context>,
2794        row: Option<usize>,
2795        col: Option<usize>,
2796        deps: &mut Vec<Dependency>,
2797        scope: &LetScope<'_>,
2798    ) -> Option<usize> {
2799        let mut upper = name.to_ascii_uppercase();
2800        if let Some(rest) = upper.strip_prefix("_XLFN.") {
2801            upper = rest.to_string();
2802        }
2803        if let Some(rest) = upper.strip_prefix("_XLWS.") {
2804            upper = rest.to_string();
2805        }
2806        match upper.as_str() {
2807            "TRANSPOSE" => {
2808                let (flat, cols) = self
2809                    .array_shape(args.first()?, context, row, col, deps, scope)
2810                    .ok()?;
2811                Some((flat.len().checked_div(cols).unwrap_or(0)).max(1))
2812            }
2813            "HSTACK" => {
2814                let mut total = 0usize;
2815                for a in args {
2816                    total += self.array_shape(a, context, row, col, deps, scope).ok()?.1;
2817                }
2818                Some(total)
2819            }
2820            "VSTACK" => {
2821                let mut max_cols = 0usize;
2822                for a in args {
2823                    max_cols =
2824                        max_cols.max(self.array_shape(a, context, row, col, deps, scope).ok()?.1);
2825                }
2826                Some(max_cols)
2827            }
2828            "CHOOSEROWS" => Some(
2829                self.array_shape(args.first()?, context, row, col, deps, scope)
2830                    .ok()?
2831                    .1,
2832            ),
2833            "CHOOSECOLS" => Some(args.len().saturating_sub(1).max(1)),
2834            "DROP" | "TAKE" => {
2835                let (_, cols) = self
2836                    .array_shape(args.first()?, context, row, col, deps, scope)
2837                    .ok()?;
2838                let is_take = upper == "TAKE";
2839                match args.get(2) {
2840                    Some(e) => {
2841                        let n = self
2842                            .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2843                            .unwrap_or(0.0) as isize;
2844                        let (s, e2) = Self::drop_take_bounds(cols as isize, n, is_take);
2845                        Some((e2 - s).max(0) as usize)
2846                    }
2847                    None => Some(if is_take { cols } else { 0 }),
2848                }
2849            }
2850            "EXPAND" => {
2851                let (_, cols) = self
2852                    .array_shape(args.first()?, context, row, col, deps, scope)
2853                    .ok()?;
2854                match args.get(2) {
2855                    Some(e) => Some(
2856                        self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2857                            .unwrap_or(cols as f64) as usize,
2858                    ),
2859                    None => Some(cols),
2860                }
2861            }
2862            "TOCOL" => Some(1),
2863            "WRAPROWS" => {
2864                let n = self
2865                    .to_f64(
2866                        &self
2867                            .evaluate_ast(args.get(1)?, context, row, col, deps, scope)
2868                            .ok()?,
2869                    )
2870                    .unwrap_or(1.0)
2871                    .max(1.0) as usize;
2872                Some(n)
2873            }
2874            "WRAPCOLS" => {
2875                let (flat, _) = self
2876                    .array_shape(args.first()?, context, row, col, deps, scope)
2877                    .ok()?;
2878                let wrap = self
2879                    .to_f64(
2880                        &self
2881                            .evaluate_ast(args.get(1)?, context, row, col, deps, scope)
2882                            .ok()?,
2883                    )
2884                    .unwrap_or(1.0)
2885                    .max(1.0) as usize;
2886                Some(flat.len().div_ceil(wrap).max(1))
2887            }
2888            "UNIQUE" | "SORT" | "SORTBY" | "FILTER" | "TRIMRANGE" => Some(
2889                self.array_shape(args.first()?, context, row, col, deps, scope)
2890                    .ok()?
2891                    .1,
2892            ),
2893            "SEQUENCE" => match args.get(1) {
2894                Some(e) => Some(
2895                    self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2896                        .unwrap_or(1.0)
2897                        .max(1.0) as usize,
2898                ),
2899                None => Some(1),
2900            },
2901            "MUNIT" => {
2902                let n = self
2903                    .to_f64(
2904                        &self
2905                            .evaluate_ast(args.first()?, context, row, col, deps, scope)
2906                            .ok()?,
2907                    )
2908                    .unwrap_or(1.0)
2909                    .max(1.0) as usize;
2910                Some(n)
2911            }
2912            "MAKEARRAY" => match args.get(1) {
2913                Some(e) => Some(
2914                    self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2915                        .unwrap_or(1.0)
2916                        .max(1.0) as usize,
2917                ),
2918                None => Some(1),
2919            },
2920            _ => None,
2921        }
2922    }
2923
2924    /// Shared `[start, end)` bound computation for `TAKE`/`DROP`: a
2925    /// positive count counts from the start, negative from the end;
2926    /// `is_take` selects which side of that split is kept.
2927    fn drop_take_bounds(total: isize, n: isize, is_take: bool) -> (isize, isize) {
2928        let n = n.clamp(-total, total);
2929        if is_take {
2930            if n >= 0 { (0, n) } else { (total + n, total) }
2931        } else if n >= 0 {
2932            (n, total)
2933        } else {
2934            (0, total + n)
2935        }
2936    }
2937
2938    /// Shared implementation for MAP/BYROW/BYCOL/REDUCE/SCAN/MAKEARRAY:
2939    /// each applies a `LAMBDA` argument to some shape of input (parallel
2940    /// arrays, rows, columns, an accumulator, or generated row/col
2941    /// indices) and collects the results -- see each branch for the
2942    /// specific shape. Dynamic-array results are returned as a flat,
2943    /// row-major `ResultData::List`, the same convention `SEQUENCE`/
2944    /// `MUNIT`/etc. already use, since this engine doesn't spill formulas
2945    /// across cells; callers pull out a single value with `INDEX`.
2946    #[allow(clippy::too_many_arguments)]
2947    fn evaluate_lambda_function(
2948        &self,
2949        func_name: &str,
2950        args: &[crate::core::parser::Expr],
2951        context: Option<&Context>,
2952        row: Option<usize>,
2953        col: Option<usize>,
2954        deps: &mut Vec<Dependency>,
2955        scope: &LetScope<'_>,
2956    ) -> Result<ResultData, EngineError> {
2957        use crate::core::parser::Expr;
2958
2959        match func_name {
2960            "MAP" => {
2961                if args.len() < 2 {
2962                    return Ok(ResultData::Error("#VALUE!".to_string()));
2963                }
2964                let (lambda_expr, array_exprs) = args.split_last().unwrap();
2965                let Some((params, body)) = Self::extract_lambda(lambda_expr) else {
2966                    return Ok(ResultData::Error("#VALUE!".to_string()));
2967                };
2968                if params.len() != array_exprs.len() {
2969                    return Ok(ResultData::Error("#VALUE!".to_string()));
2970                }
2971                let arrays: Vec<Vec<ResultData>> = array_exprs
2972                    .iter()
2973                    .map(|e| self.eval_as_array(e, context, row, col, deps, scope))
2974                    .collect::<Result<_, _>>()?;
2975                let len = arrays.iter().map(|a| a.len()).max().unwrap_or(0);
2976                let mut results = Vec::with_capacity(len);
2977                for i in 0..len {
2978                    let values: Vec<ResultData> = arrays
2979                        .iter()
2980                        .map(|a| a.get(i).cloned().unwrap_or(ResultData::None))
2981                        .collect();
2982                    results.push(
2983                        self.invoke_lambda(&params, &values, body, context, row, col, deps, scope)?,
2984                    );
2985                }
2986                Ok(ResultData::List(results))
2987            }
2988            "BYROW" | "BYCOL" => {
2989                if args.len() != 2 {
2990                    return Ok(ResultData::Error("#VALUE!".to_string()));
2991                }
2992                let Some((params, body)) = Self::extract_lambda(&args[1]) else {
2993                    return Ok(ResultData::Error("#VALUE!".to_string()));
2994                };
2995                if params.len() != 1 {
2996                    return Ok(ResultData::Error("#VALUE!".to_string()));
2997                }
2998                let num_cols = match &args[0] {
2999                    Expr::RangeRef {
3000                        start_col, end_col, ..
3001                    } => (end_col - start_col + 1).max(1),
3002                    _ => 1,
3003                };
3004                let flat = self.eval_as_array(&args[0], context, row, col, deps, scope)?;
3005                let num_rows = if num_cols == 0 {
3006                    0
3007                } else {
3008                    flat.len().div_ceil(num_cols)
3009                };
3010                let mut results = Vec::new();
3011                if func_name == "BYROW" {
3012                    for r in 0..num_rows {
3013                        let row_vals: Vec<ResultData> = (0..num_cols)
3014                            .filter_map(|c| flat.get(r * num_cols + c).cloned())
3015                            .collect();
3016                        let arg = vec![ResultData::List(row_vals)];
3017                        results.push(
3018                            self.invoke_lambda(
3019                                &params, &arg, body, context, row, col, deps, scope,
3020                            )?,
3021                        );
3022                    }
3023                } else {
3024                    for c in 0..num_cols {
3025                        let col_vals: Vec<ResultData> = (0..num_rows)
3026                            .filter_map(|r| flat.get(r * num_cols + c).cloned())
3027                            .collect();
3028                        let arg = vec![ResultData::List(col_vals)];
3029                        results.push(
3030                            self.invoke_lambda(
3031                                &params, &arg, body, context, row, col, deps, scope,
3032                            )?,
3033                        );
3034                    }
3035                }
3036                Ok(ResultData::List(results))
3037            }
3038            "REDUCE" | "SCAN" => {
3039                if args.len() != 2 && args.len() != 3 {
3040                    return Ok(ResultData::Error("#VALUE!".to_string()));
3041                }
3042                let lambda_idx = args.len() - 1;
3043                let array_idx = args.len() - 2;
3044                let Some((params, body)) = Self::extract_lambda(&args[lambda_idx]) else {
3045                    return Ok(ResultData::Error("#VALUE!".to_string()));
3046                };
3047                if params.len() != 2 {
3048                    return Ok(ResultData::Error("#VALUE!".to_string()));
3049                }
3050                let array = self.eval_as_array(&args[array_idx], context, row, col, deps, scope)?;
3051                let (mut acc, rest, mut history): (ResultData, &[ResultData], Vec<ResultData>) =
3052                    if args.len() == 3 {
3053                        let init = self.evaluate_ast(&args[0], context, row, col, deps, scope)?;
3054                        (init, &array[..], Vec::new())
3055                    } else {
3056                        match array.split_first() {
3057                            Some((first, rest)) => (first.clone(), rest, vec![first.clone()]),
3058                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
3059                        }
3060                    };
3061                for item in rest {
3062                    let call_args = [acc.clone(), item.clone()];
3063                    acc = self
3064                        .invoke_lambda(&params, &call_args, body, context, row, col, deps, scope)?;
3065                    history.push(acc.clone());
3066                }
3067                if func_name == "REDUCE" {
3068                    Ok(acc)
3069                } else {
3070                    Ok(ResultData::List(history))
3071                }
3072            }
3073            "MAKEARRAY" => {
3074                if args.len() != 3 {
3075                    return Ok(ResultData::Error("#VALUE!".to_string()));
3076                }
3077                let Some((params, body)) = Self::extract_lambda(&args[2]) else {
3078                    return Ok(ResultData::Error("#VALUE!".to_string()));
3079                };
3080                if params.len() != 2 {
3081                    return Ok(ResultData::Error("#VALUE!".to_string()));
3082                }
3083                let rows_val = self.evaluate_ast(&args[0], context, row, col, deps, scope)?;
3084                let cols_val = self.evaluate_ast(&args[1], context, row, col, deps, scope)?;
3085                let num_rows = self.to_f64(&rows_val).unwrap_or(0.0).max(0.0) as usize;
3086                let num_cols = self.to_f64(&cols_val).unwrap_or(0.0).max(0.0) as usize;
3087                let mut results = Vec::with_capacity(num_rows * num_cols);
3088                for r in 1..=num_rows {
3089                    for c in 1..=num_cols {
3090                        let call_args = [ResultData::Float(r as f64), ResultData::Float(c as f64)];
3091                        results.push(self.invoke_lambda(
3092                            &params, &call_args, body, context, row, col, deps, scope,
3093                        )?);
3094                    }
3095                }
3096                Ok(ResultData::List(results))
3097            }
3098            _ => unreachable!(),
3099        }
3100    }
3101
3102    /// Minimal A1-notation string parser for `INDIRECT`: `"A1"`,
3103    /// `"B2:C5"`, `"Sheet1!A1"`, `"Sheet1!A1:B2"`, with optional `$`
3104    /// absolute markers and an optional `'quoted sheet name'!` prefix.
3105    /// Deliberately small and local rather than shared with
3106    /// `visi/src/utils.rs`'s equivalent parser (`parse_cell_ref`/
3107    /// `parse_range_ref`): `visi-core` cannot depend on the `visi` crate
3108    /// (the dependency direction is the other way), so this necessarily
3109    /// duplicates that logic in miniature.
3110    fn parse_a1_reference(text: &str) -> Option<(Option<String>, usize, usize, usize, usize)> {
3111        let text = text.trim();
3112        let (sheet_part, ref_part) = match text.rfind('!') {
3113            Some(idx) => (Some(&text[..idx]), &text[idx + 1..]),
3114            None => (None, text),
3115        };
3116        let sheet = sheet_part.map(|s| s.trim().trim_matches('\'').to_string());
3117
3118        fn parse_cell(s: &str) -> Option<(usize, usize)> {
3119            let s = s.replace('$', "");
3120            let col_end = s.find(|c: char| c.is_ascii_digit())?;
3121            let (col_str, row_str) = s.split_at(col_end);
3122            if col_str.is_empty() || row_str.is_empty() {
3123                return None;
3124            }
3125            let mut col = 0usize;
3126            for ch in col_str.chars() {
3127                if !ch.is_ascii_alphabetic() {
3128                    return None;
3129                }
3130                col = col * 26 + (ch.to_ascii_uppercase() as usize - 'A' as usize + 1);
3131            }
3132            let row: usize = row_str.parse().ok()?;
3133            if row == 0 || col == 0 {
3134                return None;
3135            }
3136            Some((row - 1, col - 1))
3137        }
3138
3139        if let Some((start, end)) = ref_part.split_once(':') {
3140            let (r1, c1) = parse_cell(start)?;
3141            let (r2, c2) = parse_cell(end)?;
3142            Some((sheet, r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2)))
3143        } else {
3144            let (r, c) = parse_cell(ref_part)?;
3145            Some((sheet, r, c, r, c))
3146        }
3147    }
3148
3149    /// Reads a single cell, registering the appropriate local/remote
3150    /// dependency -- the same local-vs-remote branch used throughout this
3151    /// file (see e.g. `evaluate_ast`'s `Expr::CellRef` arm), factored out
3152    /// since `CELL`/`FORMULATEXT`/`ISFORMULA`/`INDIRECT`/`OFFSET` all need
3153    /// it for a reference resolved dynamically rather than parsed as an
3154    /// AST node.
3155    fn read_cell_with_deps(
3156        &self,
3157        sheet_opt: &Option<String>,
3158        r: usize,
3159        c: usize,
3160        context: Option<&Context>,
3161        deps: &mut Vec<Dependency>,
3162    ) -> ResultData {
3163        let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3164        if is_self {
3165            deps.push(Dependency::Local(CellRef::new(r, c)));
3166            self.get_result_data(&CellRef::new(r, c))
3167        } else if let Some(ctx) = context {
3168            let name = sheet_opt.clone().unwrap();
3169            deps.push(Dependency::Remote {
3170                sheet: name.clone(),
3171                cell: CellRef::new(r, c),
3172            });
3173            ctx.sheets
3174                .get(&name)
3175                .map(|s| s.get_result_data(&CellRef::new(r, c)))
3176                .unwrap_or(ResultData::None)
3177        } else {
3178            ResultData::None
3179        }
3180    }
3181
3182    /// Shared implementation for the range/reference-introspection and
3183    /// workbook-metadata functions: ROW/ROWS/COLUMN/COLUMNS need the raw
3184    /// reference's real bounds (not a flattened `evaluated_args` value);
3185    /// AREAS/ISREF are purely syntactic checks on the argument's AST
3186    /// shape; FORMULATEXT/ISFORMULA need the cell's raw source text;
3187    /// INDIRECT/OFFSET build a reference dynamically instead of relying
3188    /// on one already resolved at parse time; SHEET/SHEETS/CELL/INFO
3189    /// report workbook/environment metadata.
3190    #[allow(clippy::too_many_arguments)]
3191    fn evaluate_range_info_function(
3192        &self,
3193        func_name: &str,
3194        args: &[crate::core::parser::Expr],
3195        context: Option<&Context>,
3196        row: Option<usize>,
3197        col: Option<usize>,
3198        deps: &mut Vec<Dependency>,
3199        scope: &LetScope<'_>,
3200    ) -> Result<ResultData, EngineError> {
3201        use crate::core::parser::Expr;
3202
3203        match func_name {
3204            "ROW" => match args.first() {
3205                Some(arg) => match Self::range_bounds(arg) {
3206                    Some((_, start_row, _, end_row, _)) if end_row > start_row => {
3207                        Ok(ResultData::List(
3208                            (start_row..=end_row)
3209                                .map(|r| ResultData::Float((r + 1) as f64))
3210                                .collect(),
3211                        ))
3212                    }
3213                    Some((_, start_row, _, _, _)) => Ok(ResultData::Float((start_row + 1) as f64)),
3214                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3215                },
3216                None => match row {
3217                    Some(r) => Ok(ResultData::Float((r + 1) as f64)),
3218                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3219                },
3220            },
3221            "COLUMN" => match args.first() {
3222                Some(arg) => match Self::range_bounds(arg) {
3223                    Some((_, _, start_col, _, end_col)) if end_col > start_col => {
3224                        Ok(ResultData::List(
3225                            (start_col..=end_col)
3226                                .map(|c| ResultData::Float((c + 1) as f64))
3227                                .collect(),
3228                        ))
3229                    }
3230                    Some((_, _, start_col, _, _)) => Ok(ResultData::Float((start_col + 1) as f64)),
3231                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3232                },
3233                None => match col {
3234                    Some(c) => Ok(ResultData::Float((c + 1) as f64)),
3235                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3236                },
3237            },
3238            "ROWS" => {
3239                let Some(arg) = args.first() else {
3240                    return Ok(ResultData::Error("#VALUE!".to_string()));
3241                };
3242                let Some((sheet_opt, start_row, _, end_row, _)) = Self::range_bounds(arg) else {
3243                    return Ok(ResultData::Error("#VALUE!".to_string()));
3244                };
3245                let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3246                let actual_end_row = if end_row == usize::MAX {
3247                    if is_self {
3248                        self.row_count().saturating_sub(1)
3249                    } else {
3250                        context
3251                            .and_then(|ctx| sheet_opt.as_ref().and_then(|n| ctx.sheets.get(n)))
3252                            .map(|s| s.row_count().saturating_sub(1))
3253                            .unwrap_or(0)
3254                    }
3255                } else {
3256                    end_row
3257                };
3258                Ok(ResultData::Float(
3259                    (actual_end_row.saturating_sub(start_row) + 1) as f64,
3260                ))
3261            }
3262            "COLUMNS" => {
3263                let Some(arg) = args.first() else {
3264                    return Ok(ResultData::Error("#VALUE!".to_string()));
3265                };
3266                match Self::range_bounds(arg) {
3267                    Some((_, _, start_col, _, end_col)) => Ok(ResultData::Float(
3268                        (end_col.saturating_sub(start_col) + 1) as f64,
3269                    )),
3270                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3271                }
3272            }
3273            "AREAS" => {
3274                if args.is_empty() {
3275                    Ok(ResultData::Error("#VALUE!".to_string()))
3276                } else {
3277                    Ok(ResultData::Float(1.0))
3278                }
3279            }
3280            "ISREF" => Ok(ResultData::Boolean(matches!(
3281                args.first(),
3282                Some(Expr::CellRef { .. } | Expr::RangeRef { .. } | Expr::StructuredRef { .. })
3283            ))),
3284            "FORMULATEXT" | "ISFORMULA" => {
3285                let Some(arg) = args.first() else {
3286                    return Ok(ResultData::Error("#VALUE!".to_string()));
3287                };
3288                let Some((sheet_opt, r, c, _, _)) = Self::range_bounds(arg) else {
3289                    return Ok(ResultData::Error("#VALUE!".to_string()));
3290                };
3291                let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3292                let src = if is_self {
3293                    deps.push(Dependency::Local(CellRef::new(r, c)));
3294                    self.get_src_str(&CellRef::new(r, c))
3295                } else if let Some(ctx) = context {
3296                    let name = sheet_opt.unwrap();
3297                    deps.push(Dependency::Remote {
3298                        sheet: name.clone(),
3299                        cell: CellRef::new(r, c),
3300                    });
3301                    ctx.sheets
3302                        .get(&name)
3303                        .map(|s| s.get_src_str(&CellRef::new(r, c)))
3304                        .unwrap_or_default()
3305                } else {
3306                    String::new()
3307                };
3308                let is_formula = src.starts_with('=');
3309                if func_name == "ISFORMULA" {
3310                    Ok(ResultData::Boolean(is_formula))
3311                } else if is_formula {
3312                    Ok(ResultData::String(src))
3313                } else {
3314                    Ok(ResultData::Error("#N/A".to_string()))
3315                }
3316            }
3317            "SHEETS" => Ok(ResultData::Float(
3318                context.map(|c| c.sheets.len() + 1).unwrap_or(1) as f64,
3319            )),
3320            "SHEET" => {
3321                let sheet_name = match args.first() {
3322                    None => Some(self.name.clone()),
3323                    Some(arg) => match Self::range_bounds(arg) {
3324                        Some((sheet_opt, ..)) => {
3325                            Some(sheet_opt.unwrap_or_else(|| self.name.clone()))
3326                        }
3327                        None => self
3328                            .evaluate_ast(arg, context, row, col, deps, scope)
3329                            .ok()
3330                            .map(|v| v.to_string()),
3331                    },
3332                };
3333
3334                match sheet_name {
3335                    Some(name) => {
3336                        let ordinal = context
3337                            .and_then(|c| {
3338                                c.sheet_order
3339                                    .iter()
3340                                    .position(|n| n.eq_ignore_ascii_case(&name))
3341                            })
3342                            .map(|i| i + 1)
3343                            .unwrap_or(1);
3344                        Ok(ResultData::Float(ordinal as f64))
3345                    }
3346                    None => Ok(ResultData::Error("#N/A".to_string())),
3347                }
3348            }
3349            "CELL" => {
3350                if args.is_empty() {
3351                    return Ok(ResultData::Error("#VALUE!".to_string()));
3352                }
3353                let info_type = self
3354                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3355                    .to_string()
3356                    .to_lowercase();
3357                let bounds = args.get(1).and_then(Self::range_bounds);
3358                match info_type.as_str() {
3359                    "row" => match bounds.map(|b| b.1).or(row) {
3360                        Some(r) => Ok(ResultData::Float((r + 1) as f64)),
3361                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3362                    },
3363                    "col" => match bounds {
3364                        Some((_, _, c, _, _)) => Ok(ResultData::Float((c + 1) as f64)),
3365                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3366                    },
3367                    "address" => match bounds {
3368                        Some((_, r, c, _, _)) => Ok(ResultData::String(format!(
3369                            "${}${}",
3370                            crate::core::parser::col_idx_to_letters(c),
3371                            r + 1
3372                        ))),
3373                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3374                    },
3375                    "contents" => match bounds {
3376                        Some((sheet_opt, r, c, _, _)) => {
3377                            Ok(self.read_cell_with_deps(&sheet_opt, r, c, context, deps))
3378                        }
3379                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3380                    },
3381                    _ => Ok(ResultData::Error("#VALUE!".to_string())),
3382                }
3383            }
3384            "INFO" => {
3385                if args.is_empty() {
3386                    return Ok(ResultData::Error("#VALUE!".to_string()));
3387                }
3388                let info_type = self
3389                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3390                    .to_string()
3391                    .to_lowercase();
3392                match info_type.as_str() {
3393                    "numfile" => Ok(ResultData::Float(
3394                        context.map(|c| c.sheets.len() + 1).unwrap_or(1) as f64,
3395                    )),
3396                    "release" => Ok(ResultData::String("16.0".to_string())),
3397                    "system" => Ok(ResultData::String(
3398                        if cfg!(target_os = "macos") {
3399                            "mac"
3400                        } else {
3401                            "pcdos"
3402                        }
3403                        .to_string(),
3404                    )),
3405                    _ => Ok(ResultData::Error("#VALUE!".to_string())),
3406                }
3407            }
3408            "INDIRECT" => {
3409                if args.is_empty() {
3410                    return Ok(ResultData::Error("#VALUE!".to_string()));
3411                }
3412                let text = self
3413                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3414                    .to_string();
3415                let a1_style = match args.get(1) {
3416                    Some(a) => self.to_bool(&self.evaluate_ast(a, context, row, col, deps, scope)?),
3417                    None => true,
3418                };
3419                if !a1_style {
3420                    return Ok(ResultData::Error("#VALUE!".to_string()));
3421                }
3422                match Self::parse_a1_reference(&text) {
3423                    Some((sheet_opt, start_row, start_col, end_row, end_col)) => {
3424                        if start_row == end_row && start_col == end_col {
3425                            Ok(self.read_cell_with_deps(
3426                                &sheet_opt, start_row, start_col, context, deps,
3427                            ))
3428                        } else {
3429                            match self.materialize_range(
3430                                &sheet_opt, start_row, start_col, end_row, end_col, context,
3431                            ) {
3432                                Some(grid) => {
3433                                    Ok(ResultData::List(grid.into_iter().flatten().collect()))
3434                                }
3435                                None => Ok(ResultData::Error("#REF!".to_string())),
3436                            }
3437                        }
3438                    }
3439                    None => Ok(ResultData::Error("#REF!".to_string())),
3440                }
3441            }
3442            "OFFSET" => {
3443                if args.len() < 3 {
3444                    return Ok(ResultData::Error("#VALUE!".to_string()));
3445                }
3446                let Some((sheet_opt, base_row, base_col, base_end_row, base_end_col)) =
3447                    Self::range_bounds(&args[0])
3448                else {
3449                    return Ok(ResultData::Error("#VALUE!".to_string()));
3450                };
3451                let row_offset = self
3452                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3453                    .unwrap_or(0.0) as isize;
3454                let col_offset = self
3455                    .to_f64(&self.evaluate_ast(&args[2], context, row, col, deps, scope)?)
3456                    .unwrap_or(0.0) as isize;
3457                let base_height = (base_end_row.saturating_sub(base_row) + 1) as isize;
3458                let base_width = (base_end_col.saturating_sub(base_col) + 1) as isize;
3459                let height = match args.get(3) {
3460                    Some(a) => self
3461                        .to_f64(&self.evaluate_ast(a, context, row, col, deps, scope)?)
3462                        .unwrap_or(base_height as f64) as isize,
3463                    None => base_height,
3464                };
3465                let width = match args.get(4) {
3466                    Some(a) => self
3467                        .to_f64(&self.evaluate_ast(a, context, row, col, deps, scope)?)
3468                        .unwrap_or(base_width as f64) as isize,
3469                    None => base_width,
3470                };
3471                let new_row = base_row as isize + row_offset;
3472                let new_col = base_col as isize + col_offset;
3473                if new_row < 0 || new_col < 0 || height <= 0 || width <= 0 {
3474                    return Ok(ResultData::Error("#REF!".to_string()));
3475                }
3476                let (start_row, start_col) = (new_row as usize, new_col as usize);
3477                let (end_row, end_col) = (
3478                    start_row + (height - 1) as usize,
3479                    start_col + (width - 1) as usize,
3480                );
3481                if start_row == end_row && start_col == end_col {
3482                    Ok(self.read_cell_with_deps(&sheet_opt, start_row, start_col, context, deps))
3483                } else {
3484                    match self.materialize_range(
3485                        &sheet_opt, start_row, start_col, end_row, end_col, context,
3486                    ) {
3487                        Some(grid) => Ok(ResultData::List(grid.into_iter().flatten().collect())),
3488                        None => Ok(ResultData::Error("#REF!".to_string())),
3489                    }
3490                }
3491            }
3492            _ => unreachable!(),
3493        }
3494    }
3495
3496    /// `GETPIVOTDATA(data_field, pivot_table_ref, [field, item]...)`.
3497    /// `pivot_table_ref` must stay an unevaluated cell reference (not a
3498    /// flattened value) so its sheet/row/col can be matched against
3499    /// `context.pivot_tables`' rendered destination ranges -- the same
3500    /// reason `ROW`/`OFFSET`/etc. go through `evaluate_range_info_function`
3501    /// instead of the generic eagerly-evaluated-args path below.
3502    fn evaluate_getpivotdata(
3503        &self,
3504        args: &[crate::core::parser::Expr],
3505        context: Option<&Context>,
3506        row: Option<usize>,
3507        col: Option<usize>,
3508        deps: &mut Vec<Dependency>,
3509        scope: &LetScope<'_>,
3510    ) -> Result<ResultData, EngineError> {
3511        if args.len() < 2 || !(args.len() - 2).is_multiple_of(2) {
3512            return Ok(ResultData::Error("#VALUE!".to_string()));
3513        }
3514
3515        let data_field = self
3516            .evaluate_ast(&args[0], context, row, col, deps, scope)?
3517            .to_string();
3518
3519        let (sheet_opt, target_row, target_col, _, _) = match Self::range_bounds(&args[1]) {
3520            Some(bounds) => bounds,
3521            None => return Ok(ResultData::Error("#REF!".to_string())),
3522        };
3523        self.read_cell_with_deps(&sheet_opt, target_row, target_col, context, deps);
3524
3525        let sheet_id = match &sheet_opt {
3526            None => self.id,
3527            Some(name) if name == &self.name => self.id,
3528            Some(name) => match context.and_then(|c| c.sheets.get(name)) {
3529                Some(s) => s.id,
3530                None => return Ok(ResultData::Error("#REF!".to_string())),
3531            },
3532        };
3533
3534        let pivot_tables = context.map(|c| c.pivot_tables).unwrap_or(&[]);
3535        let pivot = match pivot_tables.iter().find(|p| {
3536            p.dest_sheet_id == sheet_id
3537                && p.last_output_end_row
3538                    .is_some_and(|end| target_row >= p.dest_row && target_row <= end)
3539                && p.last_output_end_col
3540                    .is_some_and(|end| target_col >= p.dest_col && target_col <= end)
3541        }) {
3542            Some(p) => p,
3543            None => return Ok(ResultData::Error("#REF!".to_string())),
3544        };
3545
3546        let mut criteria: Vec<(String, String)> = Vec::new();
3547        let mut i = 2;
3548        while i < args.len() {
3549            let field = self
3550                .evaluate_ast(&args[i], context, row, col, deps, scope)?
3551                .to_string();
3552            let item = self
3553                .evaluate_ast(&args[i + 1], context, row, col, deps, scope)?
3554                .to_string();
3555            criteria.push((field, item));
3556            i += 2;
3557        }
3558
3559        let mut sheet_refs: Vec<&Sheet> = context
3560            .map(|c| c.sheets.values().copied().collect())
3561            .unwrap_or_default();
3562        sheet_refs.push(self);
3563
3564        match crate::core::pivot::getpivotdata(&sheet_refs, pivot, &data_field, &criteria) {
3565            Ok(v) => Ok(v),
3566            Err(e) => Ok(ResultData::Error(e)),
3567        }
3568    }
3569
3570    /// Shared implementation for the dynamic-array reshaping functions.
3571    /// All operate on `array_shape`'s `(flat, num_cols)` view and return a
3572    /// flat, row-major `ResultData::List` -- the same convention
3573    /// `SEQUENCE`/`MUNIT`/`MAKEARRAY`/etc. already use, since this engine
3574    /// doesn't spill formulas across cells (a caller pulls out a single
3575    /// value with `INDEX`, or consumes the whole list with e.g. `SUM`).
3576    ///
3577    /// Known simplifications, each accepted given limited fuzzing time
3578    /// against real Excel for this batch: `UNIQUE`'s `by_col` and `SORT`'s
3579    /// `by_col` arguments are ignored (both always operate row-wise);
3580    /// `SORTBY` only supports a single `by_array`/`sort_order` pair, not
3581    /// the documented repeating list; `XMATCH`'s wildcard match mode and
3582    /// binary/reverse search modes aren't implemented (falls through to a
3583    /// forward linear scan).
3584    #[allow(clippy::too_many_arguments)]
3585    fn evaluate_array_reshape_function(
3586        &self,
3587        func_name: &str,
3588        args: &[crate::core::parser::Expr],
3589        context: Option<&Context>,
3590        row: Option<usize>,
3591        col: Option<usize>,
3592        deps: &mut Vec<Dependency>,
3593        scope: &LetScope<'_>,
3594    ) -> Result<ResultData, EngineError> {
3595        match func_name {
3596            "TRANSPOSE" => {
3597                let Some(arg) = args.first() else {
3598                    return Ok(ResultData::Error("#VALUE!".to_string()));
3599                };
3600                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3601                let rows = flat.len().checked_div(cols).unwrap_or(0);
3602                let mut result = Vec::with_capacity(flat.len());
3603                for c in 0..cols {
3604                    for r in 0..rows {
3605                        result.push(flat[r * cols + c].clone());
3606                    }
3607                }
3608                Ok(ResultData::List(result))
3609            }
3610            "HSTACK" | "VSTACK" => {
3611                if args.is_empty() {
3612                    return Ok(ResultData::Error("#VALUE!".to_string()));
3613                }
3614                let mut shapes = Vec::with_capacity(args.len());
3615                for a in args {
3616                    shapes.push(self.array_shape(a, context, row, col, deps, scope)?);
3617                }
3618                let mut result = Vec::new();
3619                if func_name == "HSTACK" {
3620                    let max_rows = shapes
3621                        .iter()
3622                        .map(|(f, c)| if *c == 0 { 0 } else { f.len() / c })
3623                        .max()
3624                        .unwrap_or(0);
3625                    for r in 0..max_rows {
3626                        for (flat, cols) in &shapes {
3627                            let rows = if *cols == 0 { 0 } else { flat.len() / cols };
3628                            for c in 0..*cols {
3629                                result.push(if r < rows {
3630                                    flat[r * cols + c].clone()
3631                                } else {
3632                                    ResultData::Error("#N/A".to_string())
3633                                });
3634                            }
3635                        }
3636                    }
3637                } else {
3638                    let max_cols = shapes.iter().map(|(_, c)| *c).max().unwrap_or(0);
3639                    for (flat, cols) in &shapes {
3640                        let rows = if *cols == 0 { 0 } else { flat.len() / cols };
3641                        for r in 0..rows {
3642                            for c in 0..max_cols {
3643                                result.push(if c < *cols {
3644                                    flat[r * cols + c].clone()
3645                                } else {
3646                                    ResultData::Error("#N/A".to_string())
3647                                });
3648                            }
3649                        }
3650                    }
3651                }
3652                Ok(ResultData::List(result))
3653            }
3654            "CHOOSEROWS" | "CHOOSECOLS" => {
3655                if args.len() < 2 {
3656                    return Ok(ResultData::Error("#VALUE!".to_string()));
3657                }
3658                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3659                let rows = flat.len().checked_div(cols).unwrap_or(0);
3660                let total = if func_name == "CHOOSEROWS" {
3661                    rows
3662                } else {
3663                    cols
3664                } as isize;
3665                let mut indices = Vec::with_capacity(args.len() - 1);
3666                for idx_expr in &args[1..] {
3667                    let n = self
3668                        .to_f64(&self.evaluate_ast(idx_expr, context, row, col, deps, scope)?)
3669                        .unwrap_or(0.0) as isize;
3670                    let real_idx = if n < 0 { total + n } else { n - 1 };
3671                    if real_idx < 0 || real_idx >= total {
3672                        return Ok(ResultData::Error("#VALUE!".to_string()));
3673                    }
3674                    indices.push(real_idx as usize);
3675                }
3676                let mut result = Vec::new();
3677                if func_name == "CHOOSEROWS" {
3678                    for r in indices {
3679                        for c in 0..cols {
3680                            result.push(flat[r * cols + c].clone());
3681                        }
3682                    }
3683                } else {
3684                    for r in 0..rows {
3685                        for &c in &indices {
3686                            result.push(flat[r * cols + c].clone());
3687                        }
3688                    }
3689                }
3690                Ok(ResultData::List(result))
3691            }
3692            "DROP" | "TAKE" => {
3693                if args.len() < 2 {
3694                    return Ok(ResultData::Error("#VALUE!".to_string()));
3695                }
3696                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3697                let num_rows = flat.len().checked_div(cols).unwrap_or(0) as isize;
3698                let is_take = func_name == "TAKE";
3699                let rows_n = self
3700                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3701                    .unwrap_or(0.0) as isize;
3702                let cols_n = match args.get(2) {
3703                    Some(e) => self
3704                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3705                        .unwrap_or(0.0) as isize,
3706                    None => {
3707                        if is_take {
3708                            cols as isize
3709                        } else {
3710                            0
3711                        }
3712                    }
3713                };
3714                let (row_start, row_end) = Self::drop_take_bounds(num_rows, rows_n, is_take);
3715                let (col_start, col_end) = Self::drop_take_bounds(cols as isize, cols_n, is_take);
3716                if row_start >= row_end || col_start >= col_end {
3717                    return Ok(ResultData::Error("#CALC!".to_string()));
3718                }
3719                let mut result = Vec::new();
3720                for r in row_start..row_end {
3721                    for c in col_start..col_end {
3722                        result.push(flat[(r as usize) * cols + (c as usize)].clone());
3723                    }
3724                }
3725                Ok(ResultData::List(result))
3726            }
3727            "EXPAND" => {
3728                if args.len() < 2 {
3729                    return Ok(ResultData::Error("#VALUE!".to_string()));
3730                }
3731                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3732                let orig_rows = flat.len().checked_div(cols).unwrap_or(0);
3733                let new_rows = self
3734                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3735                    .unwrap_or(orig_rows as f64) as usize;
3736                let new_cols = match args.get(2) {
3737                    Some(e) => self
3738                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3739                        .unwrap_or(cols as f64) as usize,
3740                    None => cols,
3741                };
3742                let pad = match args.get(3) {
3743                    Some(e) => self.evaluate_ast(e, context, row, col, deps, scope)?,
3744                    None => ResultData::Error("#N/A".to_string()),
3745                };
3746                if new_rows < orig_rows || new_cols < cols {
3747                    return Ok(ResultData::Error("#VALUE!".to_string()));
3748                }
3749                let mut result = Vec::with_capacity(new_rows * new_cols);
3750                for r in 0..new_rows {
3751                    for c in 0..new_cols {
3752                        result.push(if r < orig_rows && c < cols {
3753                            flat[r * cols + c].clone()
3754                        } else {
3755                            pad.clone()
3756                        });
3757                    }
3758                }
3759                Ok(ResultData::List(result))
3760            }
3761            "TOCOL" | "TOROW" => {
3762                let Some(arg) = args.first() else {
3763                    return Ok(ResultData::Error("#VALUE!".to_string()));
3764                };
3765                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3766                let rows = flat.len().checked_div(cols).unwrap_or(0);
3767                let ignore = match args.get(1) {
3768                    Some(e) => self
3769                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3770                        .unwrap_or(0.0) as i64,
3771                    None => 0,
3772                };
3773                let scan_by_col = match args.get(2) {
3774                    Some(e) => self.to_bool(&self.evaluate_ast(e, context, row, col, deps, scope)?),
3775                    None => false,
3776                };
3777                let ordered: Vec<ResultData> = if scan_by_col {
3778                    let mut v = Vec::with_capacity(flat.len());
3779                    for c in 0..cols {
3780                        for r in 0..rows {
3781                            v.push(flat[r * cols + c].clone());
3782                        }
3783                    }
3784                    v
3785                } else {
3786                    flat
3787                };
3788                let filtered: Vec<ResultData> = ordered
3789                    .into_iter()
3790                    .filter(|v| match ignore {
3791                        1 => !matches!(v, ResultData::None),
3792                        2 => !matches!(v, ResultData::Error(_)),
3793                        3 => !matches!(v, ResultData::None | ResultData::Error(_)),
3794                        _ => true,
3795                    })
3796                    .collect();
3797                Ok(ResultData::List(filtered))
3798            }
3799            "WRAPROWS" | "WRAPCOLS" => {
3800                if args.len() < 2 {
3801                    return Ok(ResultData::Error("#VALUE!".to_string()));
3802                }
3803                let (flat, _cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3804                let wrap = self
3805                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3806                    .unwrap_or(1.0)
3807                    .max(1.0) as usize;
3808                let pad = match args.get(2) {
3809                    Some(e) => self.evaluate_ast(e, context, row, col, deps, scope)?,
3810                    None => ResultData::Error("#N/A".to_string()),
3811                };
3812                if func_name == "WRAPROWS" {
3813                    let mut result = flat;
3814                    let rem = result.len() % wrap;
3815                    if rem != 0 {
3816                        result.extend(std::iter::repeat_n(pad, wrap - rem));
3817                    }
3818                    Ok(ResultData::List(result))
3819                } else {
3820                    let num_result_cols = flat.len().div_ceil(wrap).max(1);
3821                    let total = wrap * num_result_cols;
3822                    let mut result = Vec::with_capacity(total);
3823                    for i in 0..total {
3824                        let col = i / wrap;
3825                        let r = i % wrap;
3826                        let target = r * num_result_cols + col;
3827                        while result.len() <= target {
3828                            result.push(pad.clone());
3829                        }
3830                        if i < flat.len() {
3831                            result[target] = flat[i].clone();
3832                        }
3833                    }
3834                    Ok(ResultData::List(result))
3835                }
3836            }
3837            "UNIQUE" => {
3838                let Some(arg) = args.first() else {
3839                    return Ok(ResultData::Error("#VALUE!".to_string()));
3840                };
3841                let (flat, _cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3842                let exactly_once = match args.get(2) {
3843                    Some(e) => self.to_bool(&self.evaluate_ast(e, context, row, col, deps, scope)?),
3844                    None => false,
3845                };
3846                let mut seen: Vec<(String, ResultData, usize)> = Vec::new();
3847                for v in &flat {
3848                    let key = match v {
3849                        ResultData::None => "blank:".to_string(),
3850                        ResultData::Boolean(b) => format!("bool:{b}"),
3851                        ResultData::Integer(i) => format!("num:{}", *i as f64),
3852                        ResultData::Float(f) => format!("num:{f}"),
3853                        ResultData::String(s) => format!("str:{s}"),
3854                        ResultData::Error(e) => format!("err:{e}"),
3855                        ResultData::List(_) | ResultData::Dict(_) => format!("other:{v}"),
3856                    };
3857                    match seen.iter_mut().find(|(k, ..)| k == &key) {
3858                        Some(entry) => entry.2 += 1,
3859                        None => seen.push((key, v.clone(), 1)),
3860                    }
3861                }
3862                let result: Vec<ResultData> = seen
3863                    .into_iter()
3864                    .filter(|(_, _, count)| !exactly_once || *count == 1)
3865                    .map(|(_, v, _)| v)
3866                    .collect();
3867                Ok(ResultData::List(result))
3868            }
3869            "SORT" => {
3870                let Some(arg) = args.first() else {
3871                    return Ok(ResultData::Error("#VALUE!".to_string()));
3872                };
3873                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3874                let rows = flat.len().checked_div(cols).unwrap_or(0);
3875                let sort_index = match args.get(1) {
3876                    Some(e) => self
3877                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3878                        .unwrap_or(1.0) as usize,
3879                    None => 1,
3880                };
3881                let sort_order = match args.get(2) {
3882                    Some(e) => self
3883                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3884                        .unwrap_or(1.0),
3885                    None => 1.0,
3886                };
3887                let col_idx = sort_index.saturating_sub(1).min(cols.saturating_sub(1));
3888                let mut row_indices: Vec<usize> = (0..rows).collect();
3889                row_indices.sort_by(|&a, &b| {
3890                    Self::sort_compare_blanks_last(
3891                        &flat[a * cols + col_idx],
3892                        &flat[b * cols + col_idx],
3893                        sort_order,
3894                    )
3895                });
3896                let mut result = Vec::with_capacity(flat.len());
3897                for r in row_indices {
3898                    for c in 0..cols {
3899                        result.push(flat[r * cols + c].clone());
3900                    }
3901                }
3902                Ok(ResultData::List(result))
3903            }
3904            "SORTBY" => {
3905                if args.len() < 2 {
3906                    return Ok(ResultData::Error("#VALUE!".to_string()));
3907                }
3908                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3909                let rows = flat.len().checked_div(cols).unwrap_or(0);
3910                let by = self.eval_as_array(&args[1], context, row, col, deps, scope)?;
3911                let order = match args.get(2) {
3912                    Some(e) => self
3913                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3914                        .unwrap_or(1.0),
3915                    None => 1.0,
3916                };
3917                let mut row_indices: Vec<usize> = (0..rows).collect();
3918                row_indices.sort_by(|&a, &b| {
3919                    let va = by.get(a).cloned().unwrap_or(ResultData::None);
3920                    let vb = by.get(b).cloned().unwrap_or(ResultData::None);
3921                    Self::sort_compare_blanks_last(&va, &vb, order)
3922                });
3923                let mut result = Vec::with_capacity(flat.len());
3924                for r in row_indices {
3925                    for c in 0..cols {
3926                        result.push(flat[r * cols + c].clone());
3927                    }
3928                }
3929                Ok(ResultData::List(result))
3930            }
3931            "FILTER" => {
3932                if args.len() < 2 {
3933                    return Ok(ResultData::Error("#VALUE!".to_string()));
3934                }
3935                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3936                let rows = flat.len().checked_div(cols).unwrap_or(0);
3937                let include = self.eval_as_array(&args[1], context, row, col, deps, scope)?;
3938                let mut result = Vec::new();
3939                for r in 0..rows {
3940                    let keep = include.get(r).map(|v| self.to_bool(v)).unwrap_or(false);
3941                    if keep {
3942                        for c in 0..cols {
3943                            result.push(flat[r * cols + c].clone());
3944                        }
3945                    }
3946                }
3947                if result.is_empty() {
3948                    match args.get(2) {
3949                        Some(e) => Ok(self.evaluate_ast(e, context, row, col, deps, scope)?),
3950                        None => Ok(ResultData::Error("#CALC!".to_string())),
3951                    }
3952                } else {
3953                    Ok(ResultData::List(result))
3954                }
3955            }
3956            "TRIMRANGE" => {
3957                let Some(arg) = args.first() else {
3958                    return Ok(ResultData::Error("#VALUE!".to_string()));
3959                };
3960                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3961                let rows = flat.len().checked_div(cols).unwrap_or(0);
3962                let is_blank = |v: &ResultData| {
3963                    matches!(v, ResultData::None)
3964                        || matches!(v, ResultData::String(s) if s.is_empty())
3965                };
3966                let row_blank = |r: usize| (0..cols).all(|c| is_blank(&flat[r * cols + c]));
3967                let col_blank = |c: usize| (0..rows).all(|r| is_blank(&flat[r * cols + c]));
3968                let mut r_start = 0;
3969                while r_start < rows && row_blank(r_start) {
3970                    r_start += 1;
3971                }
3972                let mut r_end = rows;
3973                while r_end > r_start && row_blank(r_end - 1) {
3974                    r_end -= 1;
3975                }
3976                let mut c_start = 0;
3977                while c_start < cols && col_blank(c_start) {
3978                    c_start += 1;
3979                }
3980                let mut c_end = cols;
3981                while c_end > c_start && col_blank(c_end - 1) {
3982                    c_end -= 1;
3983                }
3984                let mut result = Vec::new();
3985                for r in r_start..r_end {
3986                    for c in c_start..c_end {
3987                        result.push(flat[r * cols + c].clone());
3988                    }
3989                }
3990                Ok(ResultData::List(result))
3991            }
3992            _ => unreachable!(),
3993        }
3994    }
3995
3996    /// The raw text typed into a cell -- `"10"`, `"=SUM(A1:A2)"` -- or `None`
3997    /// if the cell is outside the sheet's allocated grid.
3998    ///
3999    /// This is the input, not the result; see [`Sheet::get_result_data`] for
4000    /// the computed value and `Sheet::get_display_string` for what a user
4001    /// should see.
4002    pub fn get_src(&self, cell: &CellRef) -> Option<&String> {
4003        let col = self.columns.get(cell.col);
4004        if let Some(col) = col {
4005            col.src.get(cell.row)
4006        } else {
4007            None
4008        }
4009    }
4010
4011    /// [`Sheet::get_src`] with an out-of-range cell flattened to an owned
4012    /// empty string.
4013    pub fn get_src_str(&self, cell: &CellRef) -> String {
4014        let col = self.columns.get(cell.col);
4015        if let Some(col) = col {
4016            col.src.get(cell.row).cloned().unwrap_or("".to_string())
4017        } else {
4018            "".to_string()
4019        }
4020    }
4021
4022    /// [`Sheet::get_src`] as a borrowed `&str`, for callers that only read.
4023    pub fn get_src_str_ref(&self, cell: &CellRef) -> Option<&str> {
4024        let col = self.columns.get(cell.col)?;
4025        col.src.get(cell.row).map(|s| s.as_str())
4026    }
4027
4028    /// The word surrounding `char_offset` in a cell's source text, as a
4029    /// half-open range of character (not byte) indices -- what an editor needs
4030    /// for word-wise selection. See [`get_word_boundaries_from_str`].
4031    pub fn get_word_boundaries(&self, cell: &CellRef, char_offset: usize) -> (usize, usize) {
4032        let text = self.get_src_str(cell);
4033        get_word_boundaries_from_str(&text, char_offset)
4034    }
4035}