Skip to main content

visi_core/core/engine/sheet/
mod.rs

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