Skip to main content

ravenrs_sheet/engine/cmd/
formulas.rs

1//! Formulas tab: AutoSum, Insert Function, names, auditing, calculation.
2
3use crate::base::{CellError, CellRef, RangeRef, Value};
4use crate::model::{CalcMode, Cell, DefinedName, Formula, Workbook};
5use serde_json::{Value as Json, json};
6
7use super::{
8    CommandSpec, EngineError, Result, Session, always, bad, bool_param, cell_param, cmd, edit, f64_param, has_doc, ok, stash, str_param,
9    target_range, target_ranges, target_sheet, u32_param,
10};
11
12pub fn specs() -> Vec<CommandSpec> {
13    vec![
14        cmd!(
15            "formulas.autoSum",
16            "AutoSum",
17            ["Formulas", "Function Library"],
18            Some("Cmd+Shift+T"),
19            "{function?: SUM|AVERAGE|COUNT|MAX|MIN, range?}",
20            has_doc,
21            auto_sum
22        ),
23        cmd!(
24            "formulas.insertFunction",
25            "Insert Function…",
26            ["Formulas", "Function Library"],
27            Some("Shift+F3"),
28            "{name?: \"VLOOKUP\"} (UI opens the dialog; with a name, starts the formula in the active cell)",
29            has_doc,
30            insert_function
31        ),
32        cmd!(query "formulas.functions", "List Functions", [], None, "{category?, search?} → [{name, category, signature, description}]", always, list_functions),
33        cmd!(
34            "formulas.defineName",
35            "Define Name…",
36            ["Formulas", "Defined Names"],
37            None,
38            "{name, refersTo?: \"=Sheet1!$A$1:$A$10\" (default: the selection), scope?: \"Workbook\"|sheet name, comment?, oldName?, oldScope?, force?: bool} (oldName: edit that name (Name Manager › Edit); a new name also renames it in formulas; a hidden name GridCraft keeps its own data in, `_gc_<key>_<n>` or `_sc_<key>`, is refused unless force is true)",
39            has_doc,
40            define_name
41        ),
42        cmd!(
43            "formulas.deleteName",
44            "Delete Name",
45            [],
46            None,
47            "{name, scope?: \"Workbook\"|sheet name (default: every scope), force?: bool} (a hidden name GridCraft keeps its own data in, `_gc_<key>_<n>` or `_sc_<key>`, is refused unless force is true)",
48            has_doc,
49            delete_name
50        ),
51        cmd!(query "formulas.nameManager", "Name Manager", ["Formulas", "Defined Names"], Some("Cmd+F3"), "{} → names with values", has_doc, name_manager),
52        cmd!(
53            "formulas.createFromSelection",
54            "Create from Selection",
55            ["Formulas", "Defined Names"],
56            Some("Cmd+Shift+F3"),
57            "{range?, top?: true, left?: false, bottom?: false, right?: false, replace?: false (existing names are kept and listed in `skipped`), force?: false (labels of the shape GridCraft keeps its own data in, `_gc_<key>_<n>` and `_sc_<key>`, are listed in `skipped` unless force is true)}",
58            has_doc,
59            create_from_selection
60        ),
61        cmd!(
62            "formulas.pasteName",
63            "Paste Names…",
64            ["Formulas", "Defined Names"],
65            Some("F3"),
66            "{name?: \"Sales\" (one of the names listed; OK: puts the name at the caret of the cell being edited, or starts `=Sales` in the active cell), list?: true (Paste List: writes each name and what it refers to in two columns, from `at`), at?: cell (default the active cell)} → {names: [{name, refersTo}]}: the names the Paste Name dialog lists (workbook names and the active sheet's own, hidden names left out, in alphabetical order). With neither nothing changes; the menu opens the dialog.",
67            has_doc,
68            paste_name
69        ),
70        cmd!(query "formulas.tracePrecedents", "Trace Precedents", ["Formulas", "Formula Auditing"], None, "{cell?} → ranges", has_doc, trace_precedents),
71        cmd!(query "formulas.traceDependents", "Trace Dependents", ["Formulas", "Formula Auditing"], None, "{cell?} → cells", has_doc, trace_dependents),
72        cmd!(
73            query "formulas.traceError",
74            "Trace Error",
75            ["Formulas", "Formula Auditing"],
76            None,
77            "{cell?} → {sheet, cell, error, source: {sheet, cell}, arrows: [{from: {sheet, range}, to: {sheet, cell}, kind: \"error\"|\"precedent\"}], truncated}: from the cell, follows the first reference that shows the same error (in a range, its first cell showing it) back to the cell the error starts in, `source`. `error` arrows link the cells on the way; `precedent` arrows lead from the source's own references into it. A cell without an error gives `error` and `source` null and no arrows. A chain longer than 10,000 links stops there with `truncated` true and `source` null.",
78            has_doc,
79            trace_error
80        ),
81        cmd!(noundo "formulas.selectPrecedents", "Select Precedents", ["Formulas", "Formula Auditing"], Some("Ctrl+["), "{cell?, all?: true = every level (Ctrl+Shift+[)} → selects the precedent cells", has_doc, select_precedents),
82        cmd!(noundo "formulas.selectDependents", "Select Dependents", ["Formulas", "Formula Auditing"], Some("Ctrl+]"), "{cell?, all?: true = every level (Ctrl+Shift+])}", has_doc, select_dependents),
83        cmd!(noundo "formulas.removeArrows", "Remove Arrows", ["Formulas", "Formula Auditing"], None, "{}", has_doc, |_, _| ok()),
84        cmd!("formulas.showFormulas", "Show Formulas", ["Formulas", "Formula Auditing"], Some("Ctrl+`"), "{on?}", has_doc, show_formulas),
85        cmd!(
86            "formulas.convertToNumber",
87            "Convert to Number",
88            [],
89            None,
90            "{range?} → {converted} (numbers stored as text in the range or selection become numbers)",
91            has_doc,
92            convert_to_number
93        ),
94        cmd!(query "formulas.errorChecking", "Error Checking", ["Formulas", "Formula Auditing"], None, "{} → cells with errors", has_doc, error_checking),
95        cmd!(query "formulas.evaluateFormula", "Evaluate Formula", ["Formulas", "Formula Auditing"], None, "{cell?, formula?} → steps", has_doc, evaluate_formula),
96        cmd!(query "formulas.evaluate", "Evaluate", [], None, "{formula: \"=SUM(A1:A3)\", cell?} → value without changing the sheet; a formula that couldn't be entered in a cell is an error", has_doc, evaluate),
97        cmd!(noundo "formulas.calculateNow", "Calculate Now", ["Formulas", "Calculation"], Some("F9"), "{}", has_doc, calc_now),
98        cmd!(noundo "formulas.calculateSheet", "Calculate Sheet", ["Formulas", "Calculation"], Some("Shift+F9"), "{}", has_doc, calc_now),
99        cmd!(
100            "formulas.calculationOptions",
101            "Calculation Options",
102            ["Formulas", "Calculation"],
103            None,
104            "{mode?: automatic|automaticExceptTables|manual, iterative?: bool (Enable iterative calculation), maxIterations?: 1–32767 (100), maxChange?: number (0.001), precisionAsDisplayed?: bool (Set precision as displayed: numbers are stored as their format shows them; turning it on rounds every number constant permanently)} → the settings. File › Options › Formulas › Calculation options; stored in the workbook.",
105            has_doc,
106            calc_options
107        ),
108    ]
109}
110
111/// Whether a cell on the active sheet is empty.
112fn blank(sh: &crate::model::sheet::Sheet, row: u32, col: u32) -> bool {
113    sh.value(CellRef::new(row, col)).is_empty()
114}
115
116fn sum_formula(func: &str, from: CellRef, to: CellRef) -> String {
117    format!("={func}({})", RangeRef::new(from, to).a1())
118}
119
120/// Totals for a multi-cell selection as `(cell, formula)` writes. `None` when the sheet has no data.
121fn selection_totals(sh: &crate::model::sheet::Sheet, sel: RangeRef, func: &str) -> Result<Option<Vec<(CellRef, String)>>> {
122    // A selection: totals go in its empty last row or column, as Excel's do, and otherwise
123    // past it: to the right of a single row, below each column of anything taller. Columns
124    // past the sheet's data get no total (a whole-row selection doesn't write 16,384
125    // formulas), nor do rows past it, and all totals are one edit (one undo step).
126    let Some(used) = sh.cells.used_range() else { return Ok(None) };
127    if sel.start.row == sel.end.row { single_row_totals(sh, sel, used, func).map(Some) } else { block_totals(sh, sel, used, func).map(Some) }
128}
129
130/// One row: its total goes in its last cell when that is empty, else just after it.
131fn single_row_totals(sh: &crate::model::sheet::Sheet, sel: RangeRef, used: RangeRef, func: &str) -> Result<Vec<(CellRef, String)>> {
132    let row = sel.start.row;
133    let last_col = used.end.col.min(sel.end.col);
134    let end_col = if sel.is_full_rows() { last_col } else { sel.end.col };
135    let (data_end, out) = if end_col > sel.start.col && blank(sh, row, end_col) { (end_col - 1, end_col) } else { (end_col, end_col + 1) };
136    if out >= crate::base::MAX_COLS {
137        return Err(EngineError::Other("There's no room to the right of the selection for the total.".into()));
138    }
139    Ok(vec![(CellRef::new(row, out), sum_formula(func, CellRef::new(row, sel.start.col), CellRef::new(row, data_end)))])
140}
141
142/// A block of rows: each column's total below it, and each row's total in an empty last column.
143fn block_totals(sh: &crate::model::sheet::Sheet, sel: RangeRef, used: RangeRef, func: &str) -> Result<Vec<(CellRef, String)>> {
144    let mut writes: Vec<(CellRef, String)> = Vec::new();
145    let last_col = used.end.col.min(sel.end.col);
146    // An empty last column takes each row's total; rows past the data get none.
147    let rows_end = sel.end.row.min(used.end.row);
148    let right_blank = !sel.is_full_rows() && sel.end.col > sel.start.col && (sel.start.row..=rows_end).all(|r| blank(sh, r, sel.end.col));
149    let data_end_col = if right_blank { sel.end.col - 1 } else { last_col };
150    let last_row_blank = (sel.start.col..=last_col).all(|c| blank(sh, sel.end.row, c));
151    let (data_end_row, out_row) = if last_row_blank { (sel.end.row.saturating_sub(1), sel.end.row) } else { (sel.end.row, sel.end.row + 1) };
152    // Each column's total, below it: unless only the last column is there to take totals.
153    if last_row_blank || !right_blank {
154        if out_row >= crate::base::MAX_ROWS {
155            return Err(EngineError::Other("There's no room below the selection for the totals.".into()));
156        }
157        for c in sel.start.col..=data_end_col.min(last_col) {
158            writes.push((CellRef::new(out_row, c), sum_formula(func, CellRef::new(sel.start.row, c), CellRef::new(data_end_row, c))));
159        }
160    }
161    if right_blank {
162        for r in sel.start.row..=data_end_row.min(rows_end) {
163            writes.push((CellRef::new(r, sel.end.col), sum_formula(func, CellRef::new(r, sel.start.col), CellRef::new(r, data_end_col))));
164        }
165        // Both empty: the corner takes the total of the whole block.
166        if last_row_blank {
167            writes.push((
168                CellRef::new(sel.end.row, sel.end.col),
169                sum_formula(func, CellRef::new(sel.start.row, sel.start.col), CellRef::new(data_end_row, data_end_col)),
170            ));
171        }
172    }
173    Ok(writes)
174}
175
176/// The range a single cell's AutoSum guesses: the numbers above it, else the numbers to its left.
177fn guessed_range(sh: &crate::model::sheet::Sheet, at: CellRef) -> Option<RangeRef> {
178    let numeric = |c: CellRef| sh.value(c).is_number();
179    let mut range = None;
180    if at.row > 0 && (numeric(CellRef::new(at.row - 1, at.col)) || at.col == 0 || !numeric(CellRef::new(at.row, at.col - 1))) {
181        let mut top = at.row - 1;
182        while top > 0 && numeric(CellRef::new(top - 1, at.col)) {
183            top -= 1;
184        }
185        if numeric(CellRef::new(at.row - 1, at.col)) {
186            range = Some(RangeRef::new(CellRef::new(top, at.col), CellRef::new(at.row - 1, at.col)));
187        }
188    }
189    if range.is_none() && at.col > 0 && numeric(CellRef::new(at.row, at.col - 1)) {
190        let mut left = at.col - 1;
191        while left > 0 && numeric(CellRef::new(at.row, left - 1)) {
192            left -= 1;
193        }
194        range = Some(RangeRef::new(CellRef::new(at.row, left), CellRef::new(at.row, at.col - 1)));
195    }
196    range
197}
198
199fn auto_sum(s: &mut Session, p: &Json) -> Result<Json> {
200    let func = str_param(p, "function").unwrap_or("SUM").to_ascii_uppercase();
201    let d = s.doc()?;
202    let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
203    let sel = match str_param(p, "range") {
204        Some(_) => target_range(s, p)?,
205        None => d.selection.current(),
206    };
207    if !sel.is_single() {
208        let Some(writes) = selection_totals(sh, sel, &func)? else { return ok() };
209        let sheet = d.wb.active_sheet();
210        edit(s, |cx| {
211            for (c, f) in &writes {
212                let cell = super::edit::input_to_cell(f, sheet, *c, &mut cx.wb)?.unwrap_or_default();
213                cx.sheet_mut(sheet)?.set_cell(*c, cell);
214                cx.touch(sheet, *c);
215            }
216            Ok(())
217        })?;
218        return ok();
219    }
220    // Single cell: guess the range above, else to the left.
221    let at = sel.start;
222    let text = match guessed_range(sh, at) {
223        Some(r) => format!("={func}({})", r.a1()),
224        None => {
225            s.ui_requests.push(crate::engine::UiRequest::EditCell(Some(format!("={func}()"))));
226            return ok();
227        }
228    };
229    if p.get("enter").and_then(Json::as_bool) == Some(false) {
230        s.ui_requests.push(crate::engine::UiRequest::EditCell(Some(text.clone())));
231        return Ok(json!({"formula": text}));
232    }
233    s.execute("cell.set", json!({"cell": at.a1(), "input": text}))?;
234    Ok(json!({"formula": text}))
235}
236
237fn insert_function(s: &mut Session, p: &Json) -> Result<Json> {
238    match str_param(p, "name") {
239        Some(n) => {
240            s.ui_requests.push(crate::engine::UiRequest::EditCell(Some(format!("={}(", n.to_ascii_uppercase()))));
241            ok()
242        }
243        None => {
244            s.ui_requests.push(crate::engine::UiRequest::Dialog("insertFunction".into(), json!({})));
245            ok()
246        }
247    }
248}
249
250pub fn function_list() -> Vec<Json> {
251    let mut v: Vec<Json> = crate::functions::all()
252        .iter()
253        .map(|f| json!({"name": f.name, "category": format!("{:?}", f.category), "signature": f.signature, "description": f.description}))
254        .collect();
255    let specials: &[(&str, &str, &str, &str)] = &[
256        (
257            "IF",
258            "Logical",
259            "IF(logical_test, [value_if_true], [value_if_false])",
260            "Returns one value if a condition is true and another if it's false.",
261        ),
262        ("IFS", "Logical", "IFS(test1, value1, ...)", "Returns the value of the first true condition."),
263        ("IFERROR", "Logical", "IFERROR(value, value_if_error)", "Returns a fallback when a value is an error."),
264        ("IFNA", "Logical", "IFNA(value, value_if_na)", "Returns a fallback when a value is #N/A."),
265        ("CHOOSE", "Lookup", "CHOOSE(index_num, value1, [value2], ...)", "Picks a value from a list by position."),
266        ("SWITCH", "Logical", "SWITCH(expression, value1, result1, ..., [default])", "Matches a value against a list of cases."),
267        ("ROW", "Lookup", "ROW([reference])", "Row number of a reference."),
268        ("COLUMN", "Lookup", "COLUMN([reference])", "Column number of a reference."),
269        ("ROWS", "Lookup", "ROWS(array)", "Number of rows in a reference or array."),
270        ("COLUMNS", "Lookup", "COLUMNS(array)", "Number of columns in a reference or array."),
271        ("OFFSET", "Lookup", "OFFSET(reference, rows, cols, [height], [width])", "A reference shifted from a starting point."),
272        ("INDIRECT", "Lookup", "INDIRECT(ref_text, [a1])", "The reference named by a text string."),
273        ("INDEX", "Lookup", "INDEX(array, row_num, [column_num], [area_num])", "The value or reference at a position."),
274        ("SUBTOTAL", "MathTrig", "SUBTOTAL(function_num, ref1, ...)", "A subtotal that can skip hidden rows."),
275        ("AGGREGATE", "MathTrig", "AGGREGATE(function_num, options, ref1, ...)", "An aggregate that can skip hidden rows and errors."),
276        ("TEXT", "Text", "TEXT(value, format_text)", "Formats a number as text."),
277        ("LET", "Logical", "LET(name1, value1, ..., calculation)", "Names intermediate results."),
278        ("LAMBDA", "Logical", "LAMBDA([parameter1, ...], calculation)", "Creates a reusable custom function."),
279        ("MAP", "Logical", "MAP(array1, ..., lambda)", "Applies a LAMBDA to each value."),
280        ("REDUCE", "Logical", "REDUCE(initial_value, array, lambda)", "Accumulates an array into one value."),
281        ("SCAN", "Logical", "SCAN(initial_value, array, lambda)", "Running accumulation of an array."),
282        ("BYROW", "Logical", "BYROW(array, lambda)", "Applies a LAMBDA to each row."),
283        ("BYCOL", "Logical", "BYCOL(array, lambda)", "Applies a LAMBDA to each column."),
284        ("MAKEARRAY", "Logical", "MAKEARRAY(rows, cols, lambda)", "Builds an array from a LAMBDA."),
285        ("CELL", "Information", "CELL(info_type, [reference])", "Information about a cell."),
286        ("ISREF", "Information", "ISREF(value)", "TRUE for references."),
287        ("ISFORMULA", "Information", "ISFORMULA(reference)", "TRUE when the cell has a formula."),
288        ("FORMULATEXT", "Lookup", "FORMULATEXT(reference)", "The formula of a cell as text."),
289        ("SHEET", "Information", "SHEET([value])", "Sheet number."),
290        ("SHEETS", "Information", "SHEETS([reference])", "Number of sheets."),
291        ("AREAS", "Lookup", "AREAS(reference)", "Number of areas in a reference."),
292        ("TRIMRANGE", "Lookup", "TRIMRANGE(range, [trim_rows], [trim_cols])", "A range or array without its blank outer rows and columns."),
293        ("ISOMITTED", "Information", "ISOMITTED(argument)", "TRUE when a LAMBDA argument was left out."),
294    ];
295    for (n, c, sig, d) in specials {
296        if !v.iter().any(|x| x["name"] == *n) {
297            v.push(json!({"name": n, "category": c, "signature": sig, "description": d}));
298        }
299    }
300    v.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
301    v
302}
303
304fn list_functions(_: &mut Session, p: &Json) -> Result<Json> {
305    let cat = str_param(p, "category").map(str::to_ascii_lowercase);
306    let q = str_param(p, "search").map(str::to_ascii_lowercase);
307    let v: Vec<Json> = function_list()
308        .into_iter()
309        .filter(|f| cat.as_ref().is_none_or(|c| f["category"].as_str().is_some_and(|x| x.to_ascii_lowercase() == *c)))
310        .filter(|f| {
311            q.as_ref().is_none_or(|q| {
312                f["name"].as_str().is_some_and(|x| x.to_ascii_lowercase().contains(q.as_str()))
313                    || f["description"].as_str().is_some_and(|x| x.to_ascii_lowercase().contains(q.as_str()))
314            })
315        })
316        .collect();
317    Ok(Json::Array(v))
318}
319
320fn valid_name(n: &str) -> bool {
321    let mut chars = n.chars();
322    let Some(first) = chars.next() else { return false };
323    if !(first.is_alphabetic() || first == '_' || first == '\\') {
324        return false;
325    }
326    if n.chars().count() > 255 || CellRef::parse(n).is_some() || is_r1c1(n) {
327        return false;
328    }
329    n.chars().all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '\\' | '?'))
330}
331
332/// Looks like an R1C1 reference (`R`, `C`, `RC`, `R2`, `C3`, `R1C1`), which Excel doesn't
333/// accept as a name.
334pub(crate) fn is_r1c1(n: &str) -> bool {
335    let digits = |s: &str| s.bytes().take_while(u8::is_ascii_digit).count();
336    let mut rest = n;
337    if let Some(r) = rest.strip_prefix(['R', 'r']) {
338        rest = r.get(digits(r)..).unwrap_or("");
339    }
340    if let Some(c) = rest.strip_prefix(['C', 'c']) {
341        rest = c.get(digits(c)..).unwrap_or("");
342    }
343    rest.is_empty() && !n.is_empty()
344}
345
346fn define_name(s: &mut Session, p: &Json) -> Result<Json> {
347    let Some(name) = str_param(p, "name").map(str::to_string) else {
348        s.ui_requests.push(crate::engine::UiRequest::Dialog("defineName".into(), json!({})));
349        return ok();
350    };
351    if !valid_name(&name) {
352        return Err(EngineError::Other("The name that you entered is not valid.".into()));
353    }
354    for n in [Some(name.as_str()), str_param(p, "oldName")].into_iter().flatten() {
355        refuse_stash_name(&s.doc()?.wb, p, n)?;
356    }
357    let d = s.doc()?;
358    let scope = match str_param(p, "scope") {
359        None | Some("Workbook") => None,
360        Some(sh) => Some(d.wb.sheet_index(sh).ok_or_else(|| bad("formulas.defineName", "no such sheet"))?),
361    };
362    let refers = match str_param(p, "refersTo") {
363        // As in Excel, relative references are relative to the active cell; names keep them
364        // relative to A1, as files do.
365        Some(r) => match crate::formula::parse_name(r.trim_start_matches('=')) {
366            Ok(e) if crate::formula::adjust::has_relative_refs(&e) => {
367                let at = d.selection.active;
368                crate::formula::print(&crate::formula::adjust::shift_relative_wrapping(e, -(at.row as i64), -(at.col as i64)))
369            }
370            _ => r.trim_start_matches('=').to_string(),
371        },
372        None => {
373            let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
374            let r = d.selection.current();
375            let abs = |c: CellRef| format!("${}${}", crate::base::col_to_letters(c.col), c.row + 1);
376            let body = if r.is_single() { abs(r.start) } else { format!("{}:{}", abs(r.start), abs(r.end)) };
377            format!("{}!{}", crate::formula::quote_sheet(&sh.name), body)
378        }
379    };
380    if crate::formula::parse_name(&refers).is_err() {
381        return Err(EngineError::Other("There's a problem with this formula.".into()));
382    }
383    let comment = str_param(p, "comment").unwrap_or("").to_string();
384    // Editing an existing name (Name Manager › Edit…): it must exist, and its new name must not
385    // collide with another name in the target scope.
386    let old = match str_param(p, "oldName") {
387        Some(old_name) => {
388            let old_scope = match str_param(p, "oldScope") {
389                None | Some("Workbook") => None,
390                Some(sh) => Some(d.wb.sheet_index(sh).ok_or_else(|| bad("formulas.defineName", "no such sheet"))?),
391            };
392            if !d.wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(old_name) && n.scope == old_scope) {
393                return Err(bad("formulas.defineName", "no such name to edit"));
394            }
395            let same = old_name.eq_ignore_ascii_case(&name) && old_scope == scope;
396            if !same && d.wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(&name) && n.scope == scope) {
397                return Err(EngineError::Other("The name entered already exists. Enter a unique name.".into()));
398            }
399            Some((old_name.to_string(), old_scope))
400        }
401        None => None,
402    };
403    edit(s, |cx| {
404        if let Some((old_name, old_scope)) = &old {
405            cx.wb.names.retain(|n| !(n.name.eq_ignore_ascii_case(old_name) && n.scope == *old_scope));
406            if !old_name.eq_ignore_ascii_case(&name) {
407                rename_name_in_formulas(&mut cx.wb, old_name, *old_scope, &name);
408            }
409        }
410        cx.wb.names.retain(|n| !(n.name.eq_ignore_ascii_case(&name) && n.scope == scope));
411        cx.wb.names.push(DefinedName { name: name.clone(), scope, formula: refers.clone(), comment: comment.clone(), hidden: false });
412        cx.structural = true;
413        Ok(json!({"name": name, "refersTo": format!("={refers}")}))
414    })
415}
416
417/// Formulas that use a renamed name follow it: cell formulas and other names' definitions, on
418/// the sheets where `old` resolves to the renamed name (its own sheet for a sheet-scoped name;
419/// every sheet without a same-named sheet-level name for a workbook-level one).
420fn rename_name_in_formulas(wb: &mut crate::model::Workbook, old: &str, old_scope: Option<usize>, new: &str) {
421    let shadowed: Vec<usize> = wb.names.iter().filter(|n| n.name.eq_ignore_ascii_case(old)).filter_map(|n| n.scope).collect();
422    let applies = |sheet: usize| match old_scope {
423        Some(s) => s == sheet,
424        None => !shadowed.contains(&sheet),
425    };
426    // The renamed expression, or `None` when the name doesn't occur in it.
427    let rename = |e: crate::formula::Expr| {
428        let mut hit = false;
429        let out = e.map(&mut |x| match x {
430            crate::formula::Expr::Name(n) if n.eq_ignore_ascii_case(old) => {
431                hit = true;
432                crate::formula::Expr::Name(new.to_string())
433            }
434            o => o,
435        });
436        hit.then_some(out)
437    };
438    for i in 0..wb.sheets.len() {
439        if !applies(i) {
440            continue;
441        }
442        let Some(sh) = wb.sheet_mut(i) else { continue };
443        let formulas: Vec<(CellRef, std::sync::Arc<Formula>)> = sh.cells.iter().filter_map(|(c, x)| x.formula.clone().map(|f| (c, f))).collect();
444        for (c, f) in formulas {
445            let Some(e) = f.expr().and_then(rename) else { continue };
446            let mut nf = Formula::from_expr(e);
447            nf.array = f.array;
448            nf.dynamic = f.dynamic;
449            if let Some(cell) = sh.cells.get_mut(c) {
450                cell.formula = Some(std::sync::Arc::new(nf));
451            }
452        }
453    }
454    for n in wb.names.iter_mut() {
455        if n.scope.is_none_or(applies)
456            && let Some(e) = crate::formula::parse_name(&n.formula).ok().and_then(rename)
457        {
458            n.formula = crate::formula::print(&e);
459        }
460    }
461}
462
463/// Refuses a name of the shape GridCraft keeps its own workbook data in ([`super::stash`]):
464/// defining, renaming or deleting one would replace or remove part of an item (a stored macro,
465/// script or view) and leave the rest behind. `force: true` lets the call go ahead.
466/// Whether `name` is one the workbook holds GridCraft's own data in: a hidden workbook-scope
467/// name of an item's shape. A visible name of that shape is the person's own (see `stash`), and
468/// the name commands may change it.
469fn holds_gridcraft_data(wb: &Workbook, name: &str) -> bool {
470    // The stash's own rule, so the two never disagree: a legacy `_sc_` item is read visible or not.
471    stash::holds_item(wb, name)
472}
473
474fn refuse_stash_name(wb: &Workbook, p: &Json, name: &str) -> Result<()> {
475    if holds_gridcraft_data(wb, name) && !bool_param(p, "force").unwrap_or(false) {
476        return Err(EngineError::Other(format!(
477            "'{name}' is a name GridCraft keeps its own data in (stored macros, scripts or views). Pass `force: true` to change it anyway."
478        )));
479    }
480    Ok(())
481}
482
483fn delete_name(s: &mut Session, p: &Json) -> Result<Json> {
484    let name = str_param(p, "name").ok_or_else(|| bad("formulas.deleteName", "missing `name`"))?.to_string();
485    refuse_stash_name(&s.doc()?.wb, p, &name)?;
486    let scope = match str_param(p, "scope") {
487        None => None,
488        Some("Workbook") => Some(None),
489        Some(sh) => Some(Some(s.doc()?.wb.sheet_index(sh).ok_or_else(|| bad("formulas.deleteName", "no such sheet"))?)),
490    };
491    edit(s, |cx| {
492        let before = cx.wb.names.len();
493        cx.wb.names.retain(|n| !(n.name.eq_ignore_ascii_case(&name) && scope.is_none_or(|sc| n.scope == sc)));
494        if cx.wb.names.len() == before {
495            return Err(bad("formulas.deleteName", "no such name"));
496        }
497        cx.structural = true;
498        Ok(Json::Null)
499    })
500}
501
502fn name_manager(s: &mut Session, _: &Json) -> Result<Json> {
503    let d = s.doc()?;
504    let list: Vec<Json> = d
505        .wb
506        .names
507        .iter()
508        .filter(|n| !n.hidden)
509        .map(|n| {
510            let sheet = n.scope.unwrap_or(d.wb.active_sheet());
511            // parse_name, not evaluate's parse: a name's top-level commas are unions.
512            let v = crate::formula::parse_name(&n.formula)
513                .map_or(Value::Error(CellError::Name), |e| crate::calc::recalc::evaluate_expr(&d.wb, sheet, CellRef::default(), &e));
514            json!({"name": n.name, "refersTo": format!("={}", n.formula), "scope": n.scope.and_then(|i| d.wb.sheet(i)).map(|s| s.name.clone()).unwrap_or_else(|| "Workbook".into()), "value": crate::engine::cmd::inspect::value_json(&v), "comment": n.comment})
515        })
516        .collect();
517    Ok(Json::Array(list))
518}
519
520fn create_from_selection(s: &mut Session, p: &Json) -> Result<Json> {
521    let r = target_range(s, p)?;
522    let top = bool_param(p, "top").unwrap_or(true);
523    let left = bool_param(p, "left").unwrap_or(false);
524    let bottom = bool_param(p, "bottom").unwrap_or(false);
525    let right = bool_param(p, "right").unwrap_or(false);
526    let replace = bool_param(p, "replace").unwrap_or(false);
527    let d = s.doc()?;
528    let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
529    let q = crate::formula::quote_sheet(&sh.name);
530    let abs = |r: RangeRef| {
531        format!("{q}!${}${}:${}${}", crate::base::col_to_letters(r.start.col), r.start.row + 1, crate::base::col_to_letters(r.end.col), r.end.row + 1)
532    };
533    let clean = |t: String| -> String {
534        let mut n: String = t.trim().chars().map(|c| if c.is_alphanumeric() || c == '.' { c } else { '_' }).collect();
535        if !n.is_empty() && !valid_name(&n) {
536            n.insert(0, '_');
537        }
538        if valid_name(&n) { n } else { String::new() }
539    };
540    // The labels in the chosen edge rows/columns name the cells between them.
541    let (r0, r1) = (r.start.row + top as u32, r.end.row.saturating_sub(bottom as u32));
542    let (c0, c1) = (r.start.col + left as u32, r.end.col.saturating_sub(right as u32));
543    let mut made = Vec::new();
544    if r0 <= r1 && c0 <= c1 {
545        for (on, row) in [(top, r.start.row), (bottom, r.end.row)] {
546            for c in (c0..=c1).filter(|_| on) {
547                let name = clean(sh.value(CellRef::new(row, c)).display());
548                if !name.is_empty() {
549                    made.push((name, abs(RangeRef::new(CellRef::new(r0, c), CellRef::new(r1, c)))));
550                }
551            }
552        }
553        for (on, col) in [(left, r.start.col), (right, r.end.col)] {
554            for row in (r0..=r1).filter(|_| on) {
555                let name = clean(sh.value(CellRef::new(row, col)).display());
556                if !name.is_empty() {
557                    made.push((name, abs(RangeRef::new(CellRef::new(row, c0), CellRef::new(row, c1)))));
558                }
559            }
560        }
561    }
562    // Excel asks before replacing an existing name; here it's kept unless `replace` is set, and
563    // reported.
564    let exists = |n: &str| d.wb.names.iter().any(|x| x.name.eq_ignore_ascii_case(n) && x.scope.is_none());
565    // A label that names one of GridCraft's own hidden data names would replace part of an item
566    // (see `refuse_stash_name`).
567    let force = bool_param(p, "force").unwrap_or(false);
568    let ours = |n: &str| !force && holds_gridcraft_data(&d.wb, n);
569    let (made, skipped): (Vec<_>, Vec<_>) = made.into_iter().partition(|(n, _)| !ours(n) && (replace || !exists(n)));
570    let skipped: Vec<String> = skipped.into_iter().map(|(n, _)| n).collect();
571    let n = made.len();
572    edit(s, |cx| {
573        for (name, f) in made {
574            cx.wb.names.retain(|x| !(x.name.eq_ignore_ascii_case(&name) && x.scope.is_none()));
575            cx.wb.names.push(DefinedName { name, scope: None, formula: f, comment: String::new(), hidden: false });
576        }
577        cx.structural = true;
578        Ok(json!({"created": n, "skipped": skipped}))
579    })
580}
581
582/// The names Paste Names lists, as (name, refers-to text with `=`): the workbook's names and the
583/// active sheet's own, without hidden names, in alphabetical order. A sheet's name hides the
584/// workbook name it shares.
585pub fn pasteable_names(wb: &Workbook) -> Vec<(String, String)> {
586    let sheet = wb.active_sheet();
587    let shown = |local: bool| wb.names.iter().filter(move |n| !n.hidden && n.scope.is_some() == local && n.scope.is_none_or(|i| i == sheet));
588    let local: std::collections::HashSet<String> = shown(true).map(|n| crate::formula::fold_name(&n.name)).collect();
589    let mut out: Vec<(String, String)> = shown(true).map(|n| (n.name.clone(), format!("={}", n.formula))).collect();
590    for n in shown(false) {
591        if !local.contains(&crate::formula::fold_name(&n.name)) {
592            out.push((n.name.clone(), format!("={}", n.formula)));
593        }
594    }
595    out.sort_by_cached_key(|(n, _)| n.to_lowercase());
596    out
597}
598
599fn paste_name(s: &mut Session, p: &Json) -> Result<Json> {
600    let d = s.doc()?;
601    let names = pasteable_names(&d.wb);
602    let listed: Vec<Json> = names.iter().map(|(n, r)| json!({"name": n, "refersTo": r})).collect();
603    if !bool_param(p, "list").unwrap_or(false) {
604        if let Some(name) = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
605            let Some((listed_name, _)) = names.iter().find(|(n, _)| crate::formula::same_name(n, name)) else {
606                return Err(EngineError::Other(format!("There's no name \"{name}\" to paste.")));
607            };
608            s.ui_requests.push(crate::engine::UiRequest::InsertName(listed_name.clone()));
609        }
610        return Ok(json!({"names": listed}));
611    }
612    if names.is_empty() {
613        return Err(EngineError::Other("This workbook has no names to paste.".into()));
614    }
615    let at = cell_param(s, p, "at")?.unwrap_or(d.selection.active);
616    let rows = u32::try_from(names.len()).unwrap_or(u32::MAX);
617    if at.row.saturating_add(rows) > crate::base::MAX_ROWS || at.col.saturating_add(2) > crate::base::MAX_COLS {
618        return Err(EngineError::Other("There isn't room on the sheet for the list of names.".into()));
619    }
620    let sheet = d.wb.active_sheet();
621    let range = RangeRef::new(at, CellRef::new(at.row + rows - 1, at.col + 1));
622    edit(s, |cx| {
623        for (row, (name, refers)) in (at.row..).zip(&names) {
624            for (col, text) in [(at.col, name), (at.col + 1, refers)] {
625                let c = CellRef::new(row, col);
626                let sh = cx.sheet_mut(sheet)?;
627                let style = sh.style_id(c);
628                // The second column holds the refers-to text as a text value, with no formula.
629                sh.set_cell(c, Cell { value: Value::text(text.as_str()), formula: None, style, py: None });
630                cx.touch(sheet, c);
631            }
632        }
633        Ok(json!({"names": listed, "range": range.a1()}))
634    })
635}
636
637fn trace_precedents(s: &mut Session, p: &Json) -> Result<Json> {
638    let d = s.doc()?;
639    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
640    let areas = d.calc.graph.precedents_of((d.wb.active_sheet(), at));
641    let v: Vec<Json> = areas.iter().map(|a| json!({"sheet": d.wb.sheet(a.sheet).map(|s| s.name.clone()), "range": a.range.a1()})).collect();
642    Ok(Json::Array(v))
643}
644
645fn trace_dependents(s: &mut Session, p: &Json) -> Result<Json> {
646    let d = s.doc()?;
647    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
648    let mut deps = Vec::new();
649    d.calc.graph.dependents(d.wb.active_sheet(), at, &mut deps);
650    deps.sort();
651    deps.dedup();
652    let v: Vec<Json> = deps.iter().map(|(si, c)| json!({"sheet": d.wb.sheet(*si).map(|s| s.name.clone()), "cell": c.a1()})).collect();
653    Ok(Json::Array(v))
654}
655
656/// Trace Error goes back at most this many links; a longer chain stops there.
657const TRACE_ERROR_LINKS: usize = 10_000;
658
659/// Trace Error: from the cell, the first reference in its formula that shows the same error
660/// (in a range, the range's first cell showing it, row by row), then that cell's, and so on to the
661/// cell the error starts in: one none of whose references shows it, or one without references.
662/// Arrows link the cells on the way, and lead from the source's own references into it.
663fn trace_error(s: &mut Session, p: &Json) -> Result<Json> {
664    use std::collections::HashSet;
665    let d = s.doc()?;
666    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
667    let sheet = d.wb.active_sheet();
668    let name = |si: usize| d.wb.sheet(si).map(|sh| sh.name.clone());
669    let cell = |(si, c): (usize, CellRef)| json!({"sheet": name(si), "cell": c.a1()});
670    let Some(error) = d.wb.sheet(sheet).and_then(|sh| sh.value(at).as_error()) else {
671        return Ok(json!({"sheet": name(sheet), "cell": at.a1(), "error": null, "source": null, "arrows": [], "truncated": false}));
672    };
673    // A value spilled by a dynamic array comes from its anchor's formula.
674    let precedents = |(si, c): (usize, CellRef)| {
675        let anchor = d.wb.sheet(si).filter(|sh| sh.cell(c).is_none_or(|x| x.formula.is_none())).and_then(|sh| sh.spill_anchor(c));
676        d.calc.graph.precedents_of((si, anchor.unwrap_or(c)))
677    };
678    // The walk keeps to cells it hasn't been to, so a circular reference ends it.
679    let mut chain = vec![(sheet, at)];
680    let mut seen: HashSet<(usize, CellRef)> = HashSet::from([(sheet, at)]);
681    let mut truncated = false;
682    let mut last = (sheet, at);
683    while let Some(next) = precedents(last).iter().find_map(|a| first_showing(&d.wb, a, error)) {
684        if !seen.insert(next) {
685            break;
686        }
687        if chain.len() > TRACE_ERROR_LINKS {
688            truncated = true;
689            break;
690        }
691        chain.push(next);
692        last = next;
693    }
694    let mut arrows = Vec::new();
695    // A chain cut short hasn't reached its source.
696    if !truncated {
697        let mut drawn = HashSet::new();
698        for a in precedents(last) {
699            if drawn.insert(a) {
700                arrows.push(json!({"from": {"sheet": name(a.sheet), "range": a.range.a1()}, "to": cell(last), "kind": "precedent"}));
701            }
702        }
703    }
704    // From the source outwards, as the error travels.
705    for pair in chain.windows(2).rev() {
706        if let [to, from] = pair {
707            arrows.push(json!({"from": {"sheet": name(from.0), "range": from.1.a1()}, "to": cell(*to), "kind": "error"}));
708        }
709    }
710    let source = if truncated { Json::Null } else { cell(last) };
711    Ok(json!({"sheet": name(sheet), "cell": at.a1(), "error": error.as_str(), "source": source, "arrows": arrows, "truncated": truncated}))
712}
713
714/// The first cell of an area, row by row, that shows `error`: a stored cell or a value spilled
715/// into the area by a dynamic array.
716fn first_showing(wb: &Workbook, a: &crate::calc::Area, error: CellError) -> Option<(usize, CellRef)> {
717    let sh = wb.sheet(a.sheet)?;
718    let holds = |c: &Cell| !c.value.is_empty() || c.formula.is_some();
719    let stored = sh.cells.iter_range(a.range).find(|(_, c)| holds(c) && c.value.as_error() == Some(error)).map(|(at, _)| at);
720    let spilled = sh
721        .spill
722        .range(a.range.start..)
723        .take_while(|(at, _)| **at <= a.range.end)
724        .find(|(at, v)| a.range.contains(**at) && v.as_error() == Some(error) && !sh.cell(**at).is_some_and(holds))
725        .map(|(at, _)| *at);
726    stored.into_iter().chain(spilled).min().map(|c| (a.sheet, c))
727}
728
729/// Excel's Ctrl+[ / Ctrl+]: select the precedent (or dependent) cells. When they all live on
730/// another sheet, jump there. `all` walks every level (Ctrl+Shift+[ / Ctrl+Shift+]).
731fn select_trace_cells(s: &mut Session, p: &Json, precedents: bool) -> Result<Json> {
732    use std::collections::BTreeSet;
733    let d = s.doc()?;
734    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
735    let sheet = d.wb.active_sheet();
736    let all = bool_param(p, "all").unwrap_or(false);
737    // (sheet, a1) pairs to select; direct level keeps whole precedent ranges, transitive cells.
738    let mut found: BTreeSet<(usize, String)> = BTreeSet::new();
739    let mut seen: BTreeSet<(usize, CellRef)> = BTreeSet::new();
740    let mut frontier = vec![(sheet, at)];
741    seen.insert((sheet, at));
742    while let Some((ks, kc)) = frontier.pop() {
743        if precedents {
744            for a in d.calc.graph.precedents_of((ks, kc)) {
745                if all {
746                    // Walk only the stored cells inside the precedent area (sparse
747                    // range iterator, not the whole sheet): blanks are never selected
748                    // and only formula cells extend the frontier, so a formula late in
749                    // a large range is not skipped.
750                    let Some(sh) = d.wb.sheet(a.sheet) else { continue };
751                    for (c, cell) in sh.cells.iter_range(a.range) {
752                        if matches!(cell.value, Value::Empty) {
753                            continue;
754                        }
755                        if seen.insert((a.sheet, c)) {
756                            found.insert((a.sheet, c.a1()));
757                            if cell.formula.is_some() {
758                                frontier.push((a.sheet, c));
759                            }
760                        }
761                    }
762                } else {
763                    found.insert((a.sheet, a.range.a1()));
764                }
765            }
766        } else {
767            let mut deps = Vec::new();
768            d.calc.graph.dependents(ks, kc, &mut deps);
769            for k2 in deps {
770                if all {
771                    if seen.insert(k2) {
772                        found.insert((k2.0, k2.1.a1()));
773                        frontier.push(k2);
774                    }
775                } else {
776                    found.insert((k2.0, k2.1.a1()));
777                }
778            }
779        }
780        if !all {
781            break;
782        }
783    }
784    found.remove(&(sheet, at.a1()));
785    let name = if precedents { "precedent" } else { "dependent" };
786    let Some(si) = found.iter().find(|(si, _)| *si == sheet).or_else(|| found.first()).map(|(si, _)| *si) else {
787        return Err(bad("cell", format!("No {name} cells.")));
788    };
789    let list: Vec<String> = found.iter().filter(|(s, _)| *s == si).map(|(_, a)| a.clone()).collect();
790    let mut params = json!({ "range": list.join(",") });
791    if si != sheet {
792        params["sheet"] = json!(si);
793    }
794    s.execute("selection.set", params)
795}
796
797fn select_precedents(s: &mut Session, p: &Json) -> Result<Json> {
798    select_trace_cells(s, p, true)
799}
800
801fn select_dependents(s: &mut Session, p: &Json) -> Result<Json> {
802    select_trace_cells(s, p, false)
803}
804
805fn show_formulas(s: &mut Session, p: &Json) -> Result<Json> {
806    let sheet = s.doc()?.wb.active_sheet();
807    let cur = s.doc()?.wb.sheet(sheet).is_some_and(|sh| sh.show_formulas);
808    let on = bool_param(p, "on").unwrap_or(!cur);
809    edit(s, |cx| {
810        cx.sheet_mut(sheet)?.show_formulas = on;
811        Ok(json!({"on": on}))
812    })
813}
814
815/// The number a cell's text stands for when Excel's background check flags it as a number stored
816/// as text: a constant (not a formula's result) that would be a number if typed.
817pub fn number_stored_as_text(cell: &crate::model::Cell) -> Option<f64> {
818    match &cell.value {
819        Value::Text(t) if cell.formula.is_none() => crate::base::parse::number_stored_as_text(t),
820        _ => None,
821    }
822}
823
824/// Convert to Number: each number stored as text in the range becomes that number, keeping the
825/// cell's format.
826fn convert_to_number(s: &mut Session, p: &Json) -> Result<Json> {
827    let sheet = target_sheet(s, p)?;
828    let ranges = target_ranges(s, p)?;
829    edit(s, |cx| {
830        let mut converted = 0;
831        for r in &ranges {
832            let Some(r) = cx.wb.sheet(sheet).and_then(|sh| sh.used_range()).and_then(|u| r.intersection(&u)) else { continue };
833            let found: Vec<(CellRef, f64)> = cx
834                .wb
835                .sheet(sheet)
836                .map(|sh| sh.cells.iter_range(r).filter_map(|(c, x)| number_stored_as_text(x).map(|n| (c, n))).collect())
837                .unwrap_or_default();
838            let sh = cx.sheet_mut(sheet)?;
839            for (c, n) in &found {
840                sh.set_value(*c, Value::number(*n));
841            }
842            cx.changed.extend(found.iter().map(|(c, _)| (sheet, *c)));
843            converted += found.len();
844        }
845        Ok(json!({"converted": converted}))
846    })
847}
848
849fn error_checking(s: &mut Session, _: &Json) -> Result<Json> {
850    let d = s.doc()?;
851    let mut out = Vec::new();
852    for sh in &d.wb.sheets {
853        for (c, cell) in sh.cells.iter() {
854            if let Some(e) = cell.value.as_error() {
855                out.push(
856                    json!({"sheet": sh.name, "cell": c.a1(), "error": e.as_str(), "formula": cell.formula.as_ref().map(|f| format!("={}", f.text))}),
857                );
858            }
859            if out.len() >= 10_000 {
860                break;
861            }
862        }
863    }
864    let circ: Vec<String> = d
865        .calc
866        .circular
867        .iter()
868        .map(|(si, c)| format!("{}!{}", d.wb.sheet(*si).map(|s| crate::formula::quote_sheet(&s.name)).unwrap_or_default(), c.a1()))
869        .collect();
870    Ok(json!({"errors": out, "circular": circ}))
871}
872
873fn evaluate_formula(s: &mut Session, p: &Json) -> Result<Json> {
874    let d = s.doc()?;
875    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
876    let sheet = d.wb.active_sheet();
877    let text = match str_param(p, "formula") {
878        Some(f) => f.trim_start_matches('=').to_string(),
879        None => {
880            d.wb.sheet(sheet)
881                .and_then(|sh| sh.cell(at))
882                .and_then(|c| c.formula.as_ref())
883                .map(|f| f.text.clone())
884                .ok_or_else(|| EngineError::Other("The cell has no formula.".into()))?
885        }
886    };
887    let expr = crate::formula::parse(&text).map_err(|e| EngineError::Other(e.to_string()))?;
888    // Steps: each sub-expression (innermost first) with its value.
889    let mut steps = Vec::new();
890    let mut nodes = Vec::new();
891    expr.walk(&mut |e| {
892        // Borrowed, not cloned: copying every operator of a long chain A1+A2+… copies the
893        // rest of the chain each time (quadratic memory).
894        if matches!(e, crate::formula::Expr::Call(..) | crate::formula::Expr::Binary(..) | crate::formula::Expr::Ref(_)) {
895            nodes.push(e);
896        }
897    });
898    for e in nodes.into_iter().rev().take(200) {
899        let v = crate::calc::recalc::evaluate_expr(&d.wb, sheet, at, e);
900        steps.push(json!({"expression": crate::formula::print(e), "value": crate::engine::cmd::inspect::value_json(&v)}));
901    }
902    let result = crate::calc::recalc::evaluate_expr(&d.wb, sheet, at, &expr);
903    Ok(json!({"formula": format!("={text}"), "steps": steps, "result": crate::engine::cmd::inspect::value_json(&result)}))
904}
905
906/// Evaluates what typing `formula` into a cell would show. A formula `cell.set` would refuse (it
907/// doesn't parse, or calls a function with the wrong number of arguments) is an error that says
908/// why, not `#NAME?`; an unknown function name still evaluates to `#NAME?`, as in Excel.
909fn evaluate(s: &mut Session, p: &Json) -> Result<Json> {
910    let d = s.doc()?;
911    let f = str_param(p, "formula").ok_or_else(|| bad("formulas.evaluate", "missing `formula`"))?;
912    let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
913    let expr = crate::engine::cmd::edit::parse_typed_formula(f.strip_prefix('=').unwrap_or(f))?;
914    crate::engine::cmd::edit::check_arg_counts(&expr)?;
915    let v = crate::calc::recalc::evaluate_expr(&d.wb, d.wb.active_sheet(), at, &expr);
916    Ok(crate::engine::cmd::inspect::value_json(&v))
917}
918
919fn calc_now(s: &mut Session, _: &Json) -> Result<Json> {
920    let d = s.doc_mut()?;
921    let mut wb = (*d.wb).clone();
922    d.calc.recalc_all(&mut wb);
923    d.wb = std::sync::Arc::new(wb);
924    Ok(json!({"cells": d.calc.last_recalc_cells}))
925}
926
927fn calc_options(s: &mut Session, p: &Json) -> Result<Json> {
928    let mode = match str_param(p, "mode") {
929        Some("manual") => Some(CalcMode::Manual),
930        Some("automaticExceptTables") => Some(CalcMode::AutomaticExceptTables),
931        Some("automatic") => Some(CalcMode::Automatic),
932        _ => None,
933    };
934    edit(s, |cx| {
935        let before = cx.wb.calc.clone();
936        if let Some(m) = mode {
937            cx.wb.calc.mode = m;
938        }
939        if let Some(v) = bool_param(p, "iterative") {
940            cx.wb.calc.iterative = v;
941        }
942        if let Some(v) = u32_param(p, "maxIterations") {
943            cx.wb.calc.set_max_iterations(v);
944        }
945        if let Some(v) = f64_param(p, "maxChange") {
946            cx.wb.calc.set_max_change(v);
947        }
948        let precision = bool_param(p, "precisionAsDisplayed");
949        if let Some(v) = precision {
950            cx.wb.calc.precision_as_displayed = v;
951            if v && !before.precision_as_displayed {
952                crate::engine::cmd::round_all_constants_as_displayed(&mut cx.wb);
953            }
954        }
955        let c = &cx.wb.calc;
956        // Switching to automatic, or changing how circular references are calculated outside
957        // manual mode, recalculates the workbook.
958        let iteration_changed = (c.iterative, c.max_iterations, c.max_change) != (before.iterative, before.max_iterations, before.max_change);
959        // Switching precision as displayed recalculates the workbook, in manual mode too, so the
960        // values change as soon as it is switched on or off.
961        let precision_changed = c.precision_as_displayed != before.precision_as_displayed;
962        if mode == Some(CalcMode::Automatic) || (iteration_changed && c.mode != CalcMode::Manual) || precision_changed {
963            cx.structural = true;
964        }
965        Ok(json!({
966            "mode": format!("{:?}", c.mode),
967            "iterative": c.iterative,
968            "maxIterations": c.max_iterations,
969            "maxChange": c.max_change,
970            "precisionAsDisplayed": c.precision_as_displayed,
971        }))
972    })
973}
974
975#[cfg(test)]
976mod tests {
977    use super::*;
978
979    #[test]
980    fn names_with_relative_references() {
981        let mut s = Session::new();
982        s.new_workbook();
983        s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3]]})).unwrap();
984        // Defined from B2, `=Sheet1!A2` is "the cell to the left".
985        s.execute("selection.set", json!({"range": "B2"})).unwrap();
986        let r = s.execute("formulas.defineName", json!({"name": "LeftCell", "refersTo": "=Sheet1!A2"})).unwrap();
987        assert_eq!(r["refersTo"], "=Sheet1!XFD1");
988        s.execute("cell.set", json!({"cell": "B1", "input": "=LeftCell"})).unwrap();
989        s.execute("cell.set", json!({"cell": "B3", "input": "=LeftCell*10"})).unwrap();
990        let v = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["value"].clone();
991        assert_eq!(v(&mut s, "B1"), json!(1.0));
992        assert_eq!(v(&mut s, "B3"), json!(30.0));
993        // Absolute references are kept as they are.
994        let r = s.execute("formulas.defineName", json!({"name": "First", "refersTo": "=Sheet1!$A$1"})).unwrap();
995        assert_eq!(r["refersTo"], "=Sheet1!$A$1");
996    }
997
998    #[test]
999    fn the_name_commands_leave_gridcraft_data_alone() {
1000        // Defining, renaming or deleting a name of a stored item's shape would replace or remove
1001        // one of its chunks and leave the rest behind: refused unless `force` is true.
1002        let mut s = Session::new();
1003        s.new_workbook();
1004        let wb = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb);
1005        let text = "m".repeat(9_000);
1006        stash::set(wb, "macro_1", &text).unwrap();
1007        wb.names.push(DefinedName { name: "_sc_watch".into(), scope: None, formula: "\"[]\"".into(), comment: String::new(), hidden: true });
1008        // A legacy item is read whether or not it is hidden, so a visible one is data too.
1009        wb.names.push(DefinedName { name: "_sc_old".into(), scope: None, formula: "\"[]\"".into(), comment: String::new(), hidden: false });
1010        let names = |s: &Session| s.doc().unwrap().wb.names.clone();
1011        let before = names(&s);
1012        assert!(before.len() >= 3, "the item takes two chunks");
1013        for params in [
1014            json!({"name": "_gc_macro_1_0", "refersTo": "=1"}),
1015            json!({"name": "_SC_Watch", "refersTo": "=1"}),
1016            json!({"name": "Mine", "refersTo": "=1", "oldName": "_gc_macro_1_1"}),
1017            json!({"name": "_gc_macro_1_0", "refersTo": "=1", "force": false}),
1018            json!({"name": "Mine2", "refersTo": "=1", "oldName": "_SC_old"}),
1019        ] {
1020            let err = s.execute("formulas.defineName", params.clone()).unwrap_err().to_string();
1021            assert!(err.contains("force"), "{params}: {err}");
1022        }
1023        for name in ["_gc_macro_1_1", "_sc_watch", "_sc_old"] {
1024            let err = s.execute("formulas.deleteName", json!({"name": name})).unwrap_err().to_string();
1025            assert!(err.contains("force"), "{name}: {err}");
1026        }
1027        s.execute("range.setValues", json!({"range": "A1:B2", "values": [["_gc_macro_1_0", "Rate"], [1, 2]]})).unwrap();
1028        let r = s.execute("formulas.createFromSelection", json!({"range": "A1:B2", "top": true, "replace": true})).unwrap();
1029        assert_eq!((r["created"].clone(), r["skipped"].clone()), (json!(1), json!(["_gc_macro_1_0"])));
1030        let after = names(&s);
1031        assert_eq!(after.iter().filter(|n| n.name != "Rate").cloned().collect::<Vec<_>>(), before);
1032        assert_eq!(stash::get(&s.doc().unwrap().wb, "macro_1").as_deref(), Some(text.as_str()));
1033        // A visible name of the shape is the person's own (the stash never reads it): the name
1034        // commands change and remove it without `force`, as Name Manager cannot pass one.
1035        let own = DefinedName { name: "_gc_rate_0".into(), scope: None, formula: "=1".into(), comment: String::new(), hidden: false };
1036        std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).names.push(own.clone());
1037        s.execute("formulas.defineName", json!({"name": "Rate0", "refersTo": "=2", "oldName": "_gc_rate_0"})).unwrap();
1038        s.execute("formulas.deleteName", json!({"name": "Rate0"})).unwrap();
1039        std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).names.push(own);
1040        s.execute("formulas.deleteName", json!({"name": "_gc_rate_0"})).unwrap();
1041        assert_eq!(stash::get(&s.doc().unwrap().wb, "macro_1").as_deref(), Some(text.as_str()), "the hidden item is untouched");
1042        // Ordinary names and forced calls go ahead.
1043        s.execute("formulas.deleteName", json!({"name": "Rate"})).unwrap();
1044        s.execute("formulas.defineName", json!({"name": "_gc_macro_1_1", "refersTo": "=1", "force": true})).unwrap();
1045        s.execute("formulas.deleteName", json!({"name": "_gc_macro_1_1", "force": true})).unwrap();
1046        assert!(!names(&s).iter().any(|n| n.name == "_gc_macro_1_1"));
1047    }
1048
1049    #[test]
1050    fn arabic_defined_names_count_characters_not_bytes() {
1051        let mut s = Session::new();
1052        s.new_workbook();
1053        let arabic = |n: usize| "س".repeat(n);
1054        for ok in [200, 255] {
1055            s.execute("formulas.defineName", json!({"name": arabic(ok), "refersTo": "=1"})).unwrap();
1056        }
1057        assert!(s.execute("formulas.defineName", json!({"name": arabic(256), "refersTo": "=1"})).is_err());
1058        s.execute("range.setValues", json!({"range": "A1", "values": [[format!("ع{}", arabic(199))], [1]]})).unwrap();
1059        let r = s.execute("formulas.createFromSelection", json!({"range": "A1:A2", "top": true})).unwrap();
1060        assert_eq!(r["created"], 1);
1061    }
1062
1063    #[test]
1064    fn names_referring_to_several_ranges() {
1065        let mut s = Session::new();
1066        s.new_workbook();
1067        s.execute("range.setValues", json!({"range": "A1:C2", "values": [[1, 0, 5], [2, 0, 7]]})).unwrap();
1068        // Excel writes and accepts a union in a name without parentheses.
1069        let r = s.execute("formulas.defineName", json!({"name": "u", "refersTo": "=Sheet1!$A$1:$A$2,Sheet1!$C$1:$C$2"})).unwrap();
1070        assert_eq!(r["refersTo"], "=Sheet1!$A$1:$A$2,Sheet1!$C$1:$C$2");
1071        for (cell, input) in [("E1", "=SUM(u)"), ("E2", "=AREAS(u)"), ("E3", "=MIN(u)"), ("E4", "=INDEX(u,2,1,2)")] {
1072            s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap();
1073        }
1074        let v = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["value"].clone();
1075        assert_eq!(v(&mut s, "E1"), json!(15.0));
1076        assert_eq!(v(&mut s, "E2"), json!(2.0));
1077        assert_eq!(v(&mut s, "E3"), json!(1.0));
1078        assert_eq!(v(&mut s, "E4"), json!(7.0));
1079        // Renaming the sheet follows every area, and the definition keeps Excel's form.
1080        s.execute("sheet.rename", json!({"name": "Data"})).unwrap();
1081        let name = s.doc().unwrap().wb.names.iter().find(|n| n.name == "u").map(|n| n.formula.clone());
1082        assert_eq!(name.as_deref(), Some("Data!$A$1:$A$2,Data!$C$1:$C$2"));
1083        assert_eq!(v(&mut s, "E1"), json!(15.0));
1084    }
1085
1086    #[test]
1087    fn name_manager_previews_a_name_over_several_ranges() {
1088        let mut s = Session::new();
1089        s.new_workbook();
1090        s.execute("range.setValues", json!({"range": "A1:C2", "values": [[1, 0, 5], [2, 0, 7]]})).unwrap();
1091        s.execute("formulas.defineName", json!({"name": "u", "refersTo": "=Sheet1!$A$1:$A$2,Sheet1!$C$1:$C$2"})).unwrap();
1092        let list = s.execute("formulas.nameManager", json!({})).unwrap();
1093        let u = list.as_array().and_then(|l| l.iter().find(|n| n["name"] == "u")).cloned().unwrap();
1094        // The union parses as one, so the preview is what the union gives in a cell, not #NAME?.
1095        assert_eq!(u["value"], json!({"error": "#VALUE!"}));
1096    }
1097
1098    /// A workbook with `Sales` and `rate`, a hidden name, a name of Sheet1 that hides a workbook
1099    /// `Tax`, and a name of another sheet.
1100    fn named_workbook() -> Session {
1101        let mut s = Session::new();
1102        s.new_workbook();
1103        s.execute("formulas.defineName", json!({"name": "Sales", "refersTo": "=Sheet1!$A$1:$A$3"})).unwrap();
1104        s.execute("formulas.defineName", json!({"name": "rate", "refersTo": "=0.07"})).unwrap();
1105        s.execute("formulas.defineName", json!({"name": "Tax", "refersTo": "=0.2"})).unwrap();
1106        let d = s.doc_mut().unwrap();
1107        let wb = std::sync::Arc::make_mut(&mut d.wb);
1108        let name = |name: &str, scope, formula: &str, hidden| DefinedName {
1109            name: name.into(),
1110            scope,
1111            formula: formula.into(),
1112            comment: String::new(),
1113            hidden,
1114        };
1115        wb.names.push(name("_Filter", None, "Sheet1!$A$1:$B$9", true));
1116        wb.names.push(name("TAX", Some(0), "0.25", false));
1117        wb.names.push(name("Other", Some(1), "1", false));
1118        s
1119    }
1120
1121    fn text_at(s: &Session, cell: &str) -> Value {
1122        s.doc().unwrap().wb.active().unwrap().value(CellRef::parse(cell).unwrap())
1123    }
1124
1125    #[test]
1126    fn paste_names_lists_the_names_the_active_sheet_can_use() {
1127        let mut s = named_workbook();
1128        let r = s.execute("formulas.pasteName", json!({})).unwrap();
1129        assert_eq!(
1130            r["names"],
1131            json!([
1132                {"name": "rate", "refersTo": "=0.07"},
1133                {"name": "Sales", "refersTo": "=Sheet1!$A$1:$A$3"},
1134                {"name": "TAX", "refersTo": "=0.25"},
1135            ])
1136        );
1137        assert_eq!(text_at(&s, "A1"), Value::Empty, "listing the names writes nothing");
1138    }
1139
1140    #[test]
1141    fn paste_names_and_use_in_formula_ask_the_ui_to_insert_the_name() {
1142        let mut s = named_workbook();
1143        s.take_ui_requests();
1144        s.execute("formulas.pasteName", json!({"name": "Sales"})).unwrap();
1145        s.execute("formulas.useInFormula", json!({"name": "rate"})).unwrap();
1146        assert_eq!(
1147            s.take_ui_requests(),
1148            vec![crate::engine::UiRequest::InsertName("Sales".into()), crate::engine::UiRequest::InsertName("rate".into())]
1149        );
1150        assert_eq!(text_at(&s, "A1"), Value::Empty, "the cell changes only when the edit is entered");
1151        // A name Paste Name doesn't list is refused; one typed in another case is pasted as listed.
1152        assert!(s.execute("formulas.pasteName", json!({"name": "NoSuchName"})).is_err());
1153        s.execute("formulas.pasteName", json!({"name": "SALES"})).unwrap();
1154        assert_eq!(s.take_ui_requests(), vec![crate::engine::UiRequest::InsertName("Sales".into())]);
1155    }
1156
1157    #[test]
1158    fn paste_list_writes_each_name_and_what_it_refers_to() {
1159        let mut s = named_workbook();
1160        s.execute("selection.set", json!({"range": "D2"})).unwrap();
1161        let r = s.execute("formulas.pasteName", json!({"list": true})).unwrap();
1162        assert_eq!(r["range"], json!("D2:E4"));
1163        let rows = [("D2", "rate"), ("E2", "=0.07"), ("D3", "Sales"), ("E3", "=Sheet1!$A$1:$A$3"), ("D4", "TAX"), ("E4", "=0.25")];
1164        for (cell, text) in rows {
1165            assert_eq!(text_at(&s, cell), Value::text(text), "{cell}");
1166        }
1167        assert!(s.doc().unwrap().wb.active().unwrap().cell(CellRef::parse("E3").unwrap()).is_some_and(|c| c.formula.is_none()), "refers-to is text");
1168        s.execute("edit.undo", json!({})).unwrap();
1169        for (cell, _) in rows {
1170            assert_eq!(text_at(&s, cell), Value::Empty, "{cell} after Undo");
1171        }
1172        s.execute("formulas.pasteName", json!({"list": true, "at": "B7"})).unwrap();
1173        assert_eq!(text_at(&s, "B7"), Value::text("rate"));
1174    }
1175
1176    #[test]
1177    fn paste_list_needs_names_and_room() {
1178        let mut s = named_workbook();
1179        let last = format!("A{}", crate::base::MAX_ROWS - 1);
1180        assert!(s.execute("formulas.pasteName", json!({"list": true, "at": last})).is_err(), "three rows don't fit below the second-to-last row");
1181        let last_col = format!("{}1", crate::base::col_to_letters(crate::base::MAX_COLS - 1));
1182        assert!(s.execute("formulas.pasteName", json!({"list": true, "at": last_col})).is_err(), "two columns don't fit in the last column");
1183        let mut empty = Session::new();
1184        empty.new_workbook();
1185        assert!(empty.execute("formulas.pasteName", json!({"list": true})).is_err());
1186        assert_eq!(empty.execute("formulas.pasteName", json!({})).unwrap()["names"], json!([]));
1187    }
1188}