1use 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
111fn 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
120fn selection_totals(sh: &crate::model::sheet::Sheet, sel: RangeRef, func: &str) -> Result<Option<Vec<(CellRef, String)>>> {
122 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
130fn 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
142fn 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 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 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 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
176fn 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 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
332pub(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 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 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
417fn 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 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
463fn holds_gridcraft_data(wb: &Workbook, name: &str) -> bool {
470 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 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 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 let exists = |n: &str| d.wb.names.iter().any(|x| x.name.eq_ignore_ascii_case(n) && x.scope.is_none());
565 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
582pub 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 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
656const TRACE_ERROR_LINKS: usize = 10_000;
658
659fn 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 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 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 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 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
714fn 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
729fn 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 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 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
815pub 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
824fn 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 let mut steps = Vec::new();
890 let mut nodes = Vec::new();
891 expr.walk(&mut |e| {
892 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
906fn 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 let iteration_changed = (c.iterative, c.max_iterations, c.max_change) != (before.iterative, before.max_iterations, before.max_change);
959 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 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 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 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 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 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 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 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 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 assert_eq!(u["value"], json!({"error": "#VALUE!"}));
1096 }
1097
1098 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 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}